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

# Outposts 合作伙伴集成

> 通过 PKCE 代码交换从合作伙伴平台连接 outpost

<Note>
  初步说明——此流程仍处于早期开发阶段，以下细节可能会
  发生变化。
</Note>

合作伙伴平台 (例如计算提供商) 可以代表客户连接 outpost。客户的 Devin Admin 在浏览器中授权该连接；随后，Devin 会创建一个 outpost 和一个服务用户，并向合作伙伴提供一个令牌，用于针对该 outpost 运行工作器。

该流程是一种结合 [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) 的简化版 OAuth 授权码交换流程。浏览器中始终只会携带一个短期有效、一次性的 **code**；服务用户令牌通过服务端到服务端交换，绝不会经过浏览器。

<div id="prerequisites">
  ## 先决条件
</div>

* **回调 allowlist。** 你使用的每个 `callback_url` 都必须在 Devin 为你的集成配置的 allowlist 中。此项由 Cognition 配置——请提前将准确的 URL 发给我们。不在列表中的 URL 会被拒绝。
* **已启用 Outposts。** 客户的账户必须已启用 Outposts。
* **Admin 授权。** 授权连接需要一位同时拥有 enterprise-settings 和 service-user management 权限的 Devin Admin。合作伙伴无需 Devin 令牌——由管理员在其自己的浏览器会话中完成授权。

<div id="flow-overview">
  ## 流程概览
</div>

```
Partner backend                 Admin's browser                 Devin
     │                                │                            │
     │ 1. gen code_verifier,          │                            │
     │    derive code_challenge       │                            │
     │ 2. redirect to app.devin.ai/outposts/connect?…code_challenge│
     │───────────────────────────────>                            │
     │                                │ 3. admin confirms, "Connect"│
     │                                │──────confirm connection──────>
     │                                │ 4. redirect callback_url?code=…  │
     │<───────────────────────────────                            │
     │ 5. POST /outposts/connection-token  (code + code_verifier) │
     │────────────────────────────────────────────────────────────>
     │ 6. { access_token, api_base_url, … }                       │
     │<────────────────────────────────────────────────────────────
     │ 7. run outpost workers with access_token                   │
```

<div id="1-generate-a-pkce-verifier-and-challenge">
  ### 1. 生成 PKCE verifier 和 challenge
</div>

在你的后端中，针对每次连接尝试：

* 生成一个高熵的随机 **`code_verifier`**：从未保留字符集 `[A-Za-z0-9-._~]` 中选取 43–128 个字符 (例如去除填充后的 `base64url(random 32 bytes)`) 。
* 对 verifier 进行 SHA-256 哈希，再将结果编码为不带填充的 base64url，得到 **`code_challenge`** (PKCE "S256") ：

```python theme={null}
import base64, hashlib, secrets

code_verifier = secrets.token_urlsafe(32)  # 43+ 个字符，使用非保留字符集
code_challenge = (
    base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
    .rstrip(b"=")
    .decode()
)
```

将 `code_verifier` 存储在服务器端 (用你用于关联最终回调的 state 作为键) 。绝不要将 verifier 发送到浏览器——只有 challenge 可以离开你的后端。

<div id="2-redirect-the-admin-to-the-connect-page">
  ### 2. 将 Admin 重定向到连接页面
</div>

将客户的 Admin 重定向到 Devin 的连接页面，并附上 challenge 和你的回调地址：

```
https://app.devin.ai/outposts/connect
  ?callback_url=https://partner.example.com/devin/outpost-callback
  &outpost_name=my-outpost
  &outpost_image=https://partner.example.com/logo.png
  &platform=linux
  &code_challenge=<code_challenge>
```

| Param            | 必填 | 备注                                                        |
| ---------------- | -- | --------------------------------------------------------- |
| `callback_url`   | 是  | Devin 会将一次性代码发送到此地址。该地址必须在你的 allowlist 中。                 |
| `code_challenge` | 是  | 步骤 1 中的 PKCE S256 挑战值。                                    |
| `outpost_name`   | 否  | 建议的 outpost 名称。Admin 可在确认前进行修改。                           |
| `outpost_image`  | 否  | 用于表示该 outpost 的 PNG icon 的 URL，例如你的公司 logo。               |
| `platform`       | 否  | 预先选择的 outpost 平台：`macos`、`linux` 或 `windows`。Admin 可以更改它。 |

如果 Admin 尚未登录，连接页面会先暂存这些参数，并提示其先登录，然后再继续流程。

<div id="3-admin-confirms">
  ### 3. Admin 确认
</div>

连接页面会显示确认界面，并提供可编辑的 outpost 名称和平台 (如果提供了你的 `outpost_image`，也会显示) 。当 Admin 点击 **Connect** 时，Devin 会验证权限、回调 allowlist 以及 outpost 名称是否可用，存储一个加密的一次性代码 (TTL 为 10 分钟) ，并将你重定向回你的 app，同时附带该一次性代码。

目前还不存在 outpost 或服务用户——只有在兑换该代码时 (步骤 5) 才会创建它们。未兑换的代码会直接过期。

<div id="4-devin-redirects-the-code-to-your-callback">
  ### 4. Devin 将该代码重定向到你的回调地址
</div>

浏览器会重定向到你的 `callback_url`，并附带该代码：

```
https://partner.example.com/devin/outpost-callback?code=<one-time code>
```

<div id="5-exchange-the-code-server-to-server">
  ### 5. 通过服务端到服务端方式兑换授权码
</div>

在你的后端中，查找你在步骤 1 中保存的 `code_verifier`，然后在令牌端点兑换该授权码。这是一个采用表单编码、符合 OAuth 风格的令牌请求：

```bash theme={null}
curl -X POST "https://api.devin.ai/outposts/connection-token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=<one-time code>" \
  --data-urlencode "code_verifier=<code_verifier>"
```

这个端点无需进行 Devin 身份验证——只要持有该代码以及与之匹配的 PKCE verifier，就足以证明身份。它特意不做身份验证：该代码是一次性的 (会以原子方式被消耗) 、有效期很短，并且与你的 challenge 绑定。

<div id="6-receive-the-credentials">
  ### 6. 接收凭据
</div>

成功后，Devin 会创建该 outpost 以及一个具有运行 outpost 工作器所需作用域的服务用户，并返回：

```json theme={null}
{
  "outpost_id": "...",
  "account_id": "...",
  "outpost_name": "my-outpost",
  "service_user_id": "...",
  "api_base_url": "https://api.devin.ai",
  "access_token": "cog_...",
  "token_type": "bearer"
}
```

响应会包含 `Cache-Control: no-store`——请勿缓存。

<div id="7-run-outpost-workers">
  ### 7. 运行 outpost 工作器
</div>

安全地存储 `access_token` 和 `api_base_url`，并将该令牌作为 Bearer 凭据，为该 outpost 运行 outpost 工作器。

<div id="error-handling">
  ## 错误处理
</div>

令牌端点遵循 [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2)。如果授权码未知、已过期、已被兑换 (重放) 或与 PKCE 不匹配，则会返回 `400`：

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "Invalid or expired connection code"
}
```

由于这些代码均为一次性使用，并会在 10 分钟后过期，因此任何 `invalid_grant` 都应视为不可恢复的错误：丢弃已存储的 `code_verifier`，并从步骤 1 重新开始该流程。

<div id="security-notes">
  ## 安全说明
</div>

* **令牌绝不会经过浏览器。** 只有一次性代码会通过重定向传递；服务用户令牌仅会通过服务端到服务端的交换返回。
* **PKCE 会将代码绑定到你。** 没有仅保存在你的后端中的 `code_verifier`，该代码就毫无用处，因此即使拦截了重定向 (或代码) ，也无法用它完成兑换。
* **代码是一次性的，且有效期很短。** 兑换时会以原子方式消耗该代码；此外，它会在 10 分钟后过期。
* **回调 URL 已加入 allowlist。** Devin 只会将代码传递到 Cognition 已为你的集成预先批准的 `callback_url`。
* **验证该代码是否属于请求连接的用户。** 确认返回到你的回调中的代码属于最初请求建立连接的同一用户。这可以防止攻击者诱使用户在毫不知情的情况下将 Devin 绑定到攻击者控制的沙盒。
* **请合理设置 `outpost_name`。** Admin 可能会覆盖它；你传入的名称只是一项建议。
