リファレンスHTTP APIAgents

Save a new version of a profile

POST
/v1/installation-profiles

The one way to define a profile (profile.define: the workspace owner or an admin), signed in; an API key is 403 whatever its scopes. The owner sends connection_id, runtime_kind and runtime_version (400 missing_field without them) and saves an installation-wide profile op_<name>_v<n>, whose provider the server copies in from the connection. An admin saves a profile of their own workspace that narrows a version they may see: derived_from names it for a first version, a later one names previous_id and keeps its source. The server copies the runtime, connection, billing and title model from the source, so an admin sending connection_id, runtime_kind, runtime_version, title_model or project_settings is 400 source_field, and a model, tool or mode wider than the source's is 400 widens_base; a source that is unknown, another workspace's or not usable is 400 unknown_base, retired or another state. A workspace profile's name is its display name; the server keeps it under a key it makes (wl_<uuid>) and installation.name answers the display name. A new profile whose name the workspace already sees is 409 name_taken. A new version of an existing profile sends its latest id as previous_id; anything else is 409 REVISION_CONFLICT (details.reason_code: stale_previous, details.latest_id), as is the second of two saves at once. A version that runs the same as the latest (the same models, tools, mode, provider and settings, whatever the deny spelling) writes nothing and answers with that latest version and a null receipt_id (the status stays 201: tell it from a new version by receipt_id); a retired or blocked latest takes a new version instead. A first owner version may name the profile it replaces in previous_id (400 unknown_previous when there is none). 400 details.reason_code: unknown_connection, runtime_not_installed, unattended_not_allowed (bypassPermissions while PLATFORM_ALLOW_UNATTENDED_PERMISSIONS is not true), model_not_in_models or invalid_profile, each with details.field. 409 details.reason_code: id_taken when the id is already an imported profile's. A profile names no repository: the request has no repository fields, and the repositories a session reaches come from the agent's GitHub settings. Its built-in tools are named one way: tools, kept as given, or deny, the tools of the runtime version's catalog (tool_catalogs in GET /v1/installation-profiles/options) to block, the rest saved as tools; deny: [] blocks none. deny may also name mcp__<server>__<tool> tools of the MCP servers the installation lists, matched exactly; they are kept beside the catalog's, listed as mcp_deny (the response deny stays the catalog's), and a next version keeps those of the one before. A version derived from a profile that blocks such names must name every one of them in deny (400 base_mcp_deny_dropped). 400 on deny (details.field: deny): no_tool_catalog (no catalog lists runtime_version), unknown_tool, tool_pattern (Bash(rm:*): whole built-in tool names only), mcp_tool_name (not mcp__<server>__<tool>, at most 128 characters, no spaces or control characters), github_server (the built-in server's tools follow the agent's GitHub settings) or unknown_mcp_server (new name of a server the installation does not list); accept_added_tools takes catalog tools only (mcp_tool). A next version lifts a held mcp__ block only by naming it in unblock_mcp_tools (400 details.field: unblock_mcp_tools: mcp_unblock_unknown when the previous version does not hold it, mcp_unblock_conflict when deny names it too, mcp_unblock_no_previous for a first version, base_mcp_deny_dropped when the base blocks it). With previous_id, a catalog tool that version never weighed (an allow list weighed only what it allowed) runs only when named in accept_added_tools, else 400 added_tool_not_accepted. Writes a create_installation_profile receipt naming the deny and tool_catalog_id saved.

Auth: cookie session only; no scope. Idempotency-Key: ignored.

Authorization

cookieSession
cookie__Host-werft_session<token>

For the web console: the browser session cookie set by POST /v1/auth/login. It holds the scopes of the user's workspace role (owner: all; admin and member: all but sessions:recover). Requests other than GET must also send X-Requested-With: werft-web.

Header Parameters

X-Requested-With?"werft-web"

Required with a cookie session: the literal werft-web.

Value in"werft-web"

Request Body

application/json
  1. body
name*string
Match^[a-z0-9][a-z0-9-]{0,39}$
connection_id?string
Length1 <= length <= 200
runtime_kind?string
Value in"claude_agent_sdk""codex_app_server""pi_coding_agent"
runtime_version?string
Length1 <= length <= 200
model*string
Length1 <= length <= 200
models*array<>
Items1 <= items <= 32
title_model?string
Length1 <= length <= 200
tools?array<>

The built-in tools sessions run, kept as given. Exactly one of tools and deny.

Itemsitems <= 64
deny?array<>

The built-in tools of the runtime version's tool catalog to block, and mcp__<server>__<tool> names of the servers this installation lists (not the built-in github server); the server stores the rest of the catalog as tools. A derived version names every mcp__ name its base blocks; a next version keeps those of the one before. [] blocks none. Exactly one of tools and deny; 400 for a runtime version with no catalog.

Itemsitems <= 64
accept_added_tools?array<>

With deny: catalog tools the previous version never weighed that this one may run. Any such tool neither here nor in deny is 400.

Itemsitems <= 64
unblock_mcp_tools?array<>

mcp__<server>__<tool> names the previous version blocked that this next version no longer blocks: the only way to lift one, as a version otherwise keeps every mcp__ name of the one before. Each must be held by the previous version (400 mcp_unblock_unknown), not named in deny too (400 mcp_unblock_conflict) and not blocked by the base (400 base_mcp_deny_dropped); a version with no previous one cannot lift any (400 mcp_unblock_no_previous). A workspace version names its previous version in previous_id.

Itemsitems <= 64
permission_mode*string
Value in"default""acceptEdits""dontAsk""plan""bypassPermissions""auto"
project_settings?
previous_id?string
Length1 <= length <= 200
derived_from?string
Length1 <= length <= 200

Response Body

The ceiling saved, and its receipt.

application/json
  1. response
profile*
receipt_id*|null
Example Request
curl -X POST 'https://example.com/v1/installation-profiles' \  -H 'Content-Type: application/json' \  -d '{  "name": "coding",  "connection_id": "anthropic-main",  "runtime_kind": "claude_agent_sdk",  "runtime_version": "0.3.319",  "model": "claude-sonnet-5",  "models": [    "claude-sonnet-5",    "claude-haiku-5"  ],  "title_model": "claude-haiku-5",  "tools": [    "Read",    "Edit",    "Write",    "Glob",    "Grep",    "Bash"  ],  "permission_mode": "acceptEdits",  "project_settings": {    "claude_md": true  },  "previous_id": "op_coding_v1"}'
Example Response
{  "profile": {    "id": "op_coding_v2",    "source": "operator",    "model": "claude-sonnet-5",    "models": [      "claude-sonnet-5",      "claude-haiku-5"    ],    "tools": [      "Read",      "Edit",      "Write",      "Glob",      "Grep",      "Bash"    ],    "permission_mode": "acceptEdits",    "runtime_kind": "claude_agent_sdk",    "runtime_version": "0.3.319",    "billing": "metered",    "repository_mode": "required",    "repository_ids": [      "sample-app"    ],    "installation": {      "name": "coding",      "version": 2,      "origin": "console",      "state": "usable",      "connection_id": "anthropic-main",      "title_model": "claude-haiku-5",      "project_settings": {        "claude_md": true      },      "previous_id": "op_coding_v1",      "created_by": "019a0000-0000-7000-8000-0000000000b1",      "created_at": "2026-10-28T01:00:00.000Z",      "retired_by": null,      "retired_at": null,      "blocked_by": null,      "blocked_at": null    }  },  "receipt_id": "019a0000-0000-7000-8000-0000000000e3"}