> ## 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.

# Installations and surface scopes

> Place an agent on an external surface your integration bridges so Automations and agent-to-agent flows execute there under one governed identity.

Most embedded agents do not live in a chat box you built. They live on a surface your users already use: an external provider your own integration bridges, or a support queue inside your product. **Installing** an agent is how you put a published [Agent](/concepts/agents) on those surfaces so [Automations](/concepts/automations) and agent-to-agent flows can execute there.

Three resources compose the placement. They reference each other; none copies the others' data.

<CardGroup cols={3}>
  <Card title="ExternalInstallation" icon="plug">
    A verified, Workspace-owned installation of one provider, such as one Slack app installation. `exti_…`
  </Card>

  <Card title="SurfaceScope" icon="sitemap">
    A governed place inside that installation: the provider root, a team, a channel, or a DM. `surf_…`
  </Card>

  <Card title="AgentInstallation" icon="robot">
    The binding that places one Agent on specific surface scopes under a declared identity posture. `aini_…`
  </Card>
</CardGroup>

## The provider installation

An **ExternalInstallation** is an authenticated link to one provider tenant your integration operates. A given `(provider, external tenant)` has at most one active Checkfu Workspace owner.

Creating or reactivating one requires a **signed pairing proof** bound to the exact Workspace, provider tenant, operation, and installation, and valid for at most five minutes. Your provider adapter produces that proof; Checkfu verifies it.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "provider": "slack",
  "external_tenant_id": "T0123456789",
  "display_name": "Acme Slack",
  "pairing_proof": { "issued_at": "…", "expires_at": "…", "nonce": "…", "signature": "…" }
}
```

The response reports `state` (`active`, `disabled`, `disconnected`, or `uninstalled`). Provider credential material never appears on the resource, in a Session, or in a harness.

## The surface hierarchy

A **SurfaceScope** is a governed place in an installation, and it is the provider-neutral handle everything else uses. PermissionAssignments, ActionPolicies, Memory, Budgets, Automations, audit, and retention all address a SurfaceScope. Provider-specific channel or DM identifiers never enter Session state.

Each scope has a `kind` and, unless it is the `root`, a `parent_id`:

| `kind`     | Parent           |
| ---------- | ---------------- |
| `root`     | none             |
| `team`     | `root`           |
| `channel`  | `root` or `team` |
| `personal` | `root` or `team` |

A scope also declares `visibility` (`public`, `private`, `personal`), `membership` (`public`, `provider_members`, `personal`), and `inheritance` (`inherit` or `isolate`). The tree must be acyclic, `(installation, external_id)` is unique, and there is no `DELETE`. A scope's lifecycle ends by patching its `state` to `archived`.

<Note>
  A **thread** inside a channel is not another SurfaceScope. A thread maps to a Session through a [conversation binding](/concepts/conversation-bindings); the SurfaceScope is the channel it lives in.
</Note>

## Placing the agent

An **AgentInstallation** is the binding that makes an Agent answerable on surfaces. It coordinates references: it never copies the definition, and it never introduces a second access-control list.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST https://api.checkfu.com/v1/agent-installations \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Idempotency-Key: install-$(uuidgen)" \
    --header "Content-Type: application/json" \
    --data '{
      "external_installation_id": "exti_0123456789abcdef0123456789abcdef",
      "agent_deployment_id": "adep_0123456789abcdef0123456789abcdef",
      "agent_deployment_revision_selection": { "type": "current" },
      "acted_as": "prin_0123456789abcdef0123456789abcdef",
      "surface_scope_ids": ["surf_0123456789abcdef0123456789abcdef"],
      "harness": "claude-code",
      "model_routing_profile_key": "primary",
      "sandbox_profile_key": "standard"
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const installation = await checkfu.collaboration.agentInstallations.create(
    {
      external_installation_id: "exti_0123456789abcdef0123456789abcdef",
      agent_deployment_id: "adep_0123456789abcdef0123456789abcdef",
      agent_deployment_revision_selection: { type: "current" },
      acted_as: "prin_0123456789abcdef0123456789abcdef",
      surface_scope_ids: ["surf_0123456789abcdef0123456789abcdef"],
      harness: "claude-code",
      model_routing_profile_key: "primary",
      sandbox_profile_key: "standard",
    },
    { idempotencyKey: `install-${crypto.randomUUID()}` },
  )
  ```
</CodeGroup>

What the placement binds:

| Field                                                         | Binds                                                                                                                                                            |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_deployment_id` + `agent_deployment_revision_selection` | Which [AgentDeployment](/reference/agent-deployments) its Sessions admit against, and whether each Session takes the current revision or pins one exact revision |
| `external_installation_id`                                    | The provider installation it answers in                                                                                                                          |
| `surface_scope_ids`                                           | 1–1,000 active same-installation scopes it is placed on                                                                                                          |
| `identity_posture`                                            | `autonomous` (the default when omitted) or `delegated`                                                                                                           |
| `acted_as`                                                    | The Principal it executes as — its own **service** Principal when autonomous, a resolved **person** when delegated                                               |
| `harness`, `model_routing_profile_key`, `environment`         | Logical names, which must equal what the resolved release selects                                                                                                |

There are no harness or model "override" objects here. The logical names name the same contract the Agent's published release already resolves to; the installation coordinates, it does not reconfigure.

The **identity posture** declares whose authority the placement runs under, and admission rejects a mismatch rather than guessing:

* `autonomous` (the default when `identity_posture` is omitted): the placement acts as its own credential-bearing `service` Principal, which owns its Connections and PermissionAssignments. `acted_as` must be a `service`-kind Principal in the Workspace.
* `delegated`: the placement acts on behalf of a resolved user. `acted_as` must be a `person`-kind Principal, so per-user PermissionAssignment intersection evaluates a real human.

A delegated request differs only in those two fields:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "external_installation_id": "exti_0123456789abcdef0123456789abcdef",
  "agent_deployment_id": "adep_0123456789abcdef0123456789abcdef",
  "agent_deployment_revision_selection": { "type": "current" },
  "identity_posture": "delegated",
  "acted_as": "prin_fedcba9876543210fedcba9876543210",
  "surface_scope_ids": ["surf_0123456789abcdef0123456789abcdef"],
  "harness": "claude-code",
  "model_routing_profile_key": "primary",
  "sandbox_profile_key": "standard"
}
```

The lifecycle is `active ↔ disabled → archived`; `archived` is terminal. Changing `acted_as` or `identity_posture` revalidates authority and fences both the old and new Principal.

## Installed session admission derives everything

When a Session starts from an installation, the caller supplies almost nothing. The platform **derives** the rest from the placement, and supplying a derived field is **rejected, not ignored**.

An installed session request may set only:

* `agent_installation`: the placement
* `surface_scope`: which scope, within that placement, the Session runs in
* `principal`: the human on whose behalf it is being started
* `mounts`: Project mounts only
* `title`: an optional human label for the Session
* `initial_events`: up to 50 optional events, applied in order, to seed the log with at creation

It may **not** set `agent_deployment_id`, `agent_deployment_revision_number`, `version_policy`, `acted_as`, `caused_by`, `harness`, `model_routing_profile_key`, `environment`, or `compaction`. Each of those is derived from the installation. Sending any of them fails validation with `400 validation.malformed`. The request is refused, so a caller can never quietly override the Harness, model, or acting identity a placement was governed to use.

Admission then checks the full chain: an active placement in an active ExternalInstallation; the selected SurfaceScope and its complete ancestor chain all active; the requester is an active person holding `invoke` on both the Agent and the SurfaceScope; the placement's acting Principal is active, matches the declared identity posture, and holds `act_as`; and the deployment still pins its own `use_harness`, `use_model`, and `use_environment` authority. The resulting Session freezes the placement, surface, requester, actor, the exact deployment revision, and the release hash it pins. See [Two freeze horizons](/concepts/sessions-and-runs#two-freeze-horizons).

<Warning>
  Per-Session compaction overrides are rejected in v1; the frozen Agent supplies the effective policy. Check [capability status](/getting-started/status) for the live posture of the collaboration surface.
</Warning>

## Where a Session comes from

You rarely create an installed Session directly. On a live surface, the first message in a thread creates it through a [conversation binding](/concepts/conversation-bindings), which runs exactly this installed admission for you. Direct `POST /v1/sessions` with an installed request is the path only when you are not routing through a provider conversation.

## Next steps

<CardGroup cols={2}>
  <Card title="Conversation bindings" icon="link" href="/concepts/conversation-bindings">
    Turn an inbound thread into a Session, race-safely.
  </Card>

  <Card title="Add an agent to your app" icon="rocket" href="/guides/add-an-agent-to-your-app">
    The full placement-to-billing journey, end to end.
  </Card>
</CardGroup>
