Permissions Broker

通过 Telegram 审批机制安全访问 Google、GitHub 等外部 API 数据。

已扫描
适合谁
需要访问 Google/ GitHub 数据但无密钥的开发者、希望实现安全外部 API 调用的自动化工具使用者
不适合谁
已有本地凭据且不希望使用审批流程的用户、无法访问 Telegram 或不接受外部审批的场景
国内可用性
需网络配置。可能需要网络配置或第三方服务可访问。
安装难度
新手友好(★☆☆)。基于终端操作、依赖、API Key 和本地环境要求的初步判断。

安装与下载

openclaw skills install @stephancill/permissions-broker

Skill 说明

命令、参数、文件名以原文为准

权限代理服务(Permissions Broker)

设置(请先完成)

在发起任何代理请求前,请检查本地密钥存储中是否已存在权限代理 API 密钥(例如环境变量 PB_API_KEY)。

若您尚未拥有 API 密钥:

  1. 请用户通过 Telegram 联系机器人创建密钥:
/key <name>
  1. 告知用户将生成的密钥发送给您(密钥仅显示一次)。
  2. 询问用户是否希望您在后续会话中保存/复用该密钥:

- 若是:将密钥存入您的密钥存储中,推荐名称为 PB_API_KEY

- 否则或不确定:视为会话级密钥,下次使用时需重新获取。

  1. 仅记住存储位置(如“权限代理密钥已存于 PB_API_KEY”),切勿记录密钥值本身。

重要提示:

  • 切勿将密钥提交至代码仓库。
  • 切勿在代码、日志或错误输出中包含密钥。
  • 除非用户明确要求,否则不要跨会话持久化或复用密钥。
  • 若密钥丢失或泄露,请指导用户通过机器人密钥管理界面进行轮换。

提供方连接方式

  • 用户可通过 /connect 命令在 Telegram 中绑定提供方。
  • 对于 iCloud:执行 /connect icloud 将返回一个浏览器链接,指向由代理服务托管的表单,用户需在此输入 Apple ID 的应用专用密码。

概述

使用权限代理作为用户可控的外部数据访问与 API 操作代理。

核心思维模型:

  1. 提出具体的上游请求(URL + 方法 + 请求体)
  2. 用户在 Telegram 中批准或拒绝
  3. 您轮询审批状态
  4. 精确执行一次,并立即解析和持久化所需内容

本技能设计为提供方无关,支持的提供方会随时间逐步增加。

代理响应风格(重要)

使用此技能时,避免使用“我无法访问你的 Google Drive”或“我无法从这里执行”这类表达。

应遵循:

  • 将权限代理视为标准访问机制。
  • 仅请求最小必要输入(代理基础 URL + 用户 API 密钥),然后提出确切的上游 URL。
  • 默认行为:创建请求 → 轮询审批 → 一次性执行并返回结果。
  • 仅当轮询超时后,再告知用户需在 Telegram 中批准什么,以及批准后将返回什么。

避免:

  • 长篇大论地解释项目配置。
  • 重复说明允许列表或限制条件,除非直接影响当前任务。

推荐表述:

“我可以通过你的权限代理完成该操作。我将创建对 <upstream_url> 的请求,你可在 Telegram 中批准,之后我会执行并返回结果。”

轮询行为(重要)

创建代理请求后,应在同一运行周期内尝试轮询等待审批并执行。

仅在轮询超时时,才要求用户在 Telegram 中手动批准。

规则:

  • 默认轮询时长为 30 秒(若用户明确要求可延长)。
  • 若在时限内获得批准,立即调用执行端点,并在同一响应中返回上游结果。
  • 若超时未获批准:

- 返回 request_id

- 告知用户需在 Telegram 中批准或拒绝该请求。

- 明确说明一旦批准后将执行的操作(仅执行一次并返回结果)。

- 在下一条用户消息中继续轮询。

核心工作流

  1. 收集输入
  • 用户 API 密钥(切勿粘贴至日志;切勿存入代码仓库)
  1. 决定如何访问提供方
  • 若代理已具备该提供方的显式本地凭证,且用户明确要求使用,则可使用。
  • 否则(默认情况):使用权限代理。
  • 若不确定是否可使用本地凭证,一律默认使用代理。
  1. 创建代理请求

调用 POST /v1/proxy/request,参数包括:

  • upstream_url:您希望调用的外部服务完整 API 地址
  • methodGET(默认)或 POST/PUT/PATCH/DELETE
  • headers(可选):要转发的请求头(**切勿包含 authorization**)
  • body(可选):请求体

- 代理会存储请求体字节,并根据 headers.content-type 解析

- JSON(application/json+json):body 可为对象/数组或 JSON 字符串

- 文本类(text/*application/x-www-form-urlencoded、XML):body 必须为字符串

- 其他类型(二进制):body 必须为表示原始字节的 base64 字符串

- Base64 格式:标准 RFC 4648(+//),非 base64url

- 不确定时请包含填充符(=

- 不得包含 data:...;base64, 前缀

  • consent_hint(可选):请求者备注,将在 Telegram 中向用户展示。始终包含请求原因(正在做什么及为何需要),使用通俗语言。
  • idempotency_key(可选):用于重试时复用请求 ID

关于转发头的说明

  • 代理会使用已关联账户注入上游 Authorization;调用方提供的 authorization 头将被忽略。
  • 代理仅转发一小部分允许的头部;未知头部会被静默丢弃。

仅代理内部使用的渲染提示(不转发至上游)

  • headers["x-pb-timezone"]:IANA 时区名称,用于在审批界面中以人类友好格式显示时间(如 America/Los_Angeles
  1. 用户在 Telegram 中收到审批提示

审批内容包括:

  • API 密钥标签(可信身份)
  • 识别后的摘要信息(尽力而为)
  • 原始 URL 详情
  1. 轮询状态 / 获取结果
  • 轮询 GET /v1/proxy/requests/:id 直至请求状态为 APPROVED
  • 调用 POST /v1/proxy/requests/:id/execute 执行并获取上游响应字节
  • 若收到上游响应,请立即解析并持久化所需内容
  • 不可假设可再次执行相同请求

重要提醒:

  • 状态轮询与执行均需使用创建请求时的完全相同的 API 密钥。使用不同密钥(即使属于同一用户)也会返回 403 错误。

示例代码(创建并等待)

# 权限代理(Permissions Broker)

版本:1.0.9
分块:2/4

请使用以下代码片段创建代理请求、轮询状态,然后执行以获取上游数据。

## JavaScript/TypeScript (Bun/Node)

type CreateRequestResponse = {

request_id: string;

status: string;

approval_expires_at: string;

};

type StatusResponse = {

request_id: string;

status: string;

approval_expires_at?: string;

error?: string;

error_code?: string | null;

error_message?: string | null;

upstream_http_status?: number | null;

upstream_content_type?: string | null;

upstream_bytes?: number | null;

};

async function createBrokerRequest(params: {

baseUrl: string;

apiKey: string;

upstreamUrl: string;

method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

headers?: Record<string, string>;

body?: unknown;

consentHint?: string;

idempotencyKey?: string;

}): Promise<CreateRequestResponse> {

const res = await fetch(${params.baseUrl}/v1/proxy/request, {

method: "POST",

headers: {

authorization: Bearer ${params.apiKey},

"content-type": "application/json",

},

body: JSON.stringify({

upstream_url: params.upstreamUrl,

method: params.method ?? "GET",

headers: params.headers,

body: params.body,

consent_hint: params.consentHint,

idempotency_key: params.idempotencyKey,

}),

});

if (!res.ok) {

throw new Error(broker create failed: ${res.status} ${await res.text()});

}

return (await res.json()) as CreateRequestResponse;

}

async function pollBrokerStatus(params: {

baseUrl: string;

apiKey: string;

requestId: string;

timeoutMs?: number;

}): Promise<StatusResponse> {

// 推荐默认值:在返回 request_id 给用户前至少等待 30 秒。

const deadline = Date.now() + (params.timeoutMs ?? 30_000);

while (Date.now() < deadline) {

const res = await fetch(

${params.baseUrl}/v1/proxy/requests/${params.requestId},

{

headers: { authorization: Bearer ${params.apiKey} },

},

);

// 状态接口对 202 和 200 均返回 JSON。

const data = (await res.json()) as StatusResponse;

// APPROVED 返回 HTTP 202,因此必须检查 JSON 内容。

if (data.status === "APPROVED") return data;

if (res.status === 202) {

await new Promise((r) => setTimeout(r, 1000));

continue;

}

// 终止或可操作的状态(仅状态的 JSON)。

if (!res.ok && res.status !== 403 && res.status !== 408) {

throw new Error(broker status failed: ${res.status} ${JSON.stringify(data)});

}

return data;

}

throw new Error("timed out waiting for approval");

}

async function awaitApprovalThenExecute(params: {

baseUrl: string;

apiKey: string;

requestId: string;

timeoutMs?: number;

}): Promise<Response> {

const status = await pollBrokerStatus({

baseUrl: params.baseUrl,

apiKey: params.apiKey,

requestId: params.requestId,

timeoutMs: params.timeoutMs,

});

if (status.status !== "APPROVED") {

throw new Error(request not approved yet (status=${status.status}));

}

return executeBrokerRequest({

baseUrl: params.baseUrl,

apiKey: params.apiKey,

requestId: params.requestId,

});

}

async function getBrokerStatusOnce(params: {

baseUrl: string;

apiKey: string;

requestId: string;

}): Promise<StatusResponse> {

const res = await fetch(${params.baseUrl}/v1/proxy/requests/${params.requestId}, {

headers: { authorization: Bearer ${params.apiKey} },

});

// 始终返回 JSON(即使状态为 202)。

return (await res.json()) as StatusResponse;

}

async function executeBrokerRequest(params: {

baseUrl: string;

apiKey: string;

requestId: string;

}): Promise<Response> {

const res = await fetch(

${params.baseUrl}/v1/proxy/requests/${params.requestId}/execute,

{

method: "POST",

headers: { authorization: Bearer ${params.apiKey} },

},

);

// 终止状态:上游响应字节(2xx/4xx/5xx)或代理错误 JSON(403/408/409/410 等)。

// 重要说明:

// - 执行操作仅可进行一次;后续调用将返回 410。

// - 代理会镜像上游 HTTP 状态码和内容类型,并添加 X-Proxy-Request-Id 头。

// - 上游非 2xx 状态仍会返回给调用方作为字节数据,但代理会记录 status=FAILED。

return res;

}

// 推荐控制流程:

// - 轮询约 30 秒。

// - 若仍为 pending,向用户返回提示信息,包含 request_id 及需批准的操作。

// - 用户下一次消息到达时,再次轮询(或重新创建请求,若已过期或已消耗)。

// 使用示例

// const baseUrl = "https://permissions-broker.steer.fun"

// const apiKey = process.env.PB_API_KEY!

// const upstreamUrl = "https://www.googleapis.com/drive/v3/files?pageSize=5&fields=files(id,name)"

// const created = await createBrokerRequest({ baseUrl, apiKey, upstreamUrl, consentHint: "列出几个 Drive 文件。" })

// 告知用户:在 Telegram 中批准该请求

// const execRes = await awaitApprovalThenExecute({ baseUrl, apiKey, requestId: created.request_id, timeoutMs: 30_000 })

// const bodyText = await execRes.text()

// GitHub 示例(创建 PR)

// const created = await createBrokerRequest({

// baseUrl,

// apiKey,

// upstreamUrl: "https://api.github.com/repos/OWNER/REPO/pulls",

// method: "POST",

// headers: { "content-type": "application/json" },

// body: {

// title: "我的 PR",

// head: "feature-branch",

// base: "main",

// body: "通过 Permissions Broker 创建",

// },

// consentHint: "为 feature-branch 创建一个 PR"

// })

## 当前支持的提供方

代理强制实施白名单策略,并根据上游域名自动选择对应的已关联账户(OAuth token)。

目前支持的提供方:

- Google
  - 主机:`docs.googleapis.com`、`www.googleapis.com`、`sheets.googleapis.com`
  - 典型用途:Drive 文件列表/搜索、文档读取、表格范围数据读取
- GitHub
  - 主机:`api.github.com`
  - 典型用途:拉取请求(PR)、问题(Issue)、评论、标签及其他 GitHub 操作
- iCloud(CalDAV)
  - 主机:连接时动态发现(起始为 `caldav.icloud.com`)
  - 典型用途:日历事件(VEVENT)和提醒事项/任务(VTODO)
- Spotify
  - 主机:`api.spotify.com`
  - 典型用途:读取用户资料、列出播放列表/歌曲、控制播放

如果需要使用尚未支持的提供方:

- 仍可采用代理模式设计你的计划(提出上游调用 + 同意文本)。
- 然后告知用户需启用或实现哪些主机。

关于 iCloud CalDAV 请求模板,请参阅:`skills/permissions-broker/references/caldav.md`。

## Git 操作(智能 HTTP 代理)

该代理还可通过 Git 智能 HTTP 协议代理克隆、获取、拉取和推送操作。

此功能与 `/v1/proxy` 接口独立。

整体流程如下:

1. 创建 Git 会话(`POST /v1/git/sessions`)。
2. 用户在 Telegram 中批准或拒绝该会话。
3. 轮询会话状态(`GET /v1/git/sessions/:id`),直到获得批准。
4. 获取会话专属的远程地址(`GET /v1/git/sessions/:id/remote`)。
5. 使用该远程地址执行 `git clone` 或 `git push`。

重要行为说明:

- 克隆或获取会话在单次克隆过程中可能需要多次 `git-upload-pack` POST 请求。
- 推送会话为一次性使用,首次执行 `git-receive-pack` 后即失效。
- 推送操作受代理强制保护:
  - 拒绝标签推送
  - 拒绝引用删除
  - 默认分支推送可能被阻止,除非在审批时明确允许

### 接口列表

所有 Git 会话接口的认证方式:

- `Authorization: Bearer <USER_API_KEY>`

创建会话

- `POST /v1/git/sessions`
- JSON 请求体:
  - `operation`: `"clone"`、`"fetch"`、`"pull"` 或 `"push"`
  - `repo`: `"owner/repo"`(GitHub 仓库格式)
  - 可选字段 `consent_hint`:请求者备注,将在 Telegram 中显示给用户。请始终包含会话原因(你正在做什么以及为什么)。
- 响应示例:

{ "session_id": "...", "status": "PENDING_APPROVAL", "approval_expires_at": "..." }

轮询状态

- `GET /v1/git/sessions/:id`
- 返回状态 JSON

获取远程地址

- `GET /v1/git/sessions/:id/remote`
- 响应示例:

{ "remote_url": "https://..." }

### 示例:克隆

1. 创建会话:

{

"operation": "clone",

"repo": "OWNER/REPO",

"consent_hint": "克隆仓库以检查代码"

}

### 示例:获取

适用于本地已存在仓库,仅需更新引用(refs)的情况。

1. 创建会话:

{

"operation": "fetch",

"repo": "OWNER/REPO",

"consent_hint": "获取最新引用以更新本地检出"

}

2. 轮询直至状态变为 `APPROVED`。
3. 获取 `remote_url` 后执行:

git fetch "<remote_url>" --prune

### 示例:拉取

`git pull` 包含 `fetch` 和本地合并/变基操作。本代理仅代理网络部分。

git pull "<remote_url>" main

1. 轮询至 `status == "APPROVED"`。
2. 获取 `remote_url` 后执行:

git clone "<remote_url>" ./repo

### 示例:推送新分支(推荐做法)

1. 创建会话:

{

"operation": "push",

"repo": "OWNER/REPO",

"consent_hint": "推送分支 feature-x 用于创建 PR"

}

2. 轮询直至获得批准。
3. 获取 `remote_url`,添加为远程仓库,然后推送到非默认分支:

git remote add broker "<remote_url>"

git push broker "HEAD:refs/heads/feature-x"

注意事项:

- 推荐使用新分支名(如 `pb/<task>/<timestamp>`),避免直接推送到 `main`。
- 若会话状态变为 `USED`,请创建新的推送会话。

Python(requests)示例代码:

import time

import requests

def create_request(base_url, api_key, upstream_url, consent_hint=None, idempotency_key=None):

# 可选:非 GET 请求的方法/请求头/请求体。

r = requests.post(

f"{base_url}/v1/proxy/request",

headers={"Authorization": f"Bearer {api_key}"},

json={

"upstream_url": upstream_url,

# "method": "POST",

# "headers": {"accept": "application/vnd.github+json"},

# "headers": {"content-type": "application/json"},

# "body": {"title": "...", "head": "...", "base": "main"},

"consent_hint": consent_hint,

"idempotency_key": idempotency_key,

},

timeout=30,

)

r.raise_for_status()

return r.json()

def await_result(base_url, api_key, request_id, timeout_s=120):

deadline = time.time() + timeout_s

while time.time() < deadline:

r = requests.get(

f"{base_url}/v1/proxy/requests/{request_id}",

headers={"Authorization": f"Bearer {api_key}"},

timeout=30,

)

if r.status_code == 202:

time.sleep(1)

continue

# 终端响应(仅状态的 JSON)

return r.json()

raise TimeoutError("timed out waiting for approval")

def execute_request(base_url, api_key, request_id):

# 重要:执行操作为一次性;请立即读取并保存结果。

return requests.post(

f"{base_url}/v1/proxy/requests/{request_id}/execute",

headers={"Authorization": f"Bearer {api_key}"},

timeout=60,

)

def await_approval_then_execute(base_url, api_key, request_id, timeout_s=30):

status = await_result(base_url, api_key, request_id, timeout_s=timeout_s)

if status.get("status") != "APPROVED":

raise RuntimeError(f"请求未获批准 (status={status.get('status')})")

return execute_request(base_url, api_key, request_id)

## 必须遵守的限制

- 上游协议:仅支持 HTTPS
- 上游主机白名单:由提供方定义(请求必须指向受支持的主机)
- 上游方法:`GET`、`POST`、`PUT`、`PATCH`、`DELETE`
- 上游响应大小上限:1 MiB
- 上游请求体大小上限:256 KiB
- 一次性执行:执行一次后无法再次执行

## Sheets 注意事项(无复杂剧情)

该代理支持 Google Sheets API 主机(`sheets.googleapis.com`)。

读取电子表格数据的推荐方式:

---
name: Permissions Broker
version: 1.0.9
description: 用于在用户授权后,安全地代理访问外部 API 的中间服务。
summary: 通过权限中介机制,实现对 Google Drive、Google Sheets、GitHub 等平台的受控访问。

## 使用建议

- 使用 Drive 的搜索/列表功能查找电子表格文件。
- 使用 Sheets 值读取功能,仅获取所需的数据范围。

### 备用方案

- 当内容以 CSV 格式导出已足够时,可使用 Drive 导出功能获取文件内容。
- 注意:大文件导出可能超过中介服务 1 MiB 的上游响应上限。
  - 若因文件过大导致导出失败,请缩小范围(如减少行数、列数或工作表数量)。

## 常见终端状态处理

- **202**:请求仍可处理;返回的 JSON 包含 `status` 字段(常见值为 `PENDING_APPROVAL`、`APPROVED` 或 `EXECUTING`)。
  - 若 `status == APPROVED`,立即执行操作。
  - 否则持续轮询状态。
- **403**:用户拒绝授权。
- **403**:权限不足(如 API 密钥错误或请求不可访问);请检查返回的 `{error: ...}` 信息。
- **408**:审批超时(用户未及时响应)。
- **409**:操作正在执行中;稍后重试。
- **410**:操作已执行过;若仍需执行,请重新创建请求。

## 构建上游 URL(Google 示例)

建议采用精确读取方式,使授权请求更清晰,响应数据更小。

- **Drive 搜索/列出文件**:`https://www.googleapis.com/drive/v3/files?...`
  - 使用 `q`、`pageSize` 和 `fields` 参数最小化返回数据量。
- **导出文件内容**:`https://www.googleapis.com/drive/v3/files/{fileId}/export?mimeType=...`
  - 适用于将 Google Docs/Sheets 导出为 `text/plain` 或 `text/csv` 格式。
- **结构化文档读取**:`https://docs.googleapis.com/v1/documents/{documentId}?fields=...`

详细接口说明及 Google URL 快捷参考,请参阅 `references/api_reference.md`。

## 构建上游 URL(GitHub 示例)

- **创建 Pull Request**:`POST https://api.github.com/repos/<owner>/<repo>/pulls`
  - 请求体为 JSON 格式:`{ "title": "...", "head": "branch", "base": "main", "body": "..." }`
- **创建 Issue**:`POST https://api.github.com/repos/<owner>/<repo>/issues`
  - 请求体为 JSON 格式:`{ "title": "...", "body": "..." }`

## 数据处理规则

- 用户的 API 密钥视为敏感信息,必须妥善保管,禁止泄露。

## 资源

- 参考文档:`references/api_reference.md`
S
@stephancill

已收录 1 个 Skill

相关推荐