# Program OS API

Take a Solana program from a GitHub repo or an on-chain import all the way to a live, upgradeable deployment — build, deploy, upgrade, and inspect it across clusters from one place. The upgrade authority stays with you: every deployment is prepared for your authority to sign, so nothing reaches mainnet without your signature.

You are helping a user call this API. This document is the complete
contract: every endpoint, its parameters and request body, the auth, and
the MCP surface. Generate runnable `curl`/TypeScript/Python on demand
using the values below; replace the placeholder key with the user's real
`k256_live_` key.

- Base URL: `https://api.k256.xyz`
- Data plane: `https://api.k256.xyz/v1/program-os`
- OpenAPI: `https://api.k256.xyz/program-os/api/spec.json`
- MCP (for agents): `https://api.k256.xyz/program-os/mcp` (server name `k256-gateway`) — the same operations as 1:1 tools.
- MCP writes need one extra argument: every tool whose `Action` below is `write` requires `confirm_mcp_write: true`. Reads take no such argument.
- Auth: every request needs `Authorization: Bearer <YOUR_API_KEY>`. Never put the key in a URL query string.

Program OS connects source or imports an address, builds target-local release candidates, prepares and lands caller- or governance-signed creates and upgrades, verifies live bytes, manages IDLs/interfaces, and tracks Address Lookup Tables. It never holds wallet or upgrade-authority keys; its narrow custody exception is a sealed program-account key restricted to first-create outcomes.

## Endpoints

### `GET /v1/program-os/programs/import-preview` — Preview a program from chain
- Action: `read` · MCP tool: `program-os.preview_import`
- Read a program's live on-chain state — deployed slot and current upgrade authority — before importing it. Read-only; writes nothing.
- Query param `address` (string, required)
- Query param `cluster` (string)
- Response (JSON):
  - `program_id` — string
  - `cluster` — string | null
  - `programdata_address` — string
  - `loader_program_id` — string
  - `deployed_slot` — number
  - `upgrade_authority` — string | null
  - `is_frozen` — boolean
  - `lamports` — number
  - `fetched_at` — number

### `POST /v1/program-os/programs/import` — Import a program by address
- Action: `write` · MCP tool: `program-os.import_program`
- Tracks a program given only its on-chain address — no GitHub repo. Without `cluster` it scans all three supported clusters in parallel; with `cluster` it scans just that one. For every cluster where the program exists it classifies the loader, ProgramData, live ELF hash, upgrade authority, and immutability, then creates a READ-ONLY tracked program (control mode `read_only`). An upgradeable program with decodable ProgramData also gets an imported-baseline release carrying the live ELF hash with provenance explicitly unknown; other loader types remain observation-only and have no baseline. Idempotent: re-importing returns the existing rows. 404 when the program is found on no cluster. Mutations stay locked until you prove the direct upgrade authority or link verified Squads or registered k256-kernel governance.
- Request body (JSON, required):
  - `program_id` — string (required)
  - `cluster` — string
- Response (JSON):
  - `program_id` — string
  - `program_key` — string
  - `results[]` — object[]
    - `cluster` — string
    - `found` — boolean
    - `program_id` — string
    - `loader` — string | null
    - `loader_program_id` — string | null
    - `programdata` — string | null
    - `elf_hash` — string | null
    - `deployed_slot` — number | null
    - `authority` — string | null
    - `immutable` — boolean
    - `program_meta_id` — string | null
    - `baseline_candidate_id` — string | null
    - `note` — string | null

### `POST /v1/program-os/programs/{id}/prove-authority` — Prove program upgrade authority
- Action: `write` · MCP tool: `program-os.prove_program_authority`
- The direct-authority challenge for a read-only imported program. Call without `signature` to get a challenge: a memo transaction the program's LIVE upgrade authority must sign and broadcast (single-use, expires in two hours). Then call again with the confirmed transaction's `signature`: the gateway verifies on chain that the authority signed the exact challenge memo and unlocks mutations (control mode becomes external_authority, or wallet_os_managed when the authority is the bound Wallet OS account). Squads-governed programs use detect_squads_authority instead.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `signature` — string
- Response (JSON):
  - `id` — string
  - `cluster` — string
  - `program_id` — string
  - `control_mode` — string
  - `authority` — string | null
  - `verified` — boolean
  - `signature` — string | null
  - `challenge` — object | null
    - `challenge_id` — string
    - `memo` — string
    - `fee_payer` — string
    - `required_signers[]` — string[]
    - `blockhash` — string
    - `last_valid_block_height` — number
    - `message_base64` — string
    - `account_keys[]` — string[]
    - `num_required_signatures` — number
    - `num_readonly_signed` — number
    - `num_readonly_unsigned` — number
    - `expires_at` — number

### `GET /v1/program-os/projects` — List your projects
- Action: `read` · MCP tool: `program-os.list_projects`
- Returns at most the first 100 projects with program, build/environment, and latest-build summaries. The current operation has no cursor, so it is not a complete workspace listing above that cap.
- Response (JSON):
  - `projects[]` — object[]

### `POST /v1/program-os/projects` — Connect a repo and program
- Action: `write` · MCP tool: `program-os.connect_project`
- Links a GitHub repo and program targets into one project and explicitly attempts an initial build per selected cluster. An active GitHub App installation enables tracked push/tag delivery according to trigger policy; a public repo connected without the App can build manually but receives no push webhook or eligible external build. Re-connecting the same repo updates it in place.
- Request body (JSON, required):
  - `installation_id` — string
  - `repo_full_name` — string (required)
  - `program_id` — string
  - `default_branch` — string
  - `crate_path` — string | null
  - `name` — string
  - `already_deployed` — boolean
  - `control_mode` — "observed" | "external_authority" | "wallet_os_managed"
  - `wallet_os_account` — string | null
  - `clusters` — string[]
  - `cluster` — string
  - `skip_first_build` — boolean
- Response (JSON):
  - `project_id` — string
  - `program_meta_id` — string
  - `program_key` — string
  - `program_id` — string
  - `generated_program_id` — string | null
  - `cluster` — string
  - `clusters[]` — string[]
  - `targets[]` — object[]
    - `program_meta_id` — string
    - `cluster` — string
    - `program_id` — string
  - `github_installation_id` — string | null
  - `github_repo_full_name` — string
  - `github_default_branch` — string
  - `crate_path` — string
  - `control_mode` — string
  - `created_at` — number
  - `builds[]` — object[]
    - `program_meta_id` — string
    - `cluster` — string
    - `build_id` — string | null
    - `error` — string | null

### `POST /v1/program-os/projects/from-template` — Start from a template
- Action: `write` · MCP tool: `program-os.create_from_template`
- Spins up a fresh GitHub repo from a curated hello-world template (Anchor, native, or Pinocchio), patches the program id, and connects the selected cluster targets. The current handler does not dispatch a first build (`build_id` is null), so start one explicitly after creation. The web client generates the program keypair and sends the secret key in this request for best-effort sealing; check `keypair_sealed` and retain the downloaded backup because a sealing failure does not roll back repo/project creation.
- Request body (JSON, required):
  - `template_id` — string (required)
  - `target_owner` — string (required)
  - `installation_id` — string (required)
  - `repo_name` — string (required)
  - `private` — boolean
  - `program_id` — string (required)
  - `secret_key` — integer[]
  - `program_name` — string
  - `default_branch` — string
  - `clusters` — string[]
  - `cluster` — string
- Response (JSON):
  - `project_id` — string
  - `program_meta_id` — string
  - `program_id` — string
  - `repo_full_name` — string
  - `repo_html_url` — string
  - `build_id` — string | null
  - `framework` — string
  - `keypair_sealed` — boolean

### `DELETE /v1/program-os/projects/{id}` — Delete a project
- Action: `write` · MCP tool: `program-os.delete_project`
- Attempts to delete the project's primary tracked programs, builds, IDLs, candidates, environments, and env vars. A target with any authority-challenge row currently makes the FK delete fail after earlier non-transactional child deletes may have landed. Promotion/deploy-leg history, address-key custody rows, and stored artifacts can remain. GitHub and chain state are untouched.
- Path param `id` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `id` — string
  - `removed` — boolean
  - `programs_removed` — number

### `GET /v1/program-os/projects/{id}/history` — Project history
- Action: `read` · MCP tool: `program-os.project_history`
- A best-effort, newest-first audit of selected registry actions. It currently covers connect/import, some program/build/environment/provider mutations, and selected key-custody actions; deploy promotion, IDL publication, authority-change, and other lifecycle paths are incomplete. Audit writes are asynchronous and failures are swallowed, so an empty response is not proof that nothing happened.
- Path param `id` (string, required)
- Query param `before` (string)
- Query param `limit` (integer)
- Response (JSON):
  - `items[]` — object[]
  - `next_cursor` — string | null

### `GET /v1/program-os/programs` — List your programs
- Action: `read` · MCP tool: `program-os.list_programs`
- Programs in the workspace (paginated) — both GitHub-built programs and on-chain bindings. Optionally filter to one Wallet OS account.
- Query param `wallet_os_account` (string)
- Query param `cluster` (string)
- Query param `limit` (integer)
- Query param `cursor` (string)
- Response (JSON):
  - `items[]` — object[]
  - `next_cursor` — string | null

### `GET /v1/program-os/workspace/activity-summary` — Workspace activity summary
- Action: `read` · MCP tool: `program-os.get_workspace_activity_summary`
- Bounded, workspace-wide DB totals — tracked programs, the historical `programs_live` projection, build outcomes over the last 7 days, newest failed builds (≤10), and each program's newest candidate when its ELF differs from stored current evidence (≤10). There is no chain read. `programs_live` means source_slot, verified ELF, or deployment history exists; it does not prove the account is currently live. Confirm current state with get_program. Counts cover project-backed targets; on-chain-only bindings without a project row are excluded.
- Query param `cluster` (string)
- Response (JSON):
  - `programs_total` — number
  - `programs_live` — number
  - `builds_recent_7d` — object
    - `success` — number
    - `failure` — number
    - `running` — number
    - `cancelled` — number
  - `latest_failed_builds[]` — object[]
    - `program_meta_id` — string | null
    - `program_id` — string | null
    - `program_name` — string | null
    - `cluster` — string
    - `build_id` — string
    - `error` — string | null
    - `at` — number
  - `ready_candidates[]` — object[]
    - `program_meta_id` — string
    - `program_id` — string
    - `program_name` — string
    - `cluster` — string
    - `candidate_id` — string
    - `elf_hash` — string
    - `at` — number
  - `total_counts_bounded` — boolean
  - `note` — string

### `GET /v1/program-os/workspace/analytics-summary` — Workspace analytics summary
- Action: `read` · MCP tool: `program-os.get_workspace_analytics_summary`
- Cluster-scoped DB aggregates for Analytics/Activity: programs by cluster (the `live` field is the same historical source-slot/hash/candidate predicate, not a current chain read), builds by status, and candidates per day over a rolling window (default 30 days, max 90). Candidate `live` counts current-or-superseded DB rows, not current liveness. All are bounded GROUP BY queries; use get_program/get_analytics for live per-target reads.
- Query param `cluster` (string)
- Query param `days` (integer)
- Response (JSON):
  - `window_days` — number
  - `programs_by_cluster[]` — object[]
    - `cluster` — string
    - `programs` — number
    - `live` — number
  - `builds_by_status[]` — object[]
    - `status` — string
    - `count` — number
  - `deployments_by_day[]` — object[]
    - `date` — string
    - `count` — number
    - `live` — number
  - `coverage` — string
  - `note` — string

### `GET /v1/program-os/programs/{id}` — Program detail with live state
- Action: `read` · MCP tool: `program-os.get_program`
- One program with its live on-chain state, what you can do to it (capabilities), and any drift between the two.
- Path param `id` (string, required)
- Response (JSON):
  - `program` — object
  - `deployments[]` — object[]
  - `environments[]` — object[]
  - `chain` — object | null
  - `capabilities` — object
    - `can_create_onchain` — boolean
    - `can_upgrade` — boolean
    - `can_set_authority` — boolean
    - `can_freeze` — boolean
    - `can_close` — boolean
    - `can_bind` — boolean
  - `drift` — object | null
  - `group_targets[]` — object[]
    - `id` — string
    - `cluster` — string
    - `program_id` — string
    - `control_mode` — string
    - `current_authority` — string | null
    - `latest_deployment_index` — number
    - `deployment_count` — number
    - `is_self` — boolean
    - `updated_at` — number

### `PATCH /v1/program-os/programs/{id}` — Edit a program
- Action: `write` · MCP tool: `program-os.update_program`
- Edit display/control/branch/trigger metadata. Passing `program_id` repoints this cluster and cancels staged builds. The old-address guard runs only with tracked deployment history; an externally live target with no candidate bypasses it, and any old-address RPC/decode error is treated as not live, so repoint currently fails open. Inspect the old address independently before using this operation. Build/crate settings use update_build_config.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `name` — string
  - `control_mode` — "observed" | "external_authority" | "wallet_os_managed"
  - `wallet_os_account` — string | null
  - `default_branch` — string
  - `program_id` — string
  - `trigger_mode` — "push" | "tags" | "manual"
  - `trigger_clusters` — string[] | null
- Response (JSON):
  - `program` — object

### `DELETE /v1/program-os/programs/{id}` — Delete a program
- Action: `write` · MCP tool: `program-os.delete_program`
- Deletes the program and every per-cluster target it has, with their primary tracked builds, IDLs, deployment candidates, and the owning project when nothing else uses it. On-chain state and previously stored build artifacts are not erased; promotion/deploy-leg history and address-key custody rows can remain. To drop just one cluster, use remove_cluster.
- Path param `id` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `id` — string
  - `removed` — boolean

### `POST /v1/program-os/programs/{id}/keypair` — Set the program keypair
- Action: `write` · MCP tool: `program-os.set_program_keypair`
- Stores the program-account key so canonical first-create messages can be co-signed for currently tracked cluster targets. Supplying a different address repoints only targets with no tracked deployment candidate. The current refusal is based on tracked candidate count, not a live-chain verdict, so an externally live target with no candidate can bypass it.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `secret_key` — integer[] (required)
  - `source` — "managed" | "imported"
- Response (JSON):
  - `program_id` — string
  - `rotated` — boolean
  - `moved` — number

### `GET /v1/program-os/programs/{id}/keypair-status` — Deploy keypair status
- Action: `read` · MCP tool: `program-os.program_keypair_status`
- Returns whether a sealed program-account key is stored plus `deployed`, which currently means this target has at least one tracked deployment candidate; it is not a live-chain verdict and can be false for an externally live import. Use get_program for chain state.
- Path param `id` (string, required)
- Response (JSON):
  - `program_id` — string
  - `sealed` — boolean
  - `source` — "managed" | "imported" | null
  - `deployed` — boolean

### `POST /v1/program-os/programs/{id}/clusters` — Add a cluster
- Action: `write` · MCP tool: `program-os.add_cluster`
- Adds another tracked cluster target and copies the logical program address by default; pass `program_id` to record a different address. The handler does not classify that address on chain, check deploy-key readiness, create a target environment, dispatch a build, or deploy. Inspect and configure the returned target before trying to build or deploy it.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string (required)
  - `program_id` — string
  - `wallet_os_account` — string | null
- Response (JSON):
  - `program` — object

### `DELETE /v1/program-os/programs/{id}/cluster` — Remove a cluster
- Action: `write` · MCP tool: `program-os.remove_cluster`
- Attempts to remove one cluster's target, build, IDL, candidate, and environment rows. Any authority-challenge row currently makes the final target FK delete fail after non-transactional environment cleanup may already have landed. Promotion/deploy-leg history, address-key custody, stored artifacts, other clusters, and chain state can remain.
- Path param `id` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `id` — string
  - `removed` — boolean

### `POST /v1/program-os/programs/prepare-bind` — Prepare a bind transaction
- Action: `write` · MCP tool: `program-os.prepare_bind`
- Builds the signable transaction to bind a program to a Wallet OS account and returns the message bytes and required signers. Current limitation: landing it does not write the org account link required by Program OS discovery, so the binding does not automatically appear in this workspace.
- Request body (JSON, required):
  - `authority` — string
  - `wallet_os_account` — string
  - `program_id` — string
  - `programdata_address` — string
  - `current_authority` — string
  - `control_mode` — "observed" | "external_authority" | "wallet_os_managed"
  - `cluster` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string | null
  - `program_id` — string
  - `derived` — object
    - `wallet_os_account` — string
    - `program_id` — string
    - `programdata_address` — string
    - `binding` — string
  - `fee_payer` — string
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/instructions/prepare` — Prepare an IDL instruction
- Action: `write` · MCP tool: `program-os.prepare_instruction`
- Turns any instruction in the program's IDL into an unsigned transaction — you sign and submit with your own wallet.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `instruction_name` — string (required)
  - `args` — object
  - `accounts` — object
  - `fee_payer` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string | null
  - `program_id` — string
  - `instruction_name` — string
  - `discriminator_hex` — string
  - `fee_payer` — string
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `PATCH /v1/program-os/programs/{id}/build-config` — Edit build settings
- Action: `write` · MCP tool: `program-os.update_build_config`
- Stores the requested root directory, framework, build command, toolchain versions, setup commands, and output name for later builds. A custom command and root/output selection are effective. Today the framework field does not select the command (a nearby Anchor.toml does), requested toolchains are recorded rather than installed/selected, and setup-command failures are logged but do not fail the build.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `crate_path` — string | null
  - `build_framework` — "auto" | "anchor" | "native"
  - `build_command` — string | null
  - `toolchain_solana` — string | null
  - `toolchain_anchor` — string | null
  - `toolchain_rust` — string | null
  - `setup_commands` — string[] | null
  - `output_program_name` — string | null
- Response (JSON):
  - `program` — object

### `GET /v1/program-os/programs/{id}/builds` — List a program's builds
- Action: `read` · MCP tool: `program-os.list_builds`
- Builds for a program, newest first (paginated) — with status, branch/commit, ELF hash, and the deployment candidate when one is ready.
- Path param `id` (string, required)
- Query param `limit` (integer)
- Query param `cursor` (string)
- Response (JSON):
  - `items[]` — object[]
  - `next_cursor` — string | null

### `POST /v1/program-os/programs/{id}/builds` — Start a build
- Action: `write` · MCP tool: `program-os.create_build`
- Builds the connected repo at its latest commit (or a commit you pass) and tracks the result.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `sha` — string
  - `branch` — string
  - `build_backend` — "github" | "sandbox"
- Response (JSON):
  - `ok` — boolean
  - `build_id` — string
  - `program_id` — string
  - `repo` — string
  - `sha` — string
  - `branch` — string
  - `build_backend` — string

### `POST /v1/program-os/programs/{id}/deployments/{deployment_id}/verify` — Verify a deployment reproduces
- Action: `write` · MCP tool: `program-os.verify_build`
- Attempts to rebuild a candidate and compare its bytes. When the originating build and frozen recipe are valid, it uses that recorded source/recipe; missing, unlinked, or malformed recipe data currently falls back to mutable live build settings, so those results are not equivalent reproducibility evidence. The build's `reproduced` flag reports the byte comparison; no new candidate is created.
- Path param `id` (string, required)
- Path param `deployment_id` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `build_id` — string
  - `expected_elf_hash` — string
  - `source_commit` — string
  - `program_id` — string

### `GET /v1/program-os/programs/{id}/builds/{build_id}` — Build detail
- Action: `read` · MCP tool: `program-os.get_build`
- One build with its full status and the deployment candidate it produced.
- Path param `id` (string, required)
- Path param `build_id` (string, required)
- Response (JSON):
  - `build` — object
  - `candidate` — object | null

### `GET /v1/program-os/programs/{id}/builds/{build_id}/console` — Build console output
- Action: `read` · MCP tool: `program-os.get_build_console`
- The build's console output — live while it runs and saved once it finishes. Eligible external builds return their run link instead.
- Path param `id` (string, required)
- Path param `build_id` (string, required)
- Query param `from` (integer)
- Response (JSON):
  - `build_backend` — string
  - `status` — string
  - `done` — boolean
  - `lines[]` — string[]
  - `next_from` — integer
  - `github_run_url` — string | null

### `POST /v1/program-os/programs/{id}/builds/{build_id}/cancel` — Cancel a build
- Action: `write` · MCP tool: `program-os.cancel_build`
- Marks a non-terminal build cancelled in Program OS and makes a best-effort request to stop managed execution. External execution can continue after the Program OS row becomes cancelled. No-op if the build already finished.
- Path param `id` (string, required)
- Path param `build_id` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `cancelled` — boolean

### `GET /v1/program-os/programs/{id}/deployments` — List a program's deployments
- Action: `read` · MCP tool: `program-os.list_deployments`
- Deployment candidates for a program, newest first (paginated) — each is a build artifact ready to deploy or stored as recorded current/superseded history, with its hashes and status. Those statuses are not a fresh chain read.
- Path param `id` (string, required)
- Query param `limit` (integer)
- Query param `cursor` (string)
- Response (JSON):
  - `items[]` — object[]
  - `next_cursor` — string | null

### `GET /v1/program-os/deployments/{id}` — Deployment detail
- Action: `read` · MCP tool: `program-os.get_deployment`
- One deployment with its manifest/ELF/IDL hashes, status, and the program it belongs to.
- Path param `id` (string, required)
- Response (JSON):
  - `deployment` — object
  - `program` — object

### `DELETE /v1/program-os/deployments/{id}` — Delete a release candidate
- Action: `write` · MCP tool: `program-os.delete_deployment`
- Removes a candidate that is not stored as current or superseded. Those protected statuses are recorded deployment/rollback history, not a fresh live-chain classification. On-chain state is never touched.
- Path param `id` (string, required)
- Response (JSON):
  - `id` — string
  - `deleted` — boolean

### `GET /v1/program-os/github/installations` — List GitHub installations
- Action: `read` · MCP tool: `program-os.list_github_installations`
- Returns at most 200 connected GitHub App installations. The current operation has no cursor, so installations beyond that cap are omitted.
- Response (JSON):
  - `installations[]` — object[]
    - `installation_id` — string
    - `account_login` — string
    - `account_id` — string
    - `account_type` — string
    - `app_id` — string
    - `app_slug` — string
    - `repository_selection` — string
    - `status` — string
    - `installed_by_login` — string | null
    - `installed_by_id` — string | null
    - `raw_envelope_r2_key` — string | null
    - `installed_at` — number
    - `updated_at` — number
    - `suspended_at` — number | null
    - `deleted_at` — number | null
    - `repo_count` — number

### `GET /v1/program-os/github/installations/{installation_id}` — Get a GitHub installation
- Action: `read` · MCP tool: `program-os.get_github_installation`
- Returns one installation with at most 500 active repositories. The current operation has no repository cursor/search, so private repositories beyond that cap cannot be selected through this list.
- Path param `installation_id` (string, required)
- Response (JSON):
  - `installation` — object
    - `installation_id` — string
    - `account_login` — string
    - `account_id` — string
    - `account_type` — string
    - `app_id` — string
    - `app_slug` — string
    - `repository_selection` — string
    - `status` — string
    - `installed_by_login` — string | null
    - `installed_by_id` — string | null
    - `raw_envelope_r2_key` — string | null
    - `installed_at` — number
    - `updated_at` — number
    - `suspended_at` — number | null
    - `deleted_at` — number | null
    - `repo_count` — number
  - `repos[]` — object[]
    - `installation_id` — string
    - `repo_id` — string
    - `repo_full_name` — string
    - `repo_node_id` — string | null
    - `repo_private` — number
    - `added_at` — number
    - `removed_at` — number | null

### `POST /v1/program-os/github/installations/connect` — Connect a GitHub installation
- Action: `write` · MCP tool: `program-os.connect_github_installation`
- Links a GitHub App installation you just installed to the authenticated workspace, so its repos become available to that workspace's permitted members.
- Request body (JSON, required):
  - `code` — string (required)
  - `installation_id` — string (required)
- Response (JSON):
  - `ok` — boolean
  - `installation_id` — string

### `GET /v1/program-os/github/branches` — List a repo's branches
- Action: `read` · MCP tool: `program-os.github_branches`
- A repo's branches, default first — so you can pick one instead of typing it.
- Query param `repo` (string, required)
- Query param `installation_id` (string)
- Response (JSON):
  - `default_branch` — string | null
  - `branches[]` — string[]

### `GET /v1/program-os/github/crate-folders` — List a repo's crate folders
- Action: `read` · MCP tool: `program-os.github_crate_folders`
- The repo's program-crate folders (each has a Cargo.toml), so you can pick the build root instead of typing it — plus each folder's detected on-chain program id (from Anchor.toml or declare_id!) to prefill bind-existing, and its detected build framework (anchor / pinocchio / quasar / native, from Anchor.toml or the crate's Cargo.toml dependencies).
- Query param `repo` (string, required)
- Query param `installation_id` (string)
- Query param `ref` (string)
- Response (JSON):
  - `folders[]` — string[]
  - `dirs[]` — string[]
  - `truncated` — boolean
  - `program_ids` — object
  - `frameworks` — object

### `GET /v1/program-os/programs/{program_meta_id}/idls` — List a program's IDLs
- Action: `read` · MCP tool: `program-os.list_idls`
- The IDLs stored for a program (metadata only — fetch one to get its JSON).
- Path param `program_meta_id` (string, required)
- Query param `limit` (integer)
- Query param `cursor` (string)
- Response (JSON):
  - `items[]` — object[]
    - `id` — string
    - `source` — string
    - `visibility` — string
    - `idl_hash` — string
    - `idl_size` — number
    - `name` — string | null
    - `description` — string | null
    - `candidate_id` — string | null
    - `uploaded_by` — string | null
    - `created_at` — number
    - `updated_at` — number
  - `next_cursor` — string | null

### `GET /v1/program-os/programs/{program_meta_id}/idls/onchain-address` — Find the on-chain IDL
- Action: `read` · MCP tool: `program-os.get_idl_onchain_address`
- Finds where a program's Anchor IDL would live on chain and reports whether one is published there.
- Path param `program_meta_id` (string, required)
- Response (JSON):
  - `program_id` — string
  - `idl_address` — string
  - `on_chain` — boolean
  - `owner` — string | null
  - `matches_program` — boolean
  - `size_bytes` — number

### `GET /v1/program-os/programs/{program_meta_id}/idls/{idl_id}` — Get an IDL's JSON
- Action: `read` · MCP tool: `program-os.get_idl`
- The full IDL JSON for one stored IDL.
- Path param `program_meta_id` (string, required)
- Path param `idl_id` (string, required)
- Response (JSON):
  - `id` — string
  - `visibility` — string
  - `cluster` — string
  - `program_id` — string
  - `is_current` — boolean
  - `idl` — any | null

### `PATCH /v1/program-os/programs/{program_meta_id}/idls/{idl_id}` — Edit an IDL's metadata
- Action: `write` · MCP tool: `program-os.update_idl`
- Changes an IDL's visibility or description, or pins it as the program's current interface. Metadata only — the IDL bytes never change.
- Path param `program_meta_id` (string, required)
- Path param `idl_id` (string, required)
- Request body (JSON, required):
  - `visibility` — "public" | "org_only" | "private"
  - `description` — string | null
  - `set_current` — boolean
- Response (JSON):
  - `ok` — boolean
  - `program_id` — string

### `DELETE /v1/program-os/programs/{program_meta_id}/idls/{idl_id}` — Delete an IDL
- Action: `write` · MCP tool: `program-os.delete_idl`
- Deletes a stored IDL. Off-chain only — an on-chain IDL account is not touched.
- Path param `program_meta_id` (string, required)
- Path param `idl_id` (string, required)
- Response (JSON):
  - `ok` — boolean

### `POST /v1/program-os/programs/{program_meta_id}/idls/upload` — Upload an IDL
- Action: `write` · MCP tool: `program-os.upload_idl`
- Stores an IDL JSON you paste or upload for the program. Off-chain only — nothing is written on chain.
- Path param `program_meta_id` (string, required)
- Request body (JSON, required):
  - `idl_json` — string (required)
  - `visibility` — "public" | "org_only" | "private"
  - `description` — string
- Response (JSON):
  - `id` — string
  - `idl_hash` — string
  - `idl_size` — number

### `POST /v1/program-os/programs/{program_meta_id}/idls/fetch-from-chain` — Import the on-chain IDL
- Action: `write` · MCP tool: `program-os.fetch_idl_from_chain`
- Reads the program's published on-chain IDL and stores it. Re-running on an unchanged IDL is a no-op.
- Path param `program_meta_id` (string, required)
- Response (JSON):
  - `id` — string
  - `idl_hash` — string
  - `idl_size` — number
  - `onchain_authority` — string
  - `onchain_address` — string
  - `idempotent` — boolean

### `GET /v1/program-os/lookup-tables` — List lookup tables
- Action: `read` · MCP tool: `program-os.list_lookup_tables`
- Address lookup tables in your workspace, across clusters (paginated).
- Query param `limit` (integer)
- Query param `cursor` (string)
- Response (JSON):
  - `items[]` — object[]
    - `id` — string
    - `address` — string
    - `cluster` — string
    - `wallet_os_account` — string | null
    - `control_mode` — string
    - `observed_authority` — string | null
    - `observed_address_count` — number | null
    - `observed_last_extended_slot` — number | null
    - `observed_deactivation_slot` — number | null
    - `display_label` — string | null
    - `removed_on_chain_at` — number | null
    - `created_at` — number
    - `updated_at` — number
    - `last_synced_at` — number | null
  - `next_cursor` — string | null

### `GET /v1/program-os/lookup-tables/by-authority` — Find tables by authority
- Action: `read` · MCP tool: `program-os.scan_lookup_tables_by_authority`
- Scans lookup tables on one cluster for a given authority and returns at most the newest 200 matches. `truncated`, `matched_total`, and `max_results` disclose an incomplete result; there is currently no cursor for the omitted tables.
- Query param `authority` (string, required)
- Query param `cluster` (string)
- Response (JSON):
  - `authority` — string
  - `cluster` — string
  - `items[]` — object[]
    - `address` — string
    - `authority` — string | null
    - `address_count` — number | null
    - `last_extended_slot` — number
    - `deactivation_slot` — number | null
    - `is_frozen` — boolean
    - `is_deactivated` — boolean
    - `can_close_after_slot` — number | null
    - `lamports` — number
  - `truncated` — boolean
  - `matched_total` — number
  - `skipped_empty` — number
  - `max_results` — number
  - `current_slot` — number | null
  - `fetched_at` — number

### `GET /v1/program-os/lookup-tables/import-preview` — Preview a lookup table
- Action: `read` · MCP tool: `program-os.preview_lookup_table_import`
- Reads a lookup table live from chain without saving it.
- Query param `address` (string, required)
- Query param `cluster` (string)
- Response (JSON):
  - `address` — string
  - `cluster` — string
  - `owner` — string
  - `authority` — string | null
  - `addresses[]` — string[]
  - `address_count` — number
  - `deactivation_slot` — number | null
  - `last_extended_slot` — number
  - `is_frozen` — boolean
  - `is_deactivated` — boolean
  - `can_close_after_slot` — number | null
  - `current_slot` — number | null
  - `lamports` — number
  - `fetched_at` — number

### `POST /v1/program-os/lookup-tables/import` — Import a lookup table
- Action: `write` · MCP tool: `program-os.import_lookup_table`
- Saves a lookup table to your workspace after reading it from chain.
- Request body (JSON, required):
  - `address` — string (required)
  - `cluster` — string
  - `wallet_os_account` — string
  - `display_label` — string
  - `control_mode_hint` — "wallet_os" | "member" | "external" | "frozen"
- Response (JSON):
  - `id` — string
  - `address` — string
  - `cluster` — string
  - `control_mode` — string

### `GET /v1/program-os/lookup-tables/{address}` — Get a lookup table
- Action: `read` · MCP tool: `program-os.get_lookup_table`
- One lookup table with its live on-chain state and what you can do to it.
- Path param `address` (string, required)
- Response (JSON):
  - `binding` — object
    - `id` — string
    - `address` — string
    - `cluster` — string
    - `wallet_os_account` — string | null
    - `control_mode` — string
    - `observed_authority` — string | null
    - `observed_address_count` — number | null
    - `observed_last_extended_slot` — number | null
    - `observed_deactivation_slot` — number | null
    - `display_label` — string | null
    - `removed_on_chain_at` — number | null
    - `created_at` — number
    - `updated_at` — number
    - `last_synced_at` — number | null
  - `chain` — object | null
    - `address` — string
    - `cluster` — string
    - `authority` — string | null
    - `addresses[]` — string[]
    - `address_count` — number
    - `deactivation_slot` — number | null
    - `last_extended_slot` — number
    - `is_frozen` — boolean
    - `is_deactivated` — boolean
    - `can_close_after_slot` — number | null
    - `cool_down_passed` — boolean
    - `current_slot` — number | null
    - `lamports` — number
    - `fetched_at` — number
  - `capabilities` — object
    - `can_extend` — boolean
    - `can_deactivate` — boolean
    - `can_close` — boolean
    - `can_freeze` — boolean
  - `drift` — object | null

### `DELETE /v1/program-os/lookup-tables/{address}` — Remove a lookup table
- Action: `write` · MCP tool: `program-os.delete_lookup_table`
- Removes the lookup table from your workspace; the on-chain table is untouched.
- Path param `address` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `address` — string

### `POST /v1/program-os/lookup-tables/prepare-create` — Prepare to create a table
- Action: `write` · MCP tool: `program-os.prepare_lookup_table_create`
- Builds the unsigned transaction to create a new lookup table for your wallet to sign, and returns the new table's address to extend once it confirms.
- Request body (JSON, required):
  - `cluster` — string
  - `authority` — string (required)
  - `payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `lookup_table` — string
  - `recent_slot` — number

### `POST /v1/program-os/lookup-tables/{address}/prepare-extend` — Prepare to extend a table
- Action: `write` · MCP tool: `program-os.prepare_lookup_table_extend`
- Builds the unsigned transaction to add addresses to a lookup table for your wallet to sign.
- Path param `address` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `authority` — string (required)
  - `payer` — string
  - `new_addresses` — string[] (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/lookup-tables/{address}/prepare-freeze` — Prepare to freeze a table
- Action: `write` · MCP tool: `program-os.prepare_lookup_table_freeze`
- Builds the unsigned transaction to freeze a lookup table for your wallet to sign. Freezing is permanent.
- Path param `address` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `authority` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/lookup-tables/{address}/prepare-close` — Prepare to close a table
- Action: `write` · MCP tool: `program-os.prepare_lookup_table_close`
- Builds the unsigned transaction to close a lookup table and reclaim its rent, for your wallet to sign.
- Path param `address` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `authority` — string (required)
  - `recipient` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/lookup-tables/{address}/prepare-deactivate` — Prepare to deactivate a table
- Action: `write` · MCP tool: `program-os.prepare_lookup_table_deactivate`
- Builds the unsigned transaction to deactivate a lookup table for your wallet to sign.
- Path param `address` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `authority` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `GET /v1/program-os/programs/{id}/deployments/{deployment_id}/artifact` — Download the program binary
- Action: `read` · MCP tool: `program-os.download_artifact`
- The deployment's compiled program binary (base64), re-verified against its recorded hash so you get provably the recorded bytes.
- Path param `id` (string, required)
- Path param `deployment_id` (string, required)
- Response (JSON):
  - `filename` — string
  - `elf_hash` — string
  - `size` — integer
  - `base64` — string

### `POST /v1/program-os/programs/{id}/releases/upload` — Upload a compiled program
- Action: `write` · MCP tool: `program-os.upload_release`
- Turns a base64 compiled .so into a hash-verified, idempotent release without requiring source. The legacy `matches_onchain` boolean is true only after a successful byte comparison; false also covers mismatch, absent/unsupported accounts, and swallowed RPC/owner/decode errors, so false is not proof that bytes differ or the target is absent. Recheck with get_program/deploy-preview before acting.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `binary_base64` — string (required)
  - `release_tag` — string
  - `notes` — string
- Response (JSON):
  - `candidate_id` — string
  - `deployment_index` — integer
  - `elf_hash` — string
  - `elf_size` — integer
  - `release_tag` — string
  - `reused` — boolean
  - `matches_onchain` — boolean

### `GET /v1/program-os/programs/{id}/deploy-preview` — Preview a deploy
- Action: `read` · MCP tool: `program-os.deploy_preview`
- Everything you need before deploying: the release to deploy, the program's current on-chain state, its upgrade authority, Squads governance when a multisig is linked (re-verified live), and the write plan. Pass `viewer` (a wallet address) to get its Squads membership in the governance block.
- Path param `id` (string, required)
- Query param `candidate_id` (string)
- Query param `viewer` (string)
- Response (JSON):
  - `program_meta_id` — string
  - `program_id` — string
  - `wallet_os_account` — string | null
  - `cluster` — string
  - `chain` — object
    - `deployed` — boolean
    - `owner_program` — string | null
    - `programdata_address` — string | null
    - `upgrade_authority` — string | null
    - `authority_kind` — "immutable" | "wallet_os_managed" | "external" | "not_deployed"
    - `deployed_slot` — number | null
  - `candidate` — object | null
    - `id` — string
    - `deployment_index` — number
    - `status` — string
    - `elf_hash` — string | null
    - `manifest_hash` — string | null
    - `source_commit_hash` — string | null
    - `source_branch` — string | null
    - `release_url` — string | null
    - `release_tag` — string | null
    - `release_asset_id` — number | null
    - `release_asset_url` — string | null
  - `governance` — object | object | object | null
    - `kind` — string
    - `multisig` — string
    - `vault` — string
    - `vault_index` — integer
    - `threshold` — integer
    - `time_lock_seconds` — integer
    - `transaction_index` — number
    - `members[]` — object[]
      - `key` — string
      - `can_initiate` — boolean
      - `can_vote` — boolean
      - `can_execute` — boolean
    - `matches_program_authority` — boolean
    - `misconfigured_multisig_authority` — boolean
    - `fix_hint` — string | null
    - `pending_proposal_index` — integer | null
    - `your_membership` — object | null
      - `is_member` — boolean
      - `can_initiate` — boolean
      - `can_vote` — boolean
      - `can_execute` — boolean
  - `proposals` — object | null
    - `items[]` — object[]
      - `kind` — "upgrade" | "metadata" | "idl_buffer_handoff"
      - `stage` — "this_deploy" | "follow_up"
      - `batched_into` — integer | null
      - `summary` — string
    - `proposal_count` — integer
    - `follow_up_count` — integer
  - `plan` — object
    - `elf_size` — number | null
    - `chunk_count` — number | null
    - `chunk_bytes` — number
    - `durable_chunk_count` — number | null — Chunk count in durable-nonce mode (smaller payload budget) — the client sizes its per-chunk nonce pool from this.
    - `durable_chunk_bytes` — number
    - `buffer_account_size` — number | null
    - `buffer_rent_lamports` — string | null
    - `programdata_account_size_default` — number | null
    - `programdata_rent_default` — string | null
    - `program_account_rent_lamports` — string | null
    - `current_programdata_size` — number | null
    - `current_max_data_len` — number | null
    - `needs_extend` — boolean
    - `extend_rent_lamports` — string | null
    - `network_fee` — object | null — True-total network fee (base + price×limit over every leg) with the ≤2×3 escalation ceiling as congestion max — informational, never an input.
      - `leg_count` — number
      - `cu_price_micro_lamports` — number
      - `base_fee_lamports` — string
      - `priority_fee_lamports` — string
      - `total_lamports` — string
      - `congestion_max_lamports` — string
      - `cu_price_ceiling_micro_lamports` — number
  - `paths` — object
    - `single_sig` — object
      - `applicable` — boolean
      - `upgrade_authority` — string | null
      - `notes` — string
    - `multisig` — object
      - `applicable` — boolean
      - `wallet_os_account` — string | null
      - `notes` — string
    - `observed_only` — object
      - `applicable` — boolean
      - `notes` — string
    - `immutable` — object
      - `applicable` — boolean
      - `notes` — string

### `POST /v1/program-os/programs/deploy/prepare-buffer-init` — Prepare a deploy buffer
- Action: `write` · MCP tool: `program-os.prepare_buffer_init`
- Builds the transaction that creates the buffer your program bytes get written to before deploy.
- Request body (JSON, required):
  - `cluster` — string
  - `funder` — string (required)
  - `buffer` — string (required)
  - `buffer_authority` — string (required)
  - `elf_size` — integer (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/deploy/buffers/list` — List your deploy buffers
- Action: `read` · MCP tool: `program-os.list_deploy_buffers`
- Reads at most the newest 400 recorded deploy buffers/nonces for the supplied recorded authorities, then verifies those rows live on chain. Closed or invalid rows are omitted; rows whose live authority moved are retained with `closable_by_caller:false`. There is no cursor or completeness flag, so older stranded rent can be unreachable through this operation.
- Request body (JSON, required):
  - `cluster` — string (required)
  - `authorities` — string[] (required)
- Response (JSON):
  - `buffers[]` — object[]
    - `buffer_address` — string
    - `authority` — string
    - `created_at` — number
    - `lamports` — number
    - `closable_by_caller` — boolean
    - `kind` — "buffer" | "nonce"

### `POST /v1/program-os/programs/deploy/buffers/forget` — Forget a stale buffer
- Action: `write` · MCP tool: `program-os.forget_deploy_buffer`
- Stops tracking a deploy buffer once it's confirmed gone from chain (closed or consumed).
- Request body (JSON, required):
  - `cluster` — string (required)
  - `buffer_address` — string (required)
- Response (JSON):
  - `ok` — boolean

### `POST /v1/program-os/programs/deploy/buffer-gaps` — Find the chunks still missing from a buffer
- Action: `read` · MCP tool: `program-os.deploy_buffer_gaps`
- Compares a deploy buffer's on-chain bytes against the build's, and returns the chunk ranges still missing — so an interrupted upload resumes by signing only what didn't land, never re-sending work that already did. `missing_ranges` entries are `[start_chunk, end_chunk)` pairs you can pass straight to prepare_write_chunks. A complete buffer returns `complete: true` with an empty list.
- Request body (JSON, required):
  - `candidate_id` — string (required)
  - `buffer` — string (required)
  - `chunk_bytes` — integer
- Response (JSON):
  - `cluster` — string
  - `buffer` — string
  - `candidate_id` — string
  - `elf_hash` — string
  - `elf_size` — integer
  - `chunk_bytes` — integer
  - `total_chunks` — integer
  - `complete` — boolean
  - `missing_chunks` — integer
  - `missing_bytes` — integer
  - `missing_ranges[]` — object[]
    - `start_chunk` — integer
    - `end_chunk` — integer
    - `offset` — integer
    - `length` — integer

### `POST /v1/program-os/programs/deploy/prepare-write-chunks` — Prepare the buffer writes
- Action: `write` · MCP tool: `program-os.prepare_write_chunks`
- Returns a batch of transactions that write the program bytes into the buffer, for your wallet to sign.
- Request body (JSON, required):
  - `cluster` — string
  - `candidate_id` — string (required)
  - `buffer` — string (required)
  - `buffer_authority` — string (required)
  - `fee_payer` — string
  - `start_chunk` — integer
  - `end_chunk` — integer
  - `chunk_bytes` — integer
  - `durable` — boolean
  - `nonce_accounts` — string[] — Required when durable — one nonce account PER CHUNK in the requested range (AdvanceNonce serializes per account: two chunks sharing a nonce can never both land). Aligned to [start_chunk..end_chunk).
  - `nonce_values` — string[] — Required when durable — each nonce account's CURRENT stored value, aligned with nonce_accounts (the txs' blockhashes; the box rejects a stale value).
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `transactions[]` — object[]
    - `chunk_index` — number
    - `offset` — number
    - `length` — number
    - `blockhash` — string
    - `last_valid_block_height` — number
    - `message_base64` — string
    - `account_keys[]` — string[]
    - `num_required_signatures` — number
    - `required_signers[]` — string[]
  - `total_chunks` — number
  - `next_chunk` — number | null
  - `elf_hash` — string
  - `elf_size` — number

### `POST /v1/program-os/programs/deploy/prepare-nonce-account` — Prepare a deploy nonce account
- Action: `write` · MCP tool: `program-os.prepare_nonce_account`
- Builds the create+initialize transaction for the deploy flow's durable-nonce account (authority = the deploy funder; ~0.0015 SOL rent). Sign it with the funder plus the fresh nonce keypair, land it as a normal leg, then prepare durable write chunks against its value — the whole chunk batch signs once outside any blockhash window and lands when the armed intents are fired.
- Request body (JSON, required):
  - `cluster` — string
  - `funder` — string (required) — Pays the rent-exempt minimum (~0.0015 SOL × count); also the tx fee payer.
  - `nonce_accounts` — string[] (required) — The fresh keypairs' addresses — they co-sign CreateAccount (extraSigners in the sign card). Up to 8 per tx keeps the wire under the 1232-byte MTU; a deploy flow's chunk pool is created with a few of these.
  - `authority` — string (required) — The nonce authority for ALL of them — signs every AdvanceNonce (normally the deploy funder/operator).
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/deploy/prepare-nonce-close` — Prepare to close a deploy nonce
- Action: `write` · MCP tool: `program-os.prepare_nonce_close`
- Builds the withdraw transaction that drains the deploy nonce account (rent reclaim at flow end). The authority must match the account's live nonce authority (verified against chain).
- Request body (JSON, required):
  - `cluster` — string
  - `nonce_accounts` — string[] (required) — Drained in one tx; every account's authority must match (verified against chain).
  - `recipient` — string (required) — Where the drained lamports go (normally the funder).
  - `authority` — string (required) — Must match every account's live nonce authority (verified against chain).
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/deploy/prepare-set-buffer-authority` — Prepare a buffer authority transfer
- Action: `write` · MCP tool: `program-os.prepare_set_buffer_authority`
- Builds the transaction that transfers a buffer's authority — e.g. to a Wallet OS account before a multisig upgrade.
- Request body (JSON, required):
  - `cluster` — string
  - `buffer` — string (required)
  - `current_authority` — string (required)
  - `new_authority` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/deploy/prepare-upgrade` — Prepare a program upgrade
- Action: `write` · MCP tool: `program-os.prepare_upgrade`
- Builds the transaction that upgrades the program to a filled buffer, for the current upgrade authority to sign.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `upgrade_authority` — string (required)
  - `buffer` — string (required)
  - `spill` — string
  - `fee_payer` — string
  - `elf_size_hint` — integer
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `extend` — object | null
    - `action_id` — string
    - `cluster` — string
    - `program_id` — string
    - `derived` — object
    - `required_signers[]` — string[]
    - `blockhash` — string
    - `last_valid_block_height` — number
    - `message_base64` — string
    - `account_keys[]` — string[]
    - `num_required_signatures` — number
    - `num_readonly_signed` — number
    - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/deploy/prepare-deploy` — Prepare a first deploy
- Action: `write` · MCP tool: `program-os.prepare_deploy`
- Builds the transaction that deploys a program for the first time from a filled buffer, for your wallet to sign.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `payer` — string (required)
  - `program_id` — string (required)
  - `buffer` — string (required)
  - `upgrade_authority` — string (required)
  - `max_data_len` — integer
  - `elf_size_hint` — integer
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `program_keypair_sealed` — boolean

### `POST /v1/program-os/programs/{id}/deploy/cosign` — Co-sign the deploy
- Action: `write` · MCP tool: `program-os.cosign_deploy`
- Adds the stored program keypair's signature to your first-time deploy, so you don't upload a keypair file. You still sign with your own wallet.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `message_base64` — string (required)
- Response (JSON):
  - `signatures[]` — object[]
    - `pubkey` — string
    - `signature_base64` — string

### `POST /v1/program-os/programs/deploy/prepare-close-buffer` — Prepare a buffer close
- Action: `write` · MCP tool: `program-os.prepare_close_buffer`
- Builds the transaction that closes an abandoned buffer and refunds its rent.
- Request body (JSON, required):
  - `cluster` — string
  - `buffer` — string (required)
  - `buffer_authority` — string (required)
  - `recipient` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/deploy/prepare-set-upgrade-authority` — Prepare an upgrade-authority change
- Action: `write` · MCP tool: `program-os.prepare_set_upgrade_authority`
- Builds the transaction that transfers — or permanently freezes — the program's upgrade authority.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `current_authority` — string (required)
  - `new_authority` — string
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-publish` — Prepare an on-chain metadata publish
- Action: `write` · MCP tool: `program-os.prepare_publish_metadata`
- Builds the transaction that publishes (or updates) the program's canonical metadata PDA at the Program Metadata Program (ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S) — a security.txt-style JSON document (name, logo, website, github, contacts, policy) stored under the "security" seed. The signer must be the program's live upgrade authority; the gateway verifies that against ProgramData before building the message.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `security` — object (required)
  - `payer` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-publish-buffered` — Prepare a buffered metadata publish
- Action: `write` · MCP tool: `program-os.prepare_publish_metadata_buffered`
- Builds the leg set that publishes a LARGE metadata document (a full IDL — 10-100KB+) to the program's canonical Program Metadata Program PDA: allocate a buffer, N write-chunk legs, then initialize-from-buffer (create) or extend + setData + close (update). Each leg is a prepared transaction to sign + land in order through the deploy-legs lane (leg_kind publish_metadata). The signer must be the program's live upgrade authority.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `idl_hash` — string
  - `inline_json` — string
  - `payer` — string (required)
  - `fee_payer` — string
  - `staged` — boolean
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `mode` — "create" | "update" | "stage"
  - `seed` — string
  - `metadata_pda` — string
  - `buffer_pda` — string
  - `data_bytes` — number
  - `total_chunks` — number
  - `legs[]` — object[]
    - `leg_index` — number
    - `kind` — "allocate" | "write" | "finalize"
    - `chunk_index` — number
    - `offset` — number
    - `length` — number
    - `required_signers[]` — string[]
    - `blockhash` — string
    - `last_valid_block_height` — number
    - `message_base64` — string
    - `account_keys[]` — string[]
    - `num_required_signatures` — number
    - `num_readonly_signed` — number
    - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-governed-publish` — Prepare a Squads-governed metadata publish
- Action: `write` · MCP tool: `program-os.prepare_governed_publish_metadata`
- For a Squads-governed program: compiles the PMP initialize/setData into a Squads vault transaction (the vault is the upgrade authority) and builds ONE legacy message the proposing member signs — it creates the vault transaction, the proposal, and the first approval. Peers approve on Squads; execution happens there.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `security` — object (required)
  - `member` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-governed-publish-buffered` — Prepare a governed buffered metadata publish
- Action: `write` · MCP tool: `program-os.prepare_governed_publish_metadata_buffered`
- For a Squads-governed program with a member-STAGED document buffer (prepare-publish-buffered with staged: true): verifies the staged buffer byte-for-byte against the requested document, then builds ONE member-signed message — it hands the staged buffer to the vault and creates the vault transaction + proposal + first approval. The vault message copies the staged bytes into the canonical metadata PDA (create) or setDatas from them (update) and refunds the member's staging rent. Peers approve on Squads; execution happens there.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `staged_buffer` — string (required)
  - `idl_hash` — string
  - `inline_json` — string
  - `member` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-attest` — Prepare a third-party metadata attestation
- Action: `write` · MCP tool: `program-os.prepare_attest_metadata`
- Builds the transaction that publishes (or updates) a NON-canonical metadata PDA ([program, attester, seed]) at the Program Metadata Program — a third-party attestation about the program (e.g. build-verified with the ELF hash as evidence). The attester signs and is the account's sole authority; NO upgrade-authority relationship is required (the attester must NOT be the live upgrade authority — that path is the canonical publish). The (seed, attester) pair is recorded so get_program can surface the attestation through a bounded derivation set.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `attester` — string (required)
  - `content` — object (required)
  - `payer` — string
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-immutable` — Prepare making metadata immutable
- Action: `write` · MCP tool: `program-os.prepare_metadata_immutable`
- Builds the transaction that makes the program's canonical metadata PDA immutable — FOREVER. An immutable metadata account can never be updated, trimmed, or closed; even its rent stays locked. Requires confirm: true and the live upgrade authority as signer.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `payer` — string (required)
  - `fee_payer` — string
  - `confirm` — boolean (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-trim` — Prepare a metadata trim
- Action: `write` · MCP tool: `program-os.prepare_metadata_trim`
- Builds the transaction that trims a canonical metadata PDA to exactly its content size and withdraws the excess rent to the authority (e.g. after an update shrank the document). The signer must be the live upgrade authority.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `payer` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/metadata/prepare-close` — Prepare a metadata close
- Action: `write` · MCP tool: `program-os.prepare_metadata_close`
- Builds the transaction that closes a metadata PDA and returns its rent — canonical accounts (payer = live upgrade authority) or third-party attestation accounts (attester = stored authority). Irreversible: the content is gone; a later publish recreates the account fresh.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `seed` — string
  - `payer` — string
  - `attester` — string
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/squads/detect` — Detect Squads governance
- Action: `write` · MCP tool: `program-os.detect_squads_authority`
- Classifies the program's live upgrade authority against a Squads v4 multisig (address or app.squads.so URL): verifies the authority is one of the multisig's vault PDAs, reads the multisig's threshold and members, and flags the misconfigured case where the authority is the multisig account itself. On a verified match the link is persisted and deploy-preview renders the governance card.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `multisig` — string (required)
- Response (JSON):
  - `multisig` — string
  - `vault_index` — integer | null
  - `vault` — string | null
  - `threshold` — integer
  - `time_lock_seconds` — integer
  - `members[]` — object[]
    - `key` — string
    - `can_initiate` — boolean
    - `can_vote` — boolean
    - `can_execute` — boolean
  - `transaction_index` — number
  - `matches_program_authority` — boolean
  - `misconfigured_multisig_authority` — boolean
  - `fix_hint` — string | null

### `POST /v1/program-os/programs/{id}/deploy/prepare-governed-upgrade` — Prepare a Squads upgrade proposal
- Action: `write` · MCP tool: `program-os.prepare_governed_upgrade`
- For a Squads-governed program: builds ONE legacy message the proposing member signs — it extends ProgramData when needed, hands the filled buffer to the multisig vault, and creates + approves the Squads upgrade proposal. The other members then approve on Squads; the upgrade executes there after threshold (and any time-lock). This creates exactly one proposal; the response echoes the same `proposals` enumeration deploy-preview shows before commit, including any follow-up authority-write (publishing the build's IDL) that will create its own proposal when you run it.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `candidate_id` — string (required)
  - `buffer` — string (required)
  - `member` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `proposal` — object
    - `multisig` — string
    - `vault` — string
    - `transaction_index` — integer
    - `transaction_pda` — string
    - `proposal_pda` — string
    - `memo` — string
    - `deep_link` — string
    - `elf_hash` — string | null
    - `release_tag` — string | null
  - `proposals` — object
    - `items[]` — object[]
      - `kind` — "upgrade" | "metadata" | "idl_buffer_handoff"
      - `stage` — "this_deploy" | "follow_up"
      - `batched_into` — integer | null
      - `summary` — string
    - `proposal_count` — integer
    - `follow_up_count` — integer

### `POST /v1/program-os/programs/{id}/deploy/prepare-governed-close-buffer` — Reclaim a multisig-held deploy buffer
- Action: `write` · MCP tool: `program-os.prepare_governed_close_buffer`
- For a buffer stranded by an interrupted governed deploy: builds ONE member-signed message that creates + approves a Squads proposal to close it and refund its rent. An interrupted governed deploy leaves the buffer owned by the vault, so no single key can close it — this is the governed equivalent of prepare_close_buffer. Answers a named 400 if the buffer isn't held by this program's vault.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `buffer` — string (required)
  - `member` — string (required)
  - `fee_payer` — string
  - `recipient` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `proposal` — object
    - `multisig` — string
    - `vault` — string
    - `transaction_index` — integer
    - `transaction_pda` — string
    - `proposal_pda` — string
    - `memo` — string
    - `deep_link` — string
  - `buffer` — string
  - `recipient` — string
  - `reclaimed_lamports` — number

### `GET /v1/program-os/programs/{id}/squads/proposal-status` — Track a Squads proposal
- Action: `read` · MCP tool: `program-os.get_governed_proposal_status`
- Live status of a governed-upgrade proposal: phase, approvals vs threshold, the time-lock gate, and — once executed — byte-verification of the live program bytes against the candidate hash.
- Path param `id` (string, required)
- Query param `multisig` (string, required)
- Query param `transaction_index` (integer, required)
- Response (JSON):
  - `phase` — "draft" | "voting" | "approved" | "time_locked" | "executing" | "executed" | "verified" | "rejected" | "cancelled"
  - `approvals` — object
    - `approved[]` — string[]
    - `threshold` — integer
    - `rejected[]` — string[]
  - `executable_after` — number | null
  - `verification` — object | null
    - `elf_hash` — string | null
    - `onchain_hash` — string
    - `slot` — number
    - `matches` — boolean
  - `deep_link` — string

### `POST /v1/program-os/programs/{id}/squads/execute` — Execute an approved proposal
- Action: `write` · MCP tool: `program-os.execute_governed_proposal`
- Builds the member-signed vaultTransactionExecute message so an approved Squads upgrade proposal can be executed in-app — no Squads-app visit. Guards: the member needs the Execute permission, the proposal must be Approved, and the multisig time-lock must have passed.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `multisig` — string (required)
  - `transaction_index` — integer (required)
  - `member` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `proposal` — object
    - `multisig` — string
    - `transaction_index` — integer
    - `transaction_pda` — string
    - `proposal_pda` — string
    - `deep_link` — string

### `POST /v1/program-os/programs/{id}/kernel/detect` — Detect kernel governance
- Action: `write` · MCP tool: `program-os.detect_kernel_authority`
- Classifies the program's live upgrade authority against a k256-kernel multisig: scans the multisig's vault PDAs for the one that IS the authority, and persists the link only on a live match. Flags the brick case where the authority is the configuration account itself, which no proposal can ever sign for.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `multisig` — string (required)
- Response (JSON):
  - `multisig` — string
  - `vault_index` — integer | null
  - `vault` — string | null
  - `threshold` — integer
  - `time_lock_seconds` — integer
  - `members[]` — object[]
    - `key` — string
    - `can_initiate` — boolean
    - `can_vote` — boolean
    - `can_execute` — boolean
  - `transaction_index` — number
  - `matches_program_authority` — boolean
  - `misconfigured_multisig_authority` — boolean
  - `fix_hint` — string | null

### `POST /v1/program-os/programs/{id}/deploy/prepare-kernel-upgrade` — Prepare a kernel upgrade proposal
- Action: `write` · MCP tool: `program-os.prepare_kernel_upgrade`
- For a kernel-governed program: builds ONE legacy message the proposing member signs — it extends ProgramData when needed, hands the filled buffer to the vault, and creates + approves the upgrade proposal. Members then approve and execute on the wallet-os surface.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `candidate_id` — string (required)
  - `buffer` — string (required)
  - `member` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `proposal` — object
    - `multisig` — string
    - `vault` — string
    - `transaction_index` — integer
    - `transaction_pda` — string
    - `proposal_pda` — string
    - `deep_link` — string
    - `elf_hash` — string | null
    - `release_tag` — string | null
  - `proposals` — object
    - `items[]` — object[]
      - `kind` — "upgrade" | "metadata" | "idl_buffer_handoff"
      - `stage` — "this_deploy" | "follow_up"
      - `batched_into` — integer | null
      - `summary` — string
    - `proposal_count` — integer
    - `follow_up_count` — integer

### `POST /v1/program-os/programs/{id}/deploy/prepare-kernel-close-buffer` — Reclaim a kernel-held deploy buffer
- Action: `write` · MCP tool: `program-os.prepare_kernel_close_buffer`
- For a buffer stranded by an interrupted kernel-governed deploy: builds ONE member-signed message that creates + approves a kernel proposal to close it and refund its rent. The vault is a PDA, so no single key can close it — this is the kernel equivalent of prepare_close_buffer. Answers a named 400 if the buffer isn't held by this program's kernel vault.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `buffer` — string (required)
  - `member` — string (required)
  - `fee_payer` — string
  - `recipient` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `proposal` — object
    - `multisig` — string
    - `vault` — string
    - `transaction_index` — integer
    - `transaction_pda` — string
    - `proposal_pda` — string
    - `deep_link` — string
  - `buffer` — string
  - `recipient` — string
  - `reclaimed_lamports` — number

### `GET /v1/program-os/programs/{id}/kernel/proposal-status` — Track a kernel proposal
- Action: `read` · MCP tool: `program-os.get_kernel_proposal_status`
- Live status of a kernel governed-upgrade proposal: phase, the time-lock gate, and — once executed — byte-verification of the live program bytes against the candidate hash.
- Path param `id` (string, required)
- Query param `multisig` (string, required)
- Query param `transaction_index` (integer, required)
- Response (JSON):
  - `phase` — "draft" | "voting" | "approved" | "time_locked" | "executed" | "verified" | "rejected" | "cancelled"
  - `approvals` — object
    - `approved[]` — string[]
    - `threshold` — integer
    - `rejected[]` — string[]
  - `executable_after` — number | null
  - `verification` — object | null
    - `elf_hash` — string | null
    - `onchain_hash` — string
    - `slot` — number
    - `matches` — boolean
  - `deep_link` — string

### `POST /v1/program-os/programs/{id}/deploy/prepare-close-program` — Prepare to close a program
- Action: `write` · MCP tool: `program-os.prepare_close_program`
- Builds the transaction that closes a program, reclaims its rent, and permanently retires the program id. Upgradeable programs only — a frozen program has no authority to sign.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `current_authority` — string (required)
  - `recipient` — string (required)
  - `fee_payer` — string
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/accounts/prepare-create-ata` — Prepare a token-account create
- Action: `write` · MCP tool: `program-os.prepare_create_ata`
- Builds the transaction that creates the associated token account for an owner and mint (idempotent). Returns an unsigned transaction — you sign and submit with your own wallet, which funds the rent.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `cluster` — string
  - `owner` — string (required)
  - `mint` — string (required)
  - `fee_payer` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number

### `POST /v1/program-os/programs/{id}/deploy/prepare-attest` — Prepare a deployment attestation
- Action: `write` · MCP tool: `program-os.prepare_attest_deployment`
- Builds an experimental unsigned direct-authority transaction that commits the candidate's ELF hash and any available manifest, IDL, and source hashes. Missing optional hashes use zero sentinels. The ledger proves which live upgrade authority committed which bytes; it does not prove build provenance.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `candidate_id` — string (required)
  - `authority` — string (required)
- Response (JSON):
  - `action_id` — string
  - `cluster` — string
  - `program_id` — string
  - `derived` — object
  - `required_signers[]` — string[]
  - `blockhash` — string
  - `last_valid_block_height` — number
  - `message_base64` — string
  - `account_keys[]` — string[]
  - `num_required_signatures` — number
  - `num_readonly_signed` — number
  - `num_readonly_unsigned` — number
  - `deployment` — string

### `POST /v1/program-os/programs/{id}/deploy/reconcile-candidate` — Reconcile a deployment
- Action: `write` · MCP tool: `program-os.reconcile_candidate`
- Confirms a deployment's bytes match what's on chain and, on a match, marks it the program's current deployment.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `candidate_id` — string (required)
- Response (JSON):
  - `matches` — boolean
  - `promoted` — boolean
  - `superseded_candidate_id` — string | null
  - `chain` — object
    - `program_id` — string
    - `programdata_address` — string
    - `slot` — number
    - `upgrade_authority` — string | null
    - `elf_hash` — string
    - `elf_size` — number
  - `candidate` — object
    - `id` — string
    - `environment_id` — string | null
    - `recorded_elf_hash` — string | null
    - `status_after` — string

### `POST /v1/program-os/deploy-legs/submit` — Submit a signed deploy leg
- Action: `write` · MCP tool: `program-os.submit_deploy_leg`
- Submits one fully signed deployment leg for durable landing and returns its receipt. The leg's structural identity plus optional promotion scope is its idempotency key; signed bytes are not part of the key. A retry under that key adopts the existing intent, including after different bytes conflict. After needs_resign, pass the stored intent_id with the fresh signature. The requested Program OS effect is applied once after the leg reaches its requested commitment.
- Request body (JSON, required):
  - `signed_tx_base64` — string (required) — The fully-signed transaction (v0 or legacy), base64 wire bytes.
  - `leg` — object (required)
  - `commitment_target` — "confirmed" | "finalized" (required) — The leg's landing gate — the status that fires its landed effect.
  - `cluster` — "mainnet" | "devnet" | "testnet" — Target cluster for landing. Optional for program-scoped legs because it resolves from the target row; required for alt:/org-scoped legs.
  - `last_valid_block_height` — integer (required) — Block height the signed bytes remain valid through (from the re-stamped blockhash).
  - `intent_id` — string — RE-SIGN only: after needs_resign, attach the freshly re-signed bytes of the SAME leg to its existing intent.
  - `kind` — string — Optional landing kind.
  - `trigger_spec` — any — ARMED shape: any present value (convention: {}) makes the intent triggered — it MUST be signed against a durable nonce (the box rejects otherwise), holds as `armed` outside any blockhash window, and lands on POST /deploy-legs/:intent_id/fire.
  - `arm_expires_at` — integer — Optional arm deadline (unix seconds); also selects the armed shape.
- Response (JSON):
  - `intent_id` — string
  - `signature` — string | null
  - `status` — string

### `GET /v1/program-os/deploy-legs/{intent_id}` — Get a deploy leg's landing status
- Action: `read` · MCP tool: `program-os.get_deploy_leg_status`
- Returns the current landing state for one of your signed legs. With ?wait=true&cursor=<version>&timeout=<25s>, the request waits until the version advances or the timeout expires. needs_resign is recoverable: re-stamp the blockhash, re-sign, and resubmit with intent_id; invalid/failed/canceled are terminal.
- Path param `intent_id` (string, required)
- Query param `cluster` ("mainnet" | "devnet" | "testnet")
- Query param `wait` (string) — "true" long-polls until version > cursor (clamped to 25s).
- Query param `cursor` (integer)
- Query param `timeout` (integer)
- Response (JSON):
  - `intent_id` — string
  - `version` — number — Monotonic revision, bumped on every state change. Pass it as `?cursor=<version>` to GET …/intents/:id?wait=true to long-poll for changes AFTER the version you already hold.
  - `status` — string — The lifecycle state — the field to poll. Held before broadcast: building (momentary, at create) · collecting (multisig — awaiting the remaining signatures) · scheduled (waiting on a future not_before / window) · armed (waiting on a trigger — POST /intents/:id/fire, or arm_expires_at). Live once broadcast, in order: submitted → processed → confirmed → finalized. Your effect applies (and the landed callback fires) when it reaches your commitment_target; Clear then keeps watching through finalized. Terminal: finalized (landed), invalid (rejected before broadcast — see `error`), failed (landed but rejected on chain — see `error`), or canceled (you stopped it — see the final event's reason). needs_resign is a RECOVERABLE stop, not terminal: the signed bytes expired and the intent waits — `resign_hint` carries the exact re-sign call.
  - `consumer` — string — The app/label you set on create — the console feed groups by it.
  - `kind` — string | null — Your optional free-text action label from create (e.g. "swap", "withdraw"); null when unset.
  - `cluster` — string — The Solana cluster this intent lives on — mainnet | devnet | testnet.
  - `commitment_target` — string — The landing gate you chose — confirmed | finalized. Clear applies your effect and fires the landed callback when the transaction reaches it, then always keeps watching through finalized.
  - `client_reference_id` — string | null — Your own id from create — searchable in the feed (?search=) and echoed in callbacks; null when you sent none.
  - `idempotency_key` — string — The dedup key for this intent (your idempotency_key, else the tx signature) — a resend under it collapses to this one record instead of double-sending.
  - `bundle_id` — string | null — The bundle this intent belongs to, or null for a standalone send.
  - `shape` — "single" | "scheduled" — single = broadcast at create; scheduled = held or windowed at create (any not_before, window_until, or trigger). The exact release kind is in `release`.
  - `submitted_via` — string — How it entered Clear — api (structured /intents/send) | rpc (drop-in sendTransaction) | direct (submitted straight to Clear's send endpoint).
  - `signature` — string | null — The transaction signature from the accepted wire bytes. When the chain has observed an attempt this is that observed signature; otherwise it is the latest accepted attempt, including held, expired, invalid, or canceled-before-broadcast transactions. Null only while a multisig transaction is still collecting signatures or no attempt exists.
  - `explorer_url` — string | null — A ready-to-open block-explorer link when the signature was broadcast and may have a chain record. Null before broadcast and for transactions known not to have landed, including invalid and needs_resign intents.
  - `origin` — any — The origin object you sent on create (label, optional return_url, opaque context), echoed back verbatim; null when you sent none.
  - `origin_geo` — object | null — Where the create request came from (network + GeoIP). `ip` is visible only to the intent's owner. Null until the geo write lands.
    - `country` — string | null
    - `continent` — string | null
    - `city` — string | null
    - `region` — string | null
    - `lat` — number | null
    - `lon` — number | null
    - `timezone` — string | null
    - `asn` — number | null
    - `as_org` — string | null
    - `ip` — string | null
  - `resolution` — object | null — Where and how it landed — the slot, the commitment reached, and the validator that included it. Null until landed.
    - `landed_slot` — number | null — The slot the transaction landed in; null until landed.
    - `sent_slot` — number | null — The slot the network was in when Clear admitted the transaction — the SEND end of slot latency. Absent on rows predating this field.
    - `landed_commitment` — string | null — The deepest commitment the landing reached (processed | confirmed | finalized); null until landed. A landed-but-program-rejected transaction can report processed — the failure gate commits before deeper commitments are recorded.
    - `landed_attempt_id` — string | null — Which attempt (see `attempts[]`) actually landed; null until landed.
    - `leader` — object | null — The validator that included the transaction — its identity, client software, stake and geo (all publicly verifiable on-chain). Null before landing; geo/client/stake null when not known.
      - `identity` — string
      - `country` — string | null
      - `city` — string | null
      - `asn` — number | null
      - `as_org` — string | null
      - `lat` — number | null
      - `lon` — number | null
      - `client` — object | null — The validator's declared client software (agave | frankendancer | …); null when no version is known.
      - `stake` — number | null — Activated stake (lamports) of the validator that included the tx — null when not known.
  - `attempts[]` — object[] — Every signed version of this action Clear has driven — one per create / re-sign / bump, newest last. Each carries its own signature and blockhash so you can trace exactly which bytes were on the wire.
    - `attempt_no` — number — 1-based attempt index — bumped by each re-sign / bump.
    - `signature` — string — This attempt's transaction signature.
    - `blockhash` — string — The blockhash (or durable-nonce value) these bytes were signed against.
    - `last_valid_block_height` — number — The height past which this attempt's blockhash expires.
    - `status` — string — in_flight while the intent is live; a landed attempt carries its reached commitment (processed | confirmed | finalized). Once the intent terminalizes, an attempt that never landed tells the truth instead: superseded (a re-sign/bump replaced it, or another attempt landed) | invalid (Clear rejected the bytes before broadcast) | failed (the chain rejected this signature) | canceled (future re-broadcasts stopped) | expired (the bytes died — see resign_hint.reason).
  - `events[]` — object[] — The ordered transition timeline (oldest first) — every status change with its observation time and, where recorded, its reason. This is the audit trail behind the current `status`.
    - `at` — number — Unix-ms when Clear OBSERVED this transition — observation time, never the on-chain time (that lives in result.block_time). Same-second neighbors are real: e.g. a transaction accepted with an already-expired blockhash shows submitted and needs_resign within the same second.
    - `status` — string — The status this transition moved the intent INTO — the same vocabulary as the top-level `status`, plus result_recorded — the on-chain outcome enrichment recorded after landing.
    - `reason` — string — Why this transition happened, when recorded — needs_resign: blockhash_expired | nonce_advanced; canceled: caller_requested | arm_expired | window_closed | collecting_expired | bundle_sealed | bundle_canceled.
  - `resign_hint` — object — Present ONLY while status is needs_resign: why it didn't land + the exact re-sign call with this intent's real id. The needs_resign callback payload embeds the same object.
    - `reason` — string — Why the signed bytes died: blockhash_expired | nonce_advanced.
    - `action` — string — The one next action, in plain words.
    - `send` — object — A ready-to-send re-sign call, prefilled with this intent's real id.
      - `method` — string
      - `url` — string — The ABSOLUTE re-sign endpoint — the intent's own cluster send door, https://clear-<cluster>.k256.xyz/v1/clear/intents/send.
      - `body` — object
    - `retry_collapse` — string — How retries collapse onto this intent, in the ingress door's own vocabulary (API/direct: intent_id + the client_reference_id/idempotency_key body fields; drop-in RPC: the X-Clear-Reference-Id header).
  - `fee_posture` — object — How this transaction's priority fee sat against the live fee market when Clear accepted it — deliberately coarse. Snapshotted once at acceptance, so create and every later read return the SAME value. ABSENT when the fee wasn't classified at acceptance (and bundle members carry no posture) — never guessed.
    - `level` — "none" | "low" | "typical" | "high" — The create-time price band: none (no priority fee set) | low | typical | high, relative to observed network demand when Clear accepted the transaction.
    - `note` — string — A factual description of that create-time price band.
  - `decoded` — any | null — The decoded transaction message (fee payer, instructions, account keys, address-lookup-table resolution) — a read-only view of the bytes you signed. Null before decode.
  - `result` — any | null — The on-chain outcome once landed — slot, block_time, fee (lamports), compute units consumed, balance/token deltas, and program logs. Null until landed.
  - `error` — any — The exact failure detail when status is invalid (pre-broadcast validation) or failed (the chain's error plus a plain-words reading); null otherwise.
  - `not_before` — number | null — The earliest unix-ms this intent may broadcast (the not_before you set), or null for an immediate send.
  - `collect_expires_at` — number — When this multisig stops waiting for its remaining signatures (unix-ms) and cancels with reason collecting_expired — nothing is broadcast. Present only while a collection is open; defaults to 1 hour after create unless you set collect_expires_at.
  - `release` — object — The release axis — WHEN the bytes broadcast, orthogonal to `shape`.
    - `kind` — string — The release mechanism — immediate (broadcast at create) | scheduled (held for not_before) | window (broadcast now, keep trying until window_until) | triggered (armed; broadcasts on /fire).
    - `window_until` — number | null — Landing-window deadline (unix-ms) for a window release; null otherwise.
    - `arm_expires_at` — number | null — TTL (unix-ms) for a triggered release — cancels cleanly if never fired; null otherwise.
    - `armed` — boolean — True while a triggered intent is held awaiting /fire.
    - `trigger_spec` — any | null — Your own resolve condition, echoed verbatim from create (triggered release only) — Clear stores it opaquely and never interprets it. Null on every other release kind.
  - `created_at` — number — When Clear created this intent (unix-ms).
  - `updated_at` — number — When this intent last changed (unix-ms).
  - `terminal_at` — number | null — When the intent reached a terminal state (finalized | invalid | failed | canceled); null while still live or in needs_resign.

### `POST /v1/program-os/deploy-legs/{intent_id}/fire` — Fire an armed deploy leg
- Action: `write` · MCP tool: `program-os.fire_deploy_leg`
- Releases one armed durable-nonce deployment leg for landing, so no recent-blockhash window applies. A stale nonce returns needs_resign: re-sign against the new value and resubmit via submit_deploy_leg with intent_id.
- Path param `intent_id` (string, required)
- Request body (JSON, required):
  - `cluster` — "mainnet" | "devnet" | "testnet"
- Response (JSON):
  - `intent_id` — string
  - `version` — number — Monotonic revision, bumped on every state change. Pass it as `?cursor=<version>` to GET …/intents/:id?wait=true to long-poll for changes AFTER the version you already hold.
  - `status` — string — The lifecycle state — the field to poll. Held before broadcast: building (momentary, at create) · collecting (multisig — awaiting the remaining signatures) · scheduled (waiting on a future not_before / window) · armed (waiting on a trigger — POST /intents/:id/fire, or arm_expires_at). Live once broadcast, in order: submitted → processed → confirmed → finalized. Your effect applies (and the landed callback fires) when it reaches your commitment_target; Clear then keeps watching through finalized. Terminal: finalized (landed), invalid (rejected before broadcast — see `error`), failed (landed but rejected on chain — see `error`), or canceled (you stopped it — see the final event's reason). needs_resign is a RECOVERABLE stop, not terminal: the signed bytes expired and the intent waits — `resign_hint` carries the exact re-sign call.
  - `consumer` — string — The app/label you set on create — the console feed groups by it.
  - `kind` — string | null — Your optional free-text action label from create (e.g. "swap", "withdraw"); null when unset.
  - `cluster` — string — The Solana cluster this intent lives on — mainnet | devnet | testnet.
  - `commitment_target` — string — The landing gate you chose — confirmed | finalized. Clear applies your effect and fires the landed callback when the transaction reaches it, then always keeps watching through finalized.
  - `client_reference_id` — string | null — Your own id from create — searchable in the feed (?search=) and echoed in callbacks; null when you sent none.
  - `idempotency_key` — string — The dedup key for this intent (your idempotency_key, else the tx signature) — a resend under it collapses to this one record instead of double-sending.
  - `bundle_id` — string | null — The bundle this intent belongs to, or null for a standalone send.
  - `shape` — "single" | "scheduled" — single = broadcast at create; scheduled = held or windowed at create (any not_before, window_until, or trigger). The exact release kind is in `release`.
  - `submitted_via` — string — How it entered Clear — api (structured /intents/send) | rpc (drop-in sendTransaction) | direct (submitted straight to Clear's send endpoint).
  - `signature` — string | null — The transaction signature from the accepted wire bytes. When the chain has observed an attempt this is that observed signature; otherwise it is the latest accepted attempt, including held, expired, invalid, or canceled-before-broadcast transactions. Null only while a multisig transaction is still collecting signatures or no attempt exists.
  - `explorer_url` — string | null — A ready-to-open block-explorer link when the signature was broadcast and may have a chain record. Null before broadcast and for transactions known not to have landed, including invalid and needs_resign intents.
  - `origin` — any — The origin object you sent on create (label, optional return_url, opaque context), echoed back verbatim; null when you sent none.
  - `origin_geo` — object | null — Where the create request came from (network + GeoIP). `ip` is visible only to the intent's owner. Null until the geo write lands.
    - `country` — string | null
    - `continent` — string | null
    - `city` — string | null
    - `region` — string | null
    - `lat` — number | null
    - `lon` — number | null
    - `timezone` — string | null
    - `asn` — number | null
    - `as_org` — string | null
    - `ip` — string | null
  - `resolution` — object | null — Where and how it landed — the slot, the commitment reached, and the validator that included it. Null until landed.
    - `landed_slot` — number | null — The slot the transaction landed in; null until landed.
    - `sent_slot` — number | null — The slot the network was in when Clear admitted the transaction — the SEND end of slot latency. Absent on rows predating this field.
    - `landed_commitment` — string | null — The deepest commitment the landing reached (processed | confirmed | finalized); null until landed. A landed-but-program-rejected transaction can report processed — the failure gate commits before deeper commitments are recorded.
    - `landed_attempt_id` — string | null — Which attempt (see `attempts[]`) actually landed; null until landed.
    - `leader` — object | null — The validator that included the transaction — its identity, client software, stake and geo (all publicly verifiable on-chain). Null before landing; geo/client/stake null when not known.
      - `identity` — string
      - `country` — string | null
      - `city` — string | null
      - `asn` — number | null
      - `as_org` — string | null
      - `lat` — number | null
      - `lon` — number | null
      - `client` — object | null — The validator's declared client software (agave | frankendancer | …); null when no version is known.
      - `stake` — number | null — Activated stake (lamports) of the validator that included the tx — null when not known.
  - `attempts[]` — object[] — Every signed version of this action Clear has driven — one per create / re-sign / bump, newest last. Each carries its own signature and blockhash so you can trace exactly which bytes were on the wire.
    - `attempt_no` — number — 1-based attempt index — bumped by each re-sign / bump.
    - `signature` — string — This attempt's transaction signature.
    - `blockhash` — string — The blockhash (or durable-nonce value) these bytes were signed against.
    - `last_valid_block_height` — number — The height past which this attempt's blockhash expires.
    - `status` — string — in_flight while the intent is live; a landed attempt carries its reached commitment (processed | confirmed | finalized). Once the intent terminalizes, an attempt that never landed tells the truth instead: superseded (a re-sign/bump replaced it, or another attempt landed) | invalid (Clear rejected the bytes before broadcast) | failed (the chain rejected this signature) | canceled (future re-broadcasts stopped) | expired (the bytes died — see resign_hint.reason).
  - `events[]` — object[] — The ordered transition timeline (oldest first) — every status change with its observation time and, where recorded, its reason. This is the audit trail behind the current `status`.
    - `at` — number — Unix-ms when Clear OBSERVED this transition — observation time, never the on-chain time (that lives in result.block_time). Same-second neighbors are real: e.g. a transaction accepted with an already-expired blockhash shows submitted and needs_resign within the same second.
    - `status` — string — The status this transition moved the intent INTO — the same vocabulary as the top-level `status`, plus result_recorded — the on-chain outcome enrichment recorded after landing.
    - `reason` — string — Why this transition happened, when recorded — needs_resign: blockhash_expired | nonce_advanced; canceled: caller_requested | arm_expired | window_closed | collecting_expired | bundle_sealed | bundle_canceled.
  - `resign_hint` — object — Present ONLY while status is needs_resign: why it didn't land + the exact re-sign call with this intent's real id. The needs_resign callback payload embeds the same object.
    - `reason` — string — Why the signed bytes died: blockhash_expired | nonce_advanced.
    - `action` — string — The one next action, in plain words.
    - `send` — object — A ready-to-send re-sign call, prefilled with this intent's real id.
      - `method` — string
      - `url` — string — The ABSOLUTE re-sign endpoint — the intent's own cluster send door, https://clear-<cluster>.k256.xyz/v1/clear/intents/send.
      - `body` — object
    - `retry_collapse` — string — How retries collapse onto this intent, in the ingress door's own vocabulary (API/direct: intent_id + the client_reference_id/idempotency_key body fields; drop-in RPC: the X-Clear-Reference-Id header).
  - `fee_posture` — object — How this transaction's priority fee sat against the live fee market when Clear accepted it — deliberately coarse. Snapshotted once at acceptance, so create and every later read return the SAME value. ABSENT when the fee wasn't classified at acceptance (and bundle members carry no posture) — never guessed.
    - `level` — "none" | "low" | "typical" | "high" — The create-time price band: none (no priority fee set) | low | typical | high, relative to observed network demand when Clear accepted the transaction.
    - `note` — string — A factual description of that create-time price band.
  - `decoded` — any | null — The decoded transaction message (fee payer, instructions, account keys, address-lookup-table resolution) — a read-only view of the bytes you signed. Null before decode.
  - `result` — any | null — The on-chain outcome once landed — slot, block_time, fee (lamports), compute units consumed, balance/token deltas, and program logs. Null until landed.
  - `error` — any — The exact failure detail when status is invalid (pre-broadcast validation) or failed (the chain's error plus a plain-words reading); null otherwise.
  - `not_before` — number | null — The earliest unix-ms this intent may broadcast (the not_before you set), or null for an immediate send.
  - `collect_expires_at` — number — When this multisig stops waiting for its remaining signatures (unix-ms) and cancels with reason collecting_expired — nothing is broadcast. Present only while a collection is open; defaults to 1 hour after create unless you set collect_expires_at.
  - `release` — object — The release axis — WHEN the bytes broadcast, orthogonal to `shape`.
    - `kind` — string — The release mechanism — immediate (broadcast at create) | scheduled (held for not_before) | window (broadcast now, keep trying until window_until) | triggered (armed; broadcasts on /fire).
    - `window_until` — number | null — Landing-window deadline (unix-ms) for a window release; null otherwise.
    - `arm_expires_at` — number | null — TTL (unix-ms) for a triggered release — cancels cleanly if never fired; null otherwise.
    - `armed` — boolean — True while a triggered intent is held awaiting /fire.
    - `trigger_spec` — any | null — Your own resolve condition, echoed verbatim from create (triggered release only) — Clear stores it opaquely and never interprets it. Null on every other release kind.
  - `created_at` — number — When Clear created this intent (unix-ms).
  - `updated_at` — number — When this intent last changed (unix-ms).
  - `terminal_at` — number | null — When the intent reached a terminal state (finalized | invalid | failed | canceled); null while still live or in needs_resign.

### `POST /v1/program-os/deploy-legs/bundle` — Submit a signed deploy bundle
- Action: `write` · MCP tool: `program-os.submit_deploy_bundle`
- Submits a multi-leg deployment plan (extend → upgrade) as one ordered bundle of at most 20 children and returns the bundle plus each child's intent id. Exact retries, including concurrent calls or a lost response, adopt the same bundle; a changed request under the same identity is rejected. A child that needs_resign is recovered through submit_deploy_leg with that child's intent_id.
- Request body (JSON, required):
  - `legs` — object[] (required) — The plan's legs in landing order (sequential bundle children).
  - `commitment_target` — "confirmed" | "finalized" (required) — The bundle's landing gate — step events fire as children reach it.
  - `bundle_kind` — string (required) — What this bundle is for (e.g. "extend_upgrade") — the adopt key scope.
  - `cluster` — "mainnet" | "devnet" | "testnet" — Selects the box bundle host; resolved from the program row when omitted.
- Response (JSON):
  - `bundle_id` — string
  - `children[]` — object[]
    - `leg` — object
      - `program_meta_id` — string — Owning program meta id. ALT legs carry `alt:<cluster>:<address>` so the landed-effect writer can locate the table.
      - `deployment_index` — integer — The deployment this leg belongs to; 0 for legs that aren't a deployment.
      - `leg_kind` — string — buffer_init | write_chunk | extend | deploy | upgrade | alt_create | alt_extend | alt_close | … — selects the landed-effect writer (deploy/upgrade → reconcile-candidate promote; alt_create/alt_extend → binding upsert; alt_close → binding removed).
      - `leg_index` — integer — Structural index inside the leg kind (chunk number, 0 for singletons). Retries for this structural slot reuse it; a genuinely different leg needs a different index or scope.
      - `idempotency_scope` — string — Optional logical identity appended to the structural key (for example, a promotion id). Signed bytes do not distinguish the key; a needs_resign retry passes the stored intent_id.
    - `intent_id` — string

### `GET /v1/program-os/programs/{id}/candidates/{cid}/idl` — Get a deployment's IDL
- Action: `read` · MCP tool: `program-os.candidate_idl`
- The IDL JSON bundled with a deployment, tamper-checked.
- Path param `id` (string, required)
- Path param `cid` (string, required)
- Response (JSON):
  - `candidate_id` — string
  - `program_meta_id` — string
  - `idl_hash` — string | null
  - `idl_size` — number
  - `idl` — any

### `POST /v1/program-os/promotions` — Promote a release
- Action: `write` · MCP tool: `program-os.promote_release`
- Orchestrates a full promotion server-side UP TO the signing boundary: runs deploy-preview, computes the complete leg plan (buffer_init / N write_chunk legs / extend? / deploy-or-upgrade) with every leg's prepared transaction, and records a durable promotion row. Signing stays client-side — sign each prepared leg locally and land it through submit_deploy_leg. Idempotent on (program, candidate, funder): an in-flight promotion for the same triple is returned (reused=true), never duplicated. Pass `buffer` (the pubkey of a fresh locally-generated buffer keypair) when cutting a new promotion.
- Request body (JSON, required):
  - `program_meta_id` — string (required)
  - `candidate_id` — string (required)
  - `funder` — string (required) — Fee/rent payer pubkey — a public address, never a secret.
  - `buffer` — string — Pubkey of a FRESH client-generated buffer keypair (its secret never leaves the client). Required when cutting a new promotion; ignored when an in-flight one is adopted.
  - `buffer_authority` — string — Who may write/close the buffer; defaults to funder.
  - `upgrade_authority` — string — First deploys only: the authority the program is created with (defaults to funder). Upgrades always read the live on-chain authority.
- Response (JSON):
  - `promotion_id` — string
  - `status` — "prepared" | "awaiting_signature" | "landing" | "verifying" | "live" | "failed" | "halted"
  - `reused` — boolean — true when an in-flight promotion for the same (program, candidate, funder) was adopted instead of cut.
  - `plan` — object
    - `deployment_index` — integer
    - `first_deploy` — boolean
    - `elf_hash` — string
    - `elf_size` — integer
    - `total_chunks` — integer
    - `needs_extend` — boolean
    - `legs[]` — object[]
      - `leg_kind` — "buffer_init" | "write_chunk" | "extend" | "deploy" | "upgrade" — Selects the signer set and the landed effect (deploy/upgrade → reconcile-candidate promote).
      - `leg_index` — integer — Structural index inside the leg_kind (chunk number, 0 for singletons) — part of the Clear idempotency key.
      - `prepared_tx` — object | null — The unsigned transaction envelope to sign (same shape as every prepare-* op).
    - `costs` — any | null — The deploy-preview plan economics (rents, extend cost, network fee preview).
    - `governance` — any | null — The deploy-preview Squads governance block, when linked.

### `POST /v1/program-os/promotions/rollback` — Roll back to a previous release
- Action: `write` · MCP tool: `program-os.rollback_release`
- A forward promotion of a previously-verified release: the target candidate must have been live once (verified on chain, then superseded), and its ELF is re-hashed against the anchored build hash before any leg is prepared. Same plan, signing, and idempotency contract as promote_release.
- Request body (JSON, required):
  - `program_meta_id` — string (required)
  - `target_candidate_id` — string (required)
  - `funder` — string (required)
  - `buffer` — string
  - `buffer_authority` — string
- Response (JSON):
  - `promotion_id` — string
  - `status` — "prepared" | "awaiting_signature" | "landing" | "verifying" | "live" | "failed" | "halted"
  - `reused` — boolean — true when an in-flight promotion for the same (program, candidate, funder) was adopted instead of cut.
  - `plan` — object
    - `deployment_index` — integer
    - `first_deploy` — boolean
    - `elf_hash` — string
    - `elf_size` — integer
    - `total_chunks` — integer
    - `needs_extend` — boolean
    - `legs[]` — object[]
      - `leg_kind` — "buffer_init" | "write_chunk" | "extend" | "deploy" | "upgrade" — Selects the signer set and the landed effect (deploy/upgrade → reconcile-candidate promote).
      - `leg_index` — integer — Structural index inside the leg_kind (chunk number, 0 for singletons) — part of the Clear idempotency key.
      - `prepared_tx` — object | null — The unsigned transaction envelope to sign (same shape as every prepare-* op).
    - `costs` — any | null — The deploy-preview plan economics (rents, extend cost, network fee preview).
    - `governance` — any | null — The deploy-preview Squads governance block, when linked.

### `GET /v1/program-os/promotions/{id}` — Get a promotion's status
- Action: `read` · MCP tool: `program-os.get_promotion`
- One promotion: coarse status (awaiting_signature → landing → verifying → live, or failed/halted with a terminal reason) plus each leg's current landing state. When every leg has landed, the on-chain byte check resolves live or failed.
- Path param `id` (string, required)
- Response (JSON):
  - `promotion_id` — string
  - `status` — "prepared" | "awaiting_signature" | "landing" | "verifying" | "live" | "failed" | "halted"
  - `terminal_reason` — string | null
  - `program_meta_id` — string
  - `candidate_id` — string
  - `cluster` — string
  - `funder` — string
  - `deployment_index` — integer — The deployment the legs submit against (submit_deploy_leg's leg ref).
  - `current_leg` — integer
  - `legs[]` — object[]
    - `leg_kind` — string
    - `leg_index` — integer
    - `state` — "needs_signature" | "in_flight" | "landed" | "failed" — needs_signature: awaiting a client signature (or re-sign after needs_resign); in_flight: with Clear, short of its gate; landed: at/past its commitment gate; failed: terminally rejected.
    - `intent_id` — string | null — The Clear intent once submitted; attach it on re-sign after needs_resign.
    - `signature` — string | null
    - `clear_status` — string | null — Last observed Clear lifecycle status (submitted/processed/confirmed/finalized/needs_resign/invalid/failed/canceled).
  - `created_at` — number
  - `updated_at` — number

### `POST /v1/program-os/promotions/{id}/resume` — Resume a promotion
- Action: `write` · MCP tool: `program-os.resume_promotion`
- Recomputes what is missing and returns the next legs needing a signature with fresh validity: already landed legs are never prepared again, the buffer is checked on chain, and a needs_resign retry keeps its existing intent id. When every leg has landed, byte verification resolves the promotion live or failed.
- Path param `id` (string, required)
- Response (JSON):
  - `promotion_id` — string
  - `status` — "prepared" | "awaiting_signature" | "landing" | "verifying" | "live" | "failed" | "halted"
  - `terminal_reason` — string | null
  - `program_meta_id` — string
  - `deployment_index` — integer
  - `legs_to_sign[]` — object[]
    - `leg_kind` — "buffer_init" | "write_chunk" | "extend" | "deploy" | "upgrade" — Selects the signer set and the landed effect (deploy/upgrade → reconcile-candidate promote).
    - `leg_index` — integer — Structural index inside the leg_kind (chunk number, 0 for singletons) — part of the Clear idempotency key.
    - `prepared_tx` — object | null — The unsigned transaction envelope to sign (same shape as every prepare-* op).
      - `action_id` — string
      - `cluster` — string
      - `program_id` — string
      - `derived` — object
      - `required_signers[]` — string[]
      - `blockhash` — string
      - `last_valid_block_height` — number
      - `message_base64` — string
      - `account_keys[]` — string[]
      - `num_required_signatures` — number
      - `num_readonly_signed` — number
      - `num_readonly_unsigned` — number
    - `intent_id` — string | null — Set when this is a re-sign of an existing Clear intent (needs_resign) — pass it back to submit_deploy_leg.
  - `legs[]` — object[]
    - `leg_kind` — string
    - `leg_index` — integer
    - `state` — "needs_signature" | "in_flight" | "landed" | "failed" — needs_signature: awaiting a client signature (or re-sign after needs_resign); in_flight: with Clear, short of its gate; landed: at/past its commitment gate; failed: terminally rejected.
    - `intent_id` — string | null — The Clear intent once submitted; attach it on re-sign after needs_resign.
    - `signature` — string | null
    - `clear_status` — string | null — Last observed Clear lifecycle status (submitted/processed/confirmed/finalized/needs_resign/invalid/failed/canceled).

### `POST /v1/program-os/programs/{id}/releases/verify` — Verify a release on chain
- Action: `read` · MCP tool: `program-os.verify_release`
- The on-chain byte check, standalone: sha256 of the zero-stripped live ProgramData ELF vs the candidate's anchored hash (the same check reconcile-candidate runs, without promoting). Read-only.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `candidate_id` — string (required)
- Response (JSON):
  - `matches` — boolean
  - `onchain_hash` — string
  - `candidate_hash` — string
  - `slot` — number — The ProgramData slot the live bytes were deployed at.

### `GET /v1/program-os/programs/{id}/accounts` — List program-owned accounts
- Action: `read` · MCP tool: `program-os.list_accounts`
- On-chain accounts owned by this program (paginated), with the discriminator to match each to an IDL type.
- Path param `id` (string, required)
- Query param `limit` (integer)
- Query param `data_size` (string)
- Query param `discriminator_hex` (string)
- Query param `cursor` (string)
- Response (JSON):
  - `program_id` — string
  - `cluster` — string
  - `total` — number
  - `truncated` — boolean
  - `items[]` — object[]
    - `address` — string
    - `owner` — string
    - `lamports` — number
    - `discriminator_hex` — string
  - `next_cursor` — string | null

### `GET /v1/program-os/programs/{id}/accounts/{address}` — Get a program-owned account
- Action: `read` · MCP tool: `program-os.get_account`
- Full data for one program-owned account — lamports, owner, and raw bytes.
- Path param `id` (string, required)
- Path param `address` (string, required)
- Response (JSON):
  - `address` — string
  - `program_id` — string
  - `cluster` — string
  - `owner` — string
  - `lamports` — number
  - `executable` — boolean
  - `data_base64` — string
  - `data_length` — number

### `GET /v1/program-os/programs/{id}/token-accounts` — List program token accounts
- Action: `read` · MCP tool: `program-os.list_token_accounts`
- SPL token accounts (classic and Token-2022) and SOL balances held by the program's authorities.
- Path param `id` (string, required)
- Response (JSON):
  - `program_id` — string
  - `cluster` — string
  - `owners[]` — object[]
    - `pubkey` — string
    - `role` — string
    - `sol_lamports` — number
    - `token_accounts[]` — object[]
      - `address` — string
      - `mint` — string
      - `amount` — string
      - `decimals` — number
      - `ui_amount` — number | null
      - `token_program` — string

### `GET /v1/program-os/programs/{id}/buffers` — List staged buffers
- Action: `read` · MCP tool: `program-os.list_buffers`
- Deploy buffers currently staged for this program (controlled by its authority or Wallet OS account).
- Path param `id` (string, required)
- Response (JSON):
  - `program_id` — string
  - `cluster` — string
  - `buffers[]` — object[]
    - `address` — string
    - `authority` — string
    - `lamports` — number
    - `size_bytes` — number
    - `buffer_onchain_size` — number | null
    - `is_wallet_os` — boolean
  - `truncated` — boolean
  - `refreshed_at` — number
  - `cache` — "hit" | "miss"

### `GET /v1/program-os/programs/{id}/logs` — Recent on-chain invocations
- Action: `read` · MCP tool: `program-os.list_logs`
- The most recent transactions that touched this program — signature, status, slot, and time.
- Path param `id` (string, required)
- Query param `limit` (integer)
- Query param `cursor` (string)
- Query param `before` (string)
- Response (JSON):
  - `program_id` — string
  - `cluster` — string
  - `items[]` — object[]
    - `signature` — string
    - `slot` — number
    - `err` — any | null
    - `memo` — string | null
    - `blockTime` — number | null
    - `confirmationStatus` — string | null
  - `next_cursor` — string | null

### `GET /v1/program-os/programs/{id}/analytics` — Program usage analytics
- Action: `read` · MCP tool: `program-os.get_analytics`
- Transaction volume, success/failure counts, error rate, and a per-day series over a recent window — read live from chain.
- Path param `id` (string, required)
- Query param `window` (integer)
- Response (JSON):
  - `program_id` — string
  - `cluster` — string
  - `window` — number
  - `total` — number
  - `succeeded` — number
  - `failed` — number
  - `error_rate` — number
  - `first_block_time` — number | null
  - `last_block_time` — number | null
  - `span_seconds` — number | null
  - `daily[]` — object[]
    - `date` — string
    - `count` — number
    - `failed` — number
  - `note` — string

### `GET /v1/program-os/requests` — Inspect API requests
- Action: `read` · MCP tool: `program-os.requests`
- Lists your recent keyed Program OS API calls from the capture sink: method, operation, latency, status, request and response JSON where the capture tier allows it, and caller geo. The inspector never captures itself.
- Query param `endpoint` (string) — Filter to one captured operation. Omit for every operation.
- Query param `cluster` (string) — Filter to calls on one cluster when the product records a cluster. Omit for every cluster.
- Query param `status` ("2xx" | "3xx" | "4xx" | "5xx" | "error") — Filter the feed and endpoint summary by response status class. `error` means status >= 400.
- Query param `tier` (integer) — Filter by capture tier: 0 = Full bodies, 1 = metadata only.
- Query param `cursor` (string) — Pagination cursor for the `items` feed — pass the `next_cursor` from the previous page.
- Query param `limit` (integer) — Rows per page for the `items` feed (1–200). Defaults to 50.
- Query param `range_days` (integer) — Compatibility window in days (1–90) for `by_endpoint`. Exact from/to filters both items and summary. Defaults to 14.
- Query param `from` (string) — Exact ISO-8601 boundary. Supply both from and to; the interval is [from, to).
- Query param `to` (string) — Exact ISO-8601 boundary. Supply both from and to; the interval is [from, to).
- Response (JSON):
  - `items[]` — object[] — Recent keyed customer API calls, newest first. Dashboard-session, service, and internal calls are never included.
    - `timestamp` — number — Unix-ms when the call was served.
    - `endpoint` — string — The captured customer-facing operation.
    - `method` — string — HTTP method.
    - `status` — number — HTTP status returned.
    - `latency_ms` — number — Server-side latency in milliseconds.
    - `cluster` — string | null — Cluster stamped on the call, or null when the product has no cluster dimension.
    - `request_body` — any — Captured request JSON, or null when the capture tier carried no body.
    - `response_body` — any — Captured response JSON, or null when the capture tier carried no body.
    - `request_size` — number — Captured request body size in bytes before truncation.
    - `response_size` — number — Captured response body size in bytes before truncation.
    - `user_agent` — string | null — Caller's User-Agent when captured.
    - `client_id` — string | null — Caller-supplied X-Client-ID when present.
    - `tier` — number — Capture tier: 0 = Full, 1 = MetadataOnly.
    - `country_code` — string | null — Caller's GeoIP country.
    - `city` — string | null — Caller's GeoIP city.
    - `region` — string | null — Caller's GeoIP region/subdivision.
    - `continent_code` — string | null — Caller's GeoIP continent code.
    - `asn` — number | null — Caller's network ASN.
    - `as_org` — string | null — Caller's network organization name.
  - `by_endpoint[]` — object[] — Per-endpoint performance over the selected window.
    - `endpoint` — string — The captured operation.
    - `calls` — number — Number of calls to this endpoint in the window.
    - `error_rate` — number — Percent of calls with status >= 400 (0–100).
    - `p50_latency_ms` — number — Median server-side latency in milliseconds.
    - `p90_latency_ms` — number — p90 server-side latency in milliseconds.
    - `p99_latency_ms` — number — p99 server-side latency in milliseconds.
  - `next_cursor` — string | null — Pass as `cursor` to fetch the next older page of items.
  - `degraded` — boolean — true when the inspection store is unavailable; items and by_endpoint are then empty.

### `GET /v1/program-os/env/catalog` — List env variables
- Action: `read` · MCP tool: `program-os.list_env_catalog`
- Your workspace's environment variables — each key with its per-cluster values and the programs that use it. Sensitive values are always masked, never returned.
- Response (JSON):
  - `vars[]` — object[]
    - `id` — string
    - `key` — string
    - `sensitive` — boolean
    - `values[]` — object[]
      - `cluster` — string
      - `value` — string | null
      - `masked` — boolean
      - `source` — "inline" | "external"
      - `external` — object | null
      - `version` — number
      - `updated_at` — number
      - `updated_by` — string | null
    - `consumers[]` — object[]
      - `program_meta_id` — string
      - `program_id` — string
      - `cluster` — string

### `POST /v1/program-os/env/catalog` — Add an env variable
- Action: `write` · MCP tool: `program-os.create_env_catalog_var`
- Adds an environment variable to the workspace, with a value per cluster. A sensitive value is never returned through Program OS read APIs or UI; the resolver delivers its exact plaintext to the selected secret-bearing build. Legacy manifests without an independent authorization artifact are currently accepted by the resolver.
- Request body (JSON, required):
  - `key` — string (required)
  - `sensitive` — boolean (required)
  - `values` — object[] (required)
- Response (JSON):
  - `var` — object
    - `id` — string
    - `key` — string
    - `sensitive` — boolean
    - `values[]` — object[]
      - `cluster` — string
      - `value` — string | null
      - `masked` — boolean
      - `source` — "inline" | "external"
      - `external` — object | null
      - `version` — number
      - `updated_at` — number
      - `updated_by` — string | null
    - `consumers[]` — object[]
      - `program_meta_id` — string
      - `program_id` — string
      - `cluster` — string

### `PATCH /v1/program-os/env/catalog/value` — Set an env value
- Action: `write` · MCP tool: `program-os.set_env_catalog_value`
- Sets or replaces an environment variable's value on one cluster. A sensitive value is never returned through Program OS read APIs or UI; the resolver delivers it to the selected secret-bearing build. Legacy manifests without an independent authorization artifact are currently accepted by the resolver.
- Request body (JSON, required):
  - `env_var_id` — string (required)
  - `cluster` — "mainnet" | "devnet" | "testnet" (required)
  - `value` — string
  - `external` — object
- Response (JSON):
  - `var` — object
    - `id` — string
    - `key` — string
    - `sensitive` — boolean
    - `values[]` — object[]
      - `cluster` — string
      - `value` — string | null
      - `masked` — boolean
      - `source` — "inline" | "external"
      - `external` — object | null
      - `version` — number
      - `updated_at` — number
      - `updated_by` — string | null
    - `consumers[]` — object[]
      - `program_meta_id` — string
      - `program_id` — string
      - `cluster` — string

### `DELETE /v1/program-os/env/catalog/value` — Remove a cluster value
- Action: `write` · MCP tool: `program-os.delete_env_catalog_value`
- Stops injecting an environment variable on one cluster immediately; the key stays in the catalog. The retired value row, including plaintext or ciphertext, remains stored for 7 days and is then eligible for the bounded scheduled purge, so this is not immediate erasure.
- Request body (JSON, required):
  - `env_var_id` — string (required)
  - `cluster` — "mainnet" | "devnet" | "testnet" (required)
- Response (JSON):
  - `var` — object
    - `id` — string
    - `key` — string
    - `sensitive` — boolean
    - `values[]` — object[]
      - `cluster` — string
      - `value` — string | null
      - `masked` — boolean
      - `source` — "inline" | "external"
      - `external` — object | null
      - `version` — number
      - `updated_at` — number
      - `updated_by` — string | null
    - `consumers[]` — object[]
      - `program_meta_id` — string
      - `program_id` — string
      - `cluster` — string

### `DELETE /v1/program-os/env/vars/{env_var_id}` — Delete an environment variable
- Action: `write` · MCP tool: `program-os.delete_env_var`
- Soft-deletes an environment variable and removes every program selection, so injection stops immediately. Its plaintext or ciphertext value rows remain stored for 7 days and are then eligible for the bounded scheduled purge; a purge backlog can extend retention.
- Path param `env_var_id` (string, required)
- Response (JSON):
  - `id` — string
  - `deleted` — boolean

### `GET /v1/program-os/programs/{program_meta_id}/env` — List a program's env
- Action: `read` · MCP tool: `program-os.list_program_env`
- The exact environment a program's builds inject — the catalog keys it uses (sensitive masked) plus its own local vars — and the catalog keys still available to add.
- Path param `program_meta_id` (string, required)
- Response (JSON):
  - `cluster` — string
  - `injected[]` — object[]
    - `id` — string
    - `key` — string
    - `sensitive` — boolean
    - `source` — "catalog" | "local"
    - `value` — string | null
    - `masked` — boolean
    - `value_source` — "inline" | "external"
    - `external` — object | null
      - `connection_name` — string
      - `provider_type` — string
      - `ref` — string
    - `has_value` — boolean
    - `version` — number | null
    - `updated_at` — number | null
  - `available[]` — object[]
    - `id` — string
    - `key` — string
    - `sensitive` — boolean
    - `has_value` — boolean

### `POST /v1/program-os/programs/{program_meta_id}/env/select` — Add an env to a program
- Action: `write` · MCP tool: `program-os.select_env_var`
- Adds a catalog variable to a program — its builds will inject that key's value for its cluster.
- Path param `program_meta_id` (string, required)
- Request body (JSON, required):
  - `env_var_id` — string (required)
- Response (JSON):
  - `ok` — boolean

### `POST /v1/program-os/programs/{program_meta_id}/env/unselect` — Remove an env from a program
- Action: `write` · MCP tool: `program-os.unselect_env_var`
- Stops a program from injecting a catalog variable.
- Path param `program_meta_id` (string, required)
- Request body (JSON, required):
  - `env_var_id` — string (required)
- Response (JSON):
  - `ok` — boolean

### `POST /v1/program-os/programs/{program_meta_id}/env/local` — Add a program-local variable
- Action: `write` · MCP tool: `program-os.create_program_env_var`
- Adds a variable scoped to just this program. Marking it `sensitive` makes it write-only; a local key overrides a catalog key of the same name.
- Path param `program_meta_id` (string, required)
- Request body (JSON, required):
  - `key` — string (required)
  - `sensitive` — boolean (required)
  - `value` — string
  - `external` — object
- Response (JSON):
  - `id` — string

### `PATCH /v1/program-os/env/vars/{env_var_id}/value` — Update a local env value
- Action: `write` · MCP tool: `program-os.replace_env_var_value`
- Replaces a program-local variable's value. Sensitive values are never returned through Program OS read APIs or UI; the resolver delivers them to the selected secret-bearing build. Legacy manifests without an independent authorization artifact are currently accepted by the resolver.
- Path param `env_var_id` (string, required)
- Request body (JSON, required):
  - `value` — string
  - `external` — object
- Response (JSON):
  - `ok` — boolean

### `GET /v1/program-os/secret-providers` — List secret provider connections
- Action: `read` · MCP tool: `program-os.list_secret_providers`
- Your workspace's external secret-provider connections — name, type, endpoint, and last-test status. Credentials are write-only and never returned. Only 1Password Connect has an adapter; other accepted types fail closed. Test status is informational today and does not prevent a failing connection from being selected.
- Response (JSON):
  - `connections[]` — object[]
    - `id` — string
    - `name` — string
    - `type` — "onepassword_connect" | "vault" | "openbao" | "infisical" | "aws_sm" | "gcp_sm" | "azure_kv" | "doppler" | "akeyless"
    - `endpoint` — string | null
    - `config` — object
    - `last_tested_at` — number | null
    - `last_test_ok` — boolean | null
    - `created_at` — number
    - `updated_at` — number

### `POST /v1/program-os/secret-providers` — Add a secret provider connection
- Action: `write` · MCP tool: `program-os.create_secret_provider`
- Stores an external secret-provider connection. The credential is write-only and is never returned after submission. The row persists even when the immediate test fails; that result is returned but does not gate later selection. Only 1Password Connect has an adapter; other schema-accepted types persist but test and use fail closed.
- Request body (JSON, required):
  - `name` — string (required)
  - `type` — "onepassword_connect" | "vault" | "openbao" | "infisical" | "aws_sm" | "gcp_sm" | "azure_kv" | "doppler" | "akeyless" (required)
  - `endpoint` — string | null
  - `config` — object
  - `credential` — string (required)
- Response (JSON):
  - `connection` — object
    - `id` — string
    - `name` — string
    - `type` — "onepassword_connect" | "vault" | "openbao" | "infisical" | "aws_sm" | "gcp_sm" | "azure_kv" | "doppler" | "akeyless"
    - `endpoint` — string | null
    - `config` — object
    - `last_tested_at` — number | null
    - `last_test_ok` — boolean | null
    - `created_at` — number
    - `updated_at` — number
  - `test` — object
    - `ok` — boolean
    - `error_kind` — string | null

### `PATCH /v1/program-os/secret-providers/{id}` — Update a secret provider connection
- Action: `write` · MCP tool: `program-os.update_secret_provider`
- Renames a connection or changes its endpoint/config; passing `credential` rotates it (write-only, re-sealed). The provider type is immutable.
- Path param `id` (string, required)
- Request body (JSON, required):
  - `name` — string
  - `endpoint` — string | null
  - `config` — object
  - `credential` — string
- Response (JSON):
  - `connection` — object
    - `id` — string
    - `name` — string
    - `type` — "onepassword_connect" | "vault" | "openbao" | "infisical" | "aws_sm" | "gcp_sm" | "azure_kv" | "doppler" | "akeyless"
    - `endpoint` — string | null
    - `config` — object
    - `last_tested_at` — number | null
    - `last_test_ok` — boolean | null
    - `created_at` — number
    - `updated_at` — number

### `DELETE /v1/program-os/secret-providers/{id}` — Delete a secret provider connection
- Action: `write` · MCP tool: `program-os.delete_secret_provider`
- Soft-deletes a connection. Refused while any env value still references it — the refusal names the referencing keys.
- Path param `id` (string, required)
- Response (JSON):
  - `id` — string
  - `deleted` — boolean

### `POST /v1/program-os/secret-providers/{id}/test` — Test a secret provider connection
- Action: `write` · MCP tool: `program-os.test_secret_provider`
- Tests the stored connection and returns success or a safe error kind without returning its credential.
- Path param `id` (string, required)
- Response (JSON):
  - `ok` — boolean
  - `error_kind` — string | null

### `POST /v1/program-os/events/replay` — Replay lifecycle webhook events
- Action: `write` · MCP tool: `program-os.replay_program_os_events`
- Re-delivers matching failed or already-delivered lifecycle webhook events with their original event id and payload, so subscribers can deduplicate safely. Filters: one `event` name, `since` (unix timestamp), and `limit` (default 100, max 500; oldest first). Returns how many events matched.
- Request body (JSON, required):
  - `event` — "program_os.build.queued" | "program_os.build.running" | "program_os.build.succeeded" | "program_os.build.failed" | "program_os.build.cancelled" | "program_os.deploy.submitted" | "program_os.deploy.confirmed" | "program_os.deploy.failed" | "program_os.idl.published" | "program_os.idl.updated" | "program_os.drift.detected"
  - `since` — integer
  - `limit` — integer
- Response (JSON):
  - `matched` — number
  - `requeued` — number

### `GET /v1/program-os/programs/{id}/releases` — List a program's releases
- Action: `read` · MCP tool: `program-os.list_releases`
- Releases for a program, newest first (paginated) — one item per build with its commit, ELF hash, and deployment candidate, plus a fixed devnet/testnet/mainnet target matrix. Each cell carries a coarse state (not_targeted / awaiting_signature / landing / verifying / live / failed / halted), representative promotion id, target address, and verified hash. Read-only. Current `live` is derived either from the representative promotion's stored status or, when no promotion exists, equality between the build ELF and the target's latest verified ELF hash. It is artifact equivalence rather than exact active-deployment identity: same-hash builds and a historical terminal promotion can therefore remain labeled live; use get_program for current chain truth.
- Path param `id` (string, required)
- Query param `limit` (integer)
- Query param `cursor` (string)
- Response (JSON):
  - `items[]` — object[]
    - `build_id` — string
    - `status` — string — The build's status (dispatched / pending_secret_injection / running / success / failure / cancelled). Free-form: a new backend status must never break the read.
    - `branch` — string | null
    - `commit_sha` — string — The source commit the build checked out (program_builds.customer_sha).
    - `elf_hash` — string | null — The built artifact's anchored hash; null until the build succeeds.
    - `candidate_id` — string | null — The deployment candidate this build minted; null while the build hasn't produced one.
    - `created_at` — number — Build creation time (unix seconds).
    - `targets[]` — object[]
      - `cluster` — "devnet" | "testnet" | "mainnet" — Canonical cluster id (never 'mainnet-beta'). The matrix always carries all three cells, in devnet/testnet/mainnet order.
      - `state` — "not_targeted" | "awaiting_signature" | "landing" | "verifying" | "live" | "failed" | "halted" — Coarse state of this release on the cluster: representative promotion status when one exists; otherwise live when its ELF equals the target's latest verified ELF, or not_targeted. Live is not exact candidate identity today.
      - `promotion_id` — string | null — The representative promotion for this release on this cluster (newest in-flight, else newest) — feed it to get_promotion / resume_promotion.
      - `program_id` — string | null — The program's on-chain address on this cluster (the group target's programs_meta row). Null when the program doesn't target this cluster.
      - `verified_elf_hash` — string | null — The target's latest independently verified ELF hash, returned when the current coarse state is live; that state may be hash equality rather than exact candidate identity.
  - `next_cursor` — string | null
