> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Connections and catalog

> Configure provider accounts, connected tool discovery, catalog search, and Skills.

This is the detailed reference for configuring and governing integrations. For
the short version and help choosing an execution path, start with
[Tools](/concepts/tools).

Checkfu splits external capabilities into four resources:

|                 | What it is                                        | What the agent gets                                    |
| --------------- | ------------------------------------------------- | ------------------------------------------------------ |
| **Connection**  | An authorized third-party account                 | The ability to call that account, never its credential |
| **Tool**        | A callable operation discovered from a ToolSource | A name, an input schema, and a governed call path      |
| **Custom tool** | A callable operation your own app executes        | A name and an input schema; the call comes back to you |
| **Skill**       | Instructions and files                            | Content delivered into the sandbox                     |

A Tool is something an agent *calls* and Checkfu runs. A Custom tool is called the same way but runs in **your** application. A Skill is something an agent *reads*. They are not variations on one idea.

## Connections

A **Connection** is a Workspace-scoped handle to one provider account. Runtime-backed accounts keep credentials entirely inside the selected IntegrationRuntime adapter; native accounts use Checkfu's sealed first-party custody. Neither mode exposes credential bytes through Checkfu's API, events, logs, sandbox, or harness input, and there is no read-back endpoint.

### Object model and custody

These are the nouns Connect Center and CapabilityGateway share. A vault, package, snapshot, or placement is never permission.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
%%{init: {"theme":"neutral","themeVariables":{"fontFamily":"ui-sans-serif, system-ui, sans-serif","primaryColor":"#F5F5F4","primaryBorderColor":"#A8A29E","primaryTextColor":"#282828","lineColor":"#78716C","secondaryColor":"#FAFAF9","tertiaryColor":"#FFFFFF"}}}%%
flowchart TB
  P[Principal] -->|owns| C[authorized account]
  O[Qualified offering] -->|package ceiling| C
  C -->|immutable epoch| R[immutable revision]
  R -->|pins| S[tool catalog snapshot]
  S --> T[exact tool binding]
  D[vault document] -->|compiles| PA[PermissionAssignment]
  V[vault read model] -.->|read projection| PA
  PA -->|use_connection| C
  C -.-> Native[Native: Checkfu sealed store]
  C -.-> Runtime[Runtime-backed: adapter store]
  Native --> E1[Checkfu outbound edge]
  Runtime --> E2[Adapter outbound edge]
```

|                        | Native custody                        | Runtime-backed custody                                          |
| ---------------------- | ------------------------------------- | --------------------------------------------------------------- |
| Secret store           | Checkfu sealed record                 | Selected IntegrationRuntime adapter                             |
| Public Connection data | Opaque handle and non-secret metadata | Opaque handle and non-secret metadata                           |
| Token refresh          | Checkfu refreshes inline near expiry  | Adapter refreshes internally and reports normalized state       |
| Injection              | Checkfu final outbound edge           | Adapter final outbound edge                                     |
| Readback               | None                                  | None through Checkfu or the secret-free IntegrationRuntime port |
| Revoke                 | Local deny, then custody erase        | Local deny, then adapter cleanup                                |

A live supplier offering list is candidate metadata. Checkfu qualification of an exact package and runtime adapter is what makes a row connectable.

### Connect Center

Connect Center is the setup surface for Checkfu's dashboard and embedded customer applications:

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
%%{init: {"theme":"neutral","themeVariables":{"fontFamily":"ui-sans-serif, system-ui, sans-serif","primaryColor":"#F5F5F4","primaryBorderColor":"#A8A29E","primaryTextColor":"#282828","lineColor":"#78716C","secondaryColor":"#FAFAF9","tertiaryColor":"#FFFFFF"}}}%%
sequenceDiagram
    participant P as Your product
    participant C as Checkfu
    participant U as End user
    participant A as Selected adapter
    P->>C: list qualified offerings for Principal
    C-->>P: reviewed apps + placements + existing accounts
    P->>C: begin ConnectionSession
    C-->>P: durable session + short-lived handoff
    P->>U: open Checkfu trusted handoff
    U->>A: completes OAuth or credential entry
    A->>C: exact lifecycle result
    C->>C: admit immutable Connection revision + snapshot
    P->>C: observe session, review tools, apply explicit access
```

Your backend sends only credential-free intent. The handoff posts directly to the selected adapter's trusted origin, so the embedding browser and backend never handle the provider credential.

Completion is asynchronous and **not** pushed to you: there is no connection-lifecycle webhook (webhook fan-out is Session-scoped, and a Connection is a control-plane resource with no session event). When the user finishes the provider's OAuth or hosted credential entry, Checkfu observes the short-lived ConnectionSession and adopts its immutable Connection revision only after exact runtime authority confirms completion. The browser carries no authority of its own, and a replayed handoff cannot mint a second account. Use the Principal-scoped Integration Gateway session operations—or the SDK Connect Center controller—to begin, observe, replay, reconnect, and revoke without your application handling provider secrets.

`integrationGateway.listOfferings` is Principal-scoped and returns only Checkfu-qualified packages, joined with placement availability and that Principal's existing accounts. `beginConnectionSession` accepts the offering, label, return URL, optional placement, and optional narrowing. The package remains the ceiling: a caller may narrow scopes or destinations but cannot widen them.

<Note>
  Reconnect and revoke sessions bind the exact Connection version. Access apply, Bridge revoke, native health checks, and other mutations likewise use optimistic concurrency or idempotency keys; retries never create ambient authority.
</Note>

Connection reads accept `root`, `admin`, `developer`, and `viewer`; setup,
reconnect, health checks, access changes, and revocation require `root`, `admin`, or `developer`. Runner
keys are data-plane credentials and cannot browse Connections. Connections and their owning
Principals are private to the selected Workspace: a foreign ID is indistinguishable from a missing
ID, and a denied request cannot create, rotate, health-check, or revoke anything.

### Native custody diagnostics (advanced)

`GET /v1/connection-providers` is a lower-level reflection of native custody declarations. It is not the Connect Center catalog and must not be used to build a second provider wall. Runtime-backed apps, qualification, placements, and account joins live only in `integrationGateway.listOfferings`. The native reflection carries:

* `provider`: the native custody provider name used by the lower-level Connection operations.
* `custody_kind`: the deployment-declared credential mode of the integration — `oauth` (interactive connect flow, served by the MCP specification's own discovery, registration, token, and refresh endpoints), `api_key` (write-only credential entry through a ConnectionSession), `bearer`, or `mtls` (a deployment-held Worker certificate binding activated through hosted setup without exposing certificate bytes).
* `scope_mode` and `scopes`: `fixed` means the deployment pinned an exact reviewed scope set (shown in `scopes`) and an authorize request must match it exactly; `caller_specified` means the authorize request supplies the scopes and `scopes` is empty.
* `configured`: false while custody cannot start setup. An incomplete declaration — an `oauth` integration missing the MCP `server_url` it authorizes against, or an `mtls` integration missing its exact host binding, say — fails the whole custody configuration closed rather than serving half of it, and every declared provider is then listed with `configured: false` so an embedded wall stays honest.
* `review`: the honesty label for how much human review this provider's catalog entry has had — `curated`, `structural`, or `unreviewed` (D218). See below.
* `tool_count`: how many tools the entry's own ingested source describes, or `null` where the source publishes no tool list. Every MCP row is `null` — a registry entry names no tools, so the count is only knowable from a real `tools/list` on a connected session. Render `null` as "discovered on connect", not as zero.
* `connect_defaults`: the published package's reviewed connect declaration — its own description, its reviewed minimum scopes, and its reviewed egress rules — or `null` when nothing published one for this deployment's connect flow (D222).

<Warning>
  **A ProviderPackage is a runtime ceiling, not only catalog metadata.** Before
  custody starts, Checkfu resolves the exact package for the provider and
  credential flow, verifies its custody identity, requires its minimum scopes,
  rejects scopes above its allowed ceiling, and accepts only narrower egress
  rules. The Connection freezes the package name, version, digest, and review
  tier in `provider_package`. New authority without an unambiguous package is
  refused. Rows created before package binding remain visibly unbound with a
  `null` `provider_package`; no public migration operation silently promotes them.
</Warning>

<Note>
  **Which providers can be `oauth` is narrower than it looks.** The OAuth path is the MCP specification's — discovery, dynamic client registration, token, and refresh all derive from an integration's declared `server_url` — which is why adding an MCP provider is a catalog row rather than code. A plain, non-MCP provider API has no such standard to ride, and Checkfu ships no per-provider OAuth engine: those integrations connect with a pasted credential (`api_key` or `bearer`) instead. Per-provider OAuth for non-MCP APIs is deferred until a customer names one (D215), so a catalog capture whose source publishes only OAuth2 is refused rather than half-supported.
</Note>

The native reflection is not a second authority: it reports the exact native custody mapping, and lower-level operations still enforce the ProviderPackage ceiling. The legacy interactive `authorize` operation excludes mTLS because that flow has no authorization URL. The reflection never returns custody identifiers, mTLS binding names, integration keys, connect tokens, or secret material.

Use Connect Center to render “connect your apps.” It shows every qualified offering with review, availability, setup flow, placement, destinations, and tool count while keeping supplier coordinates private. The server owns every refusal; clients present its typed unavailable state rather than reimplementing qualification or package ceilings.

#### What `review` promises, and what it does not

Breadth and review capacity do not grow at the same rate, so the catalog states which one a row has had rather than implying both (D218):

| `review`     | What it means                                                                                                                                              |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `curated`    | A human read this entry on a stated date. Its egress hosts, base URL, and scopes are reviewed declarations                                                 |
| `structural` | Exactly what the pipeline could prove from the provider's own self-describing source — its OpenAPI document or MCP registry entry — and reviewed by nobody |
| `unreviewed` | A provider the deployment configured that no published catalog entry covers at all                                                                         |

**Show the label; do not hide the rows.** A weaker label is information your user should have, not a reason to drop a provider from the wall — the same availability-honesty rule the rest of these docs follow, applied to assurance instead of capability. The label also cannot flatter itself: a curated review may only *fill in* what the structural capture left out, and one that moves a host, a base URL, a tool count, a scope, or a probe outside what the source published is refused. Promoting a provider buys review, never a different provider.

The corollary is worth stating because it is what makes the catalog trustworthy: where a provider's source publishes no auth mode, no scope vocabulary, no absolute server URL, or no parameter-free health probe, the pipeline **refuses the entry by name** instead of guessing a default. A provider missing from the wall has a published reason rather than a silence.

### Health and drift

A Connection reports three independent fields:

* `lifecycle`: `pending`, `active`, `disabled`, or `revoked`. The administrative state.
* `health`: `pending`, `healthy`, `reauthorization_required`, or `unavailable`. Whether it currently works.
* `drift`: `none`, `custody_configuration`, or `capability_refresh`. The latter means an IntegrationRuntime discovered a new immutable capability snapshot; it remains quarantined until explicitly adopted, so the admitted predecessor authority does not change.

`POST /v1/connections/{id}/health-check` refreshes these fields for native
custody-backed Connections. Integration Gateway Connections report
`capability_refresh` through their connection workflow and cannot enter that
legacy custody mutation path. In both cases, a Connection with any drift
reports `health: "unavailable"` in its control-plane projection. A
`custody_configuration` drift is broken and fails closed. During an
Integration Gateway `capability_refresh`, however, the authority retains the
admitted predecessor revision while the new snapshot is quarantined for
explicit review. Released-agent selection resolves the exact admitted immutable
ConnectionRevision through CapabilityGateway, so the already-pinned predecessor
remains callable without silently adopting the quarantined candidate.

Health is where a lapsed grant surfaces, because nothing pushes one at you. Custody refreshes an expiring token inline, at the moment a ToolInvocation needs it; a refresh the authorization server answers with `invalid_grant` marks the grant for reconnect, so that ToolInvocation fails closed immediately, every later ToolInvocation fails closed without presenting the dead token again, and the next health check reports `reauthorization_required`. Only `POST /v1/connections/{id}/reauthorize` clears it. Connect Center offers Reconnect for that grant failure and for a genuinely unavailable account; a quarantined capability refresh stays a review of the successor snapshot, not a credential reconnect. Poll the health check on the cadence your product can tolerate rather than waiting for a notification that never comes.

### Egress rules

Every Connection carries an explicit, default-deny network boundary. A rule names a host, port, path prefix, and allowed methods. A request that does not match a rule is not sent.

The scheme is not a choice: only `https` is representable. Hosts must be public DNS names. Loopback, `.internal` suffixes, and IP literals are all rejected at the schema.

<Warning>
  `allowed_headers` is itself default-deny, not an optional extra filter. Every header on a request must appear in the rule's allowlist, so **omitting `allowed_headers` means no request headers may be sent at all**. A rule that forgets to allow `Content-Type` will not match a request that sends one.
</Warning>

An explicit denylist blocks routing- and authorization-bearing headers (`Authorization`, `Host`, custom override headers, and the forwarding-proxy header prefixes), so a tool call cannot smuggle its own credential or redirect the call to a different host. The prefixes belonging to the credential aggregator D215 retired stay on that denylist: relaxing a fence is not a cleanup, and a caller can still try them against a provider that happens to honor them.

### How a harness calls one

A Connection is exposed to the harness as its own MCP server at `/v1/runtime/mcp/{connection_id}`, authenticated by the Run's short-lived admission token. It is harness-facing rather than customer-facing, so it does not appear in the generated API reference; it is published in the complete OpenAPI document at `/api/openapi.complete.json`, and you never call it yourself. The harness asks for a tool call by name; Checkfu compiles the outbound request server-side, applies governance and approvals, and returns only the response. For credentialed HTTP and OpenAPI tools, the selected custody boundary attaches the credential at its outbound edge. A public upstream MCP tool bypasses custody and carries no credential to the provider.

The endpoint is narrower than "every Connection in the Workspace". It serves a Connection only when:

* the Connection is owned by the Run's acting Principal;
* its `lifecycle` is `active` **and** its `health` is `healthy`.

`tools/list` is narrower still: it returns only the tools attached to the admitted Agent version that belong to this Connection's provider, not the Connection's full discovered surface.

This is what "credential-free" means concretely: the agent's process never holds a secret it could exfiltrate. Credentialed HTTP calls receive their secret only after the agent's involvement ends; public upstream MCP calls remain credential-free end to end. Authenticated upstream MCP is not part of the v1 public-MCP path.

### Who can reach what

Access to a Connection is a `use_connection` [PermissionAssignment](/concepts/tenancy-and-governance) and nothing else. Two read projections answer the question from each end, and both derive from the live PermissionAssignment rows on every read — so a PermissionAssignment deleted through any surface disappears from them immediately:

* `GET /v1/agents/{agent_id}/assigned-connections` — what this agent can reach, each edge joined with the Connection's provider, label, lifecycle, health, and owner Principal, plus the PermissionAssignment id, optional time window, and rationale.
* `GET /v1/connections/{connection_id}/assigned-agents` — which agents hold this Connection. Deliberately the agent direction only: principal- and group-subject PermissionAssignments on the same Connection (vault member access, below) are not listed here.

`POST /v1/agents/{agent_id}/assigned-connections` assigns one Connection — or every Connection a vault names *at this moment*, never a standing subscription — by minting ordinary PermissionAssignments. `DELETE .../assigned-connections/{connection_id}` revokes every matching PermissionAssignment and is idempotent by construction: unassigning a pair that was never assigned succeeds with an empty `revoked_permission_assignment_ids`. Revocation takes effect at the next live authorization check, **including for a Session already running**.

<Note>
  Time bounds are part of PermissionAssignment identity, which produces one surprising case worth knowing before you hit it. Assigning a window beside an existing *unbounded* PermissionAssignment adds a second row rather than narrowing the first — the unbounded one still authorizes. To time-box access, unassign the unbounded PermissionAssignment.
</Note>

### Vaults: a named grouping, not a second permission system

A **ConnectionVault** is the "these Connections, these people, these agents" surface a connections builder renders. It is a named, Workspace-scoped grouping whose member access **compiles into ordinary `use_connection` PermissionAssignments**, one per member × Connection, each compiled edge naming the exact PermissionAssignment id it produced.

The vault row itself grants nothing. No runtime check consults it, and deleting a compiled PermissionAssignment revokes access whatever the vault still lists. Read the vault to render or edit; read the PermissionAssignments for the truth about live access.

`POST /v1/connection-vaults/apply` takes the whole document inline and reconciles: it mints PermissionAssignments for new edges, re-mints any compiled PermissionAssignment deleted elsewhere, and revokes only PermissionAssignments **the vault itself compiled** for dropped edges. Up to 1024 edges compile in one apply, `dry_run=true` returns the plan without materializing, and an `Idempotency-Key` is required.

Three behaviors to design around:

* **The compile refuses to adopt.** A pre-existing equivalent PermissionAssignment minted outside the vault conflicts rather than being absorbed. Because the apply also revokes, adopting a row it did not create would let a later vault edit destroy an operator's independent authority — so it declines and you reconcile.
* **Members are PermissionAssignment subjects.** A member is a `principal` or `group` ref, and a Group member costs one row per Connection while staying live through ordinary group expansion. Membership churn is a Group PATCH, not a PermissionAssignment rewrite.
* **Archiving is not revoking.** `POST /v1/connection-vaults/{vault_id}/archive` soft-fences the row and is reversible by re-applying the same name. It never revokes the compiled PermissionAssignments — withdrawing access is a re-apply with a smaller document, so revocation stays a deliberate, auditable PermissionAssignment change.

The document carries no time window on purpose. A window belongs to one agent's access to one Connection; a vault's member access is standing membership, and one clock over the whole document would expire every member at once. Time bounds ride the assignment verb instead.

## Tool sources

A **ToolSource** is a descriptor Checkfu discovers tools from. Three kinds:

| Kind      | Configuration                             |
| --------- | ----------------------------------------- |
| `mcp`     | A remote MCP server `url`                 |
| `openapi` | A `document_url` and a `base_url`         |
| `api`     | A `base_url` plus inline tool definitions |

`POST /v1/tool-sources/{id}/sync` re-runs discovery and refreshes the Tool rows, updating `tool_count`, `last_sync_at`, and `status` (`pending`, `healthy`, or `sync_failed`).

Each discovered Tool carries an `input_schema`, a `schema_hash`, and `safety_hints`. The hints are a struct of three independent booleans, not an enum:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "read_only": true, "destructive": false, "requires_approval": false }
```

They feed the approval decision, but they do not decide it alone. Governance is consulted first and can deny or require approval regardless of the hints. For an HTTP-bound tool, a ToolInvocation skips review only when the transport is read-only, `read_only` is true, `destructive` is false, and `requires_approval` is false. MCP transport always uses POST, and MCP hints are authored by the upstream server and replaced on every source sync, so they are catalog metadata rather than authorization evidence: an MCP-bound tool never skips review on hints, and only an explicit `allow` ActionPolicy auto-allows one. A tool marked `read_only` **and** `requires_approval` still goes to a human. See [ActionApprovals](/concepts/action-approvals).

`GET /v1/tool-sources/{id}/tools` returns bounded Tool summaries with each schema hash, safety hints, transport binding, and enablement state. Retrieve one Tool with `GET /v1/tool-sources/{id}/tools/{name}` when you need its complete input and output schemas. The targeted `PATCH` returns that same full Tool shape after changing enablement.

<Note>
  `PATCH /v1/tool-sources/{id}/tools/{name}` takes `expected_version` and `enabled`: enable/disable is the only property it controls. Transport bindings are immutable on a Tool by design: the upstream URL comes from the ToolSource descriptor at discovery time, so it cannot be repointed on an individual tool. Permission is a separate concern handled by PermissionAssignments and ActionPolicies.
</Note>

### The two defaults a source carries

Enablement is not only a per-tool `PATCH`. A ToolSource row carries the postures its tools inherit, so the answer is readable per source rather than inferred from a constant (D216):

* `default_permission` is `always_ask`: a tool from a governed source needs a human on each use until an [ActionPolicy](/concepts/tenancy-and-governance) says otherwise. Governed means it crosses the CapabilityGateway, which every ToolSource kind does — MCP and HTTP alike — so the default is uniform rather than per-kind. The counterpart is `always_allow`, the posture an [AgentDefinition](/concepts/agents)'s built-in toolset resolves under. That is not a weaker choice about the same boundary: built-ins never reach the CapabilityGateway, so there is no governed decision to soften, and scoping them is the launch surface's job.
* `default_enabled` decides what a **newly discovered** tool's `enabled` starts at. It defaults to `true`, which is what every existing source keeps: the source admits what it discovers. Pass `default_enabled: false` at create for **default-off subsetting** — every tool this source discovers, now and on every later sync, arrives disabled until you enable it by name, so an upstream server that grows a new tool overnight cannot hand an agent a capability nobody reviewed. The posture is fixed at creation, like the source's name and provider.

A sync never re-decides a tool that already exists. An explicit enable or disable is a review, and re-running discovery does not overwrite one — only a tool seen for the first time takes the source's `default_enabled`.

<Note>
  An [AgentDefinition](/concepts/agents#what-a-definition-contains)'s built-in toolset (`agent_toolset`) has a field with the same name, and it is a different one. That `default_enabled` scopes the platform's built-in tools on one agent; this one decides what a ToolSource's *discovered* tools start at. They meet no boundary in common — built-ins never cross the CapabilityGateway.
</Note>

## Searching the catalog

`POST /v1/catalog/search` ranks the Workspace's discovered Tools and its Skills against a query string. It takes a `query` of 1 to 1,024 characters, an optional `kinds` filter of `tool`, `skill`, or both, and an optional `limit` between 1 and 100. Each result carries its `kind`, its identity, its `description`, and a `score`.

A tool result may also carry a `stats` object: the Workspace's own reliability history for that exact tool, projected from settled ToolInvocations. It is present only when there is history to report.

| Field                                | What it counts                                                                                                 |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `window_days`                        | The window the counts cover                                                                                    |
| `calls`                              | Settled calls in the window                                                                                    |
| `completed`                          | Calls that finished                                                                                            |
| `denied`                             | Calls governance refused. A policy verdict, not tool health, so denials count neither for nor against the tool |
| `indeterminate`                      | Calls whose outcome could not be established                                                                   |
| `duration_ms_sum`, `duration_ms_max` | Exact, mergeable timing totals. Never percentiles                                                              |
| `complete`                           | `false` when the window read was partial, so a low count is honestly a partial count rather than a quiet one   |

These are exact counts you divide yourself, and they are observability and ranking input only. Nothing on an enforcement path reads them: a tool with a terrible completion ratio is still called if a PermissionAssignment and ActionPolicy allow it.

A result may additionally carry `matched_concepts`, up to eight names, when the query reached that entry through a mapped [Concept](/concepts/concepts) rather than by matching its own text. Concepts are the Workspace's vocabulary over this catalog; they only raise scores, and with none configured this search ranks exactly as text-only search does.

## Custom tools

Not every operation belongs to a third-party account. A **Custom tool** is declared inline on the [AgentDefinition](/concepts/agents) with `kind: "custom"`, carrying its own `input_schema` rather than a discovered one, and **your application executes it**. Checkfu never does. When the agent calls one, the Run parks in `waiting` until you post the matching `user.custom_tool_result`.

That is why a Custom tool has no Connection and no ToolSource: there is no platform-held credential, no egress rule, and no outbound request at Checkfu's edge. The call comes back to your own code, on your own network path, under your own auth.

### ActionPolicy-only, and allowed by default

A Custom tool needs no PermissionAssignment. The Session was already admitted to run the definition that declares it, so calling it adds no authority the Session did not already have, and the park itself touches no credential or egress. Checkfu therefore evaluates **ActionPolicy alone**, and with no ActionPolicy in the way the call proceeds.

An active ActionPolicy on that definition's `use_tool` permission is what restricts it:

* **deny**: the required action fails closed, as a terminal Run failure.
* **require\_approval**: the call becomes a human decision before your app is allowed to act.

### The governed action flow

The escalated path is worth walking through, because the ActionApproval here authorizes *you* to act, not Checkfu to act on your behalf:

1. The harness calls the tool, and `agent.tool_use` records the exact arguments, before any decision.
2. `run.requires_action` parks the Run, naming the pending ActionApproval alongside the `tool_use_id`.
3. A reviewer approves. Checkfu appends `action_approval.resolved` and `run.action_authorized` atomically, moving that same compute-closed Run from waiting on a human to waiting on your result. No Run, dispatch, lease, or Sandbox is created.
4. Your app executes the call it read from `agent.tool_use` and posts the exact `user.custom_tool_result`. That is what creates the one ordinary continuation Run. For a child thread, post through the primary Session endpoint and echo the cross-posted `session_thread_id` alongside `tool_use_id`.

Wait for `run.action_authorized` before executing. A denial does not fail the Run: it ends the wait and continues with `policy.denied` guidance, so the agent is told the call was refused and carries on. Because approval releases no secret, a Custom-tool ActionApproval carries no proof token to redeem. `run.action_authorized` in the event log is the durable fact that the decision took effect.

For CMA-shaped clients, the reviewer may send `user.tool_confirmation` to the
ordinary Session event endpoint. A child confirmation carries the
`session_thread_id` from the primary `agent.tool_use` cross-post. `allow` moves
the same wait into its Custom-result phase; it does not authorize the platform
to execute customer code or replace the later `user.custom_tool_result`.

[ActionApprovals](/concepts/action-approvals) covers what the reviewer sees and how the decision is frozen; [Handle an approval](/guides/handle-an-approval) walks the responding code.

## Skills

A **Skill** is Workspace-owned content: instructions plus optional files, delivered into the sandbox filesystem. Skills are `instructional` or `executable`. They have no input schema, no transport binding, and no approval path, because nothing is being called.

Skills follow the Agent Skills directory shape, so they move between systems as ordinary archives:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
<name>/
  SKILL.md          # YAML frontmatter: name (must match the directory), description, kind
  ...supporting files
```

`GET /v1/skills/{id}/export` produces a deterministic tar: `SKILL.md` first, then files sorted by path. `POST /v1/skills/import` accepts one and stores the `SKILL.md` text verbatim.

Every committed edit creates an immutable version. List them with `GET /v1/skills/{id}/versions`, read one with `GET /v1/skills/{id}/versions/{version}`, or export that exact directory from `GET /v1/skills/{id}/versions/{version}/export`. Session admission pins one of these versions and its content digest, so editing the current Skill cannot change or strand a Session that has already been admitted.

Frontmatter written for another tool survives the round trip byte for byte **when its `name` matches the skill**. If it does not match, import rejects the archive; if a stored skill carries no matching frontmatter, export synthesizes a fresh block rather than emitting the original. The parser reads flat single-line `key: value` scalars only, so a `description` written as a YAML block scalar is not indexed.

A **SkillSource** binds a Git Project as the authoring home so skills can be maintained in a repository and synced. Sync takes `expected_version` and an `acted_as` Principal, which must own and hold `use_connection` on the Project's Connection. It reports a per-skill outcome of `created`, `updated`, `unchanged`, `invalid`, or `missing`. A skill that disappeared upstream is reported as `missing`, never deleted for you.

## Skills that improve themselves

An agent that solves something hard the slow way has learned a skill worth keeping. A **SkillProposal** turns that trajectory into a reusable skill: authored, verified, and staged for a human to merge. It never delivers an unreviewed skill to a Run: the merge gate is the product.

The lifecycle is a job that produces an immutable artifact and adopts it only through the existing [SkillSource](#skills) loop:

1. **Author.** `POST /v1/skill-proposals` names 1–8 source Sessions (the first fixes the task class and its Agent) and a `model_routing_profile_key`; a platform job reads the trajectories and writes one immutable single-file `instructional` skill. Its entry is capped at 2560 characters: a *verifiability* bound, not a skill-size limit (see the next step). Status moves `authoring → drafted`, or `authoring_failed`.
2. **Verify.** `POST /v1/skill-proposals/{id}/verify` takes `expected_version` plus the requesting Principal and replays the task in one isolated child Session that carries the complete proposed skill in its context, graded by an [Outcome](/concepts/outcomes) rubric derived from the original failure. The proposal reserves that exact Session before admission, so an exact retry resumes it instead of launching another. The verdict lands on the proposal; `drafted → verifying → verified` or `verification_failed`. Only a `verified` proposal may submit. (The whole skill rides the graded context rather than a staged skills mount, which is why the entry is bounded.)
3. **Submit.** `POST /v1/skill-proposals/{id}/submit` takes the reviewed `expected_version`, SkillSource, and acting Principal. It moves `verified → submitting`, prepares one exact commit, records that commit, and then publishes the bound repo's `checkfu/<id>` branch through the governed [Connection](#connections). Exact retries resume the recorded attempt; a stale or different request conflicts. The branch *is* the change proposal (no pull request in v1). From here nothing is automatic: a human merges, sync runs, and the ordinary SkillSource → Skill path versions and delivers it. The proposal records the resulting skill version, closing the chain: session → proposal → verification → merge → version → future releases.

`POST /v1/skill-proposals/{id}/discard` takes `expected_version` and closes a proposal you do not want. It is available from `drafted`, `authoring_failed`, `verifying`, `verified`, and `verification_failed`; `authoring`, `submitted`, and already-terminal `discarded` do not start a new discard. A `submitting` proposal is discardable only while its reservation has no prepared commit; once a commit is recorded, provider publication may already be in flight and the exact submit must resume. A `verification_failed` proposal can instead be sent back to `verifying` and retried. `GET /v1/skill-proposals/suggestions/{session}` is a read-only structural signal: it reports whether a Session looks skill-worthy (`many_tool_calls`, or `failure_then_success`) so a console can *offer* the "make a skill" button. It never authors anything on its own, and it reads only structural counts, so it is safe under zero-data-retention.

<Note>
  The gate is deliberate. Every shipped self-authoring system we studied stages agent-written skills behind human approval or replay verification; the ungated skill marketplace that did neither grew a measurable malicious-skill supply chain. A verified proposal is a strong suggestion; a merged skill is a decision a person made.
</Note>

## Surfaces that exist only in part

Several shapes commonly assumed to exist do not:

* A ToolSource has no item-level `GET` or `DELETE`. The per-source operations are `sync` and listing its tools; individual Tools support targeted `GET` and `PATCH`.
* Neither PermissionAssignments nor ActionPolicies have a `PATCH`. Both are replaced rather than edited.
* PermissionAssignments and ActionPolicies both have item-level `GET` and `DELETE` operations.
* A SkillSource has a `GET` but no `PATCH` or `DELETE`.

## Next steps

<CardGroup cols={2}>
  <Card title="ActionApprovals" icon="user-check" href="/concepts/action-approvals">
    How a tool call becomes a human decision, and how that decision is frozen.
  </Card>

  <Card title="Agents" icon="robot" href="/concepts/agents">
    How a definition references the tools and skills discovered here.
  </Card>
</CardGroup>
