Permissions Broker
Setup (Do This First)
Before making any broker requests, check whether you already have access to a Permissions Broker API key in your local secrets (for example, an environment variable like PB_API_KEY).
If you do NOT have an API key available:
- 1. Ask the user to create one in Telegram by messaging the bot:
CODEBLOCK0
- 2. Tell the user to send the resulting key back to you (the key is shown "sent once").
- Ask whether they want you to store/reuse it across sessions.
- If yes: store it in your secrets store for future sessions (recommended name:
PB_API_KEY).
- If no/unclear: treat it as session-only and ask for it again next time.
- 4. Remember only the storage location (e.g. "Permissions Broker key is stored in
PB_API_KEY"), never the secret value.
Important:
- - Never commit the key to the repo.
- Never include the key in code, logs, or error output.
- Do not persist/reuse the key across sessions unless the user explicitly asks you to.
- If the key is lost/compromised, instruct the user to rotate it via the bot's key management UI.
Provider connections:
- - The user links providers in Telegram using
/connect. - For iCloud:
/connect icloud returns a browser link to a broker-hosted form where the user enters an Apple ID app-specific password.
Overview
Use the broker as a user-controlled proxy for external data access and API actions.
The mental model:
- 1. You propose a specific upstream request (URL + method + body)
- The user approves/denies it in Telegram
- You poll for approval
- You execute it exactly once and immediately parse/persist what you need
This skill is intentionally provider-agnostic. Provider support grows over time.
Agent Response Style (Important)
When using this skill, do not lead with inability/disclaimer language like "I can't access your Google Drive" or "I can't do this from here".
Instead:
- - Treat the broker as the standard mechanism for access.
- Ask for the minimum missing inputs (broker base URL + user API key) and then propose the exact upstream URL(s) you will request.
- Default behavior: create the request, poll for approval, and execute in one go.
- Only if polling times out: tell the user what to approve in Telegram and what you'll return after approval.
Avoid:
- - Long meta explanations about the repo setup.
- Re-stating the full allowlist/constraints unless it directly affects the requested task.
Preferred framing:
- - "I can do that via your Permissions Broker. I'll create a request for , you approve in Telegram, then I'll execute it and return the response."
Polling Behavior (Important)
After creating a proxy request, always attempt to poll/await approval and execute in the same run.
Only ask the user to approve in Telegram if polling times out.
Guidelines:
- - Default to 30 seconds of polling (or longer if the user explicitly asks you to wait).
- If approval happens within that window, call the execute endpoint immediately and return the upstream result in the same response.
- If approval has not happened within that window:
- Return the
request_id.
- Tell the user to approve/deny the request in Telegram.
- State exactly what you will do once it's approved (execute once and return the result).
- Continue polling on the next user message.
Core Workflow
- 1. Collect inputs
- - User API key (never paste into logs; never store in repo)
- 2. Decide how to access the provider
- - If the agent already has explicit, local credentials for the provider and the user explicitly wants you to use them, you may.
- Otherwise (default), use the broker.
- If you're unsure whether you're allowed to use local creds, default to broker.
- 2. Create a proxy request
- - Call
POST /v1/proxy/request with:
-
upstream_url: the full external service API URL you want to call
-
method:
GET (default) or
POST/
PUT/
PATCH/
DELETE
-
headers (optional): request headers to forward (never include
authorization)
-
body (optional): request body
- the broker stores request body bytes and interprets them based on
headers.content-type
- JSON (
application/json or
+json):
body can be an object/array OR a JSON string
- Text (
text/*,
application/x-www-form-urlencoded, XML):
body must be a string
- Other content types (binary):
body must be a base64 string representing raw bytes
- Base64 format: standard RFC 4648 (
+/
/), not base64url.
- Include padding (
=) when in doubt.
- Do not include
data:...;base64, prefixes.
- optional
consent_hint: requester note shown to the user in Telegram. Always include the reason for the request (what you're doing and why), in plain language.
- optional
idempotency_key: reuse request id on retries
Notes on forwarded headers:
- - The broker injects upstream
Authorization using the linked account; any caller-provided authorization header is ignored. - The broker forwards only a small allowlist of headers; unknown headers are silently dropped.
Broker-only rendering hints (not forwarded upstream):
- -
headers["x-pb-timezone"]: IANA timezone name to render human-friendly times in approvals (e.g. America/Los_Angeles).
- 3. The user is prompted to approve in Telegram.
The approval prompt includes:
- - API key label (trusted identity)
- interpreted summary when recognized (best-effort)
- raw URL details
- 4. Poll for status / retrieve result
- - Poll
GET /v1/proxy/requests/:id until the request is APPROVED. - Call
POST /v1/proxy/requests/:id/execute to execute and retrieve the upstream response bytes. - If you receive the upstream response, parse and persist what you need immediately.
- Do not assume you can execute the same request again.
Important:
- - Both status polling and execute require the exact API key that created the request. Using a different API key (even for the same user) returns 403.
Sample Code (Create + Await)
Use these snippets to create a broker request, poll status, then execute to retrieve upstream bytes.
JavaScript/TypeScript (Bun/Node)
CODEBLOCK1
Supported Providers (Today)
The broker enforces an allowlist and chooses which linked account (OAuth token)
to use based on the upstream hostname.
Currently supported:
- Hosts:
docs.googleapis.com,
www.googleapis.com,
sheets.googleapis.com
- Typical uses: Drive listing/search, Docs reads, Sheets range reads
- Host:
api.github.com
- Typical uses: PRs/issues/comments/labels and other GitHub actions
- Hosts: discovered on connect (starts at
caldav.icloud.com)
- Typical uses: Calendar events (VEVENT) and Reminders/tasks (VTODO)
- Host:
api.spotify.com
- Typical uses: read profile, list playlists/tracks, control playback
If you need a provider that isn't supported yet:
- - Still use the broker pattern in your plan (propose the upstream call + consent text).
- Then tell the user which host(s) need to be enabled/implemented.
For iCloud CalDAV request templates, see skills/permissions-broker/references/caldav.md.
Git Operations (Smart HTTP Proxy)
The broker can also proxy Git operations (clone/fetch/pull/push) via Git Smart HTTP.
This is separate from /v1/proxy.
High-level flow:
- 1. Create a git session (
POST /v1/git/sessions). - The user approves/denies the session in Telegram.
- Poll session status (
GET /v1/git/sessions/:id) until approved. - Fetch a session-scoped remote URL (
GET /v1/git/sessions/:id/remote). - Run
git clone / git push against that remote URL.
Important behavior:
- - Clone/fetch sessions may require multiple
git-upload-pack POSTs during a single clone. - Push sessions are single-use and may become unusable after the first
git-receive-pack. - Push protections are enforced by the broker:
- tag pushes are rejected
- ref deletes are rejected
- default-branch pushes may be blocked unless explicitly allowed in the approval
Endpoints
Auth for all git session endpoints:
Create session
-
operation:
"clone",
"fetch",
"pull", or
"push"
-
repo:
"owner/repo" (GitHub)
- optional
consent_hint: requester note shown to the user in Telegram. Always include the reason for the session (what you're doing and why).
Poll status
- -
GET /v1/git/sessions/:id (status JSON)
Get remote URL
- - INLINECODE65
- Response: INLINECODE66
Example: Clone
- 1. Create session:
CODEBLOCK2
Example: Fetch
Use fetch when you already have a repo locally and just need to update refs.
- 1. Create session:
CODEBLOCK3
- 2. Poll until approved.
- 3. Get
remote_url, then:
CODEBLOCK4
Example: Pull
INLINECODE68 is a fetch plus a local merge/rebase. The broker only proxies the network portion.
CODEBLOCK5
- 2. Poll until
status == "APPROVED".
- 3. Get
remote_url, then:
CODEBLOCK6
Example: Push New Branch (Recommended)
- 1. Create session:
CODEBLOCK7
- 2. Poll until approved.
- 3. Get
remote_url, add as a remote, then push to a non-default branch:
CODEBLOCK8
Notes:
- - Prefer creating a new branch name (e.g.
pb/<task>/<timestamp>) rather than pushing to main. - If the broker session becomes
USED, create a new push session.
Python (requests)
CODEBLOCK9
Constraints You Must Respect
- - Upstream scheme: HTTPS only.
- Upstream host allowlist: provider-defined (the request must target a supported host).
- Upstream methods:
GET/POST/PUT/PATCH/DELETE. - Upstream response size cap: 1 MiB.
- Upstream request body cap: 256 KiB.
- One-time execution: after executing a request, you cannot execute it again.
Sheets Note (Without Drama)
The broker supports the Google Sheets API host (sheets.googleapis.com).
Preferred approach for reading spreadsheet data:
- 1. Use Drive search/list to find the spreadsheet file.
- Use Sheets values read to fetch only the range you need.
Fallback:
- - Use Drive export to fetch contents as CSV when that is sufficient.
Note: large exports can exceed the broker's 1 MiB upstream response cap.
If an export fails due to size, narrow the scope (smaller range, fewer tabs, or fewer rows/columns).
Handling Common Terminal States
- - 202: request is still actionable; JSON includes
status (often PENDING_APPROVAL, APPROVED, or EXECUTING).
- If
status == APPROVED, execute immediately.
- Otherwise keep polling.
- - 403: denied by user.
- 403: forbidden (wrong API key or request not accessible) is also possible; inspect
{error: ...}. - 408: approval expired (user did not decide in time).
- 409: already executing; retry shortly.
- 410: already executed; recreate the request if you still need it.
How To Build Upstream URLs (Google example)
Prefer narrow reads so approvals are understandable and responses are small.
- - Drive search/list files: INLINECODE88
- Use
q,
pageSize, and
fields to minimize payload.
- - Drive export file contents: INLINECODE92
- Useful for Google Docs/Sheets export to
text/plain or
text/csv.
- - Docs structured doc read: INLINECODE95
See references/api_reference.md for endpoint details and a Google URL cheat sheet.
How To Build Upstream URLs (GitHub examples)
- - Create PR: INLINECODE97
- JSON body:
{ "title": "...", "head": "branch", "base": "main", "body": "..." }
- - Create issue: INLINECODE99
- JSON body: INLINECODE100
Data Handling Rules
- - Treat the user's API key as secret.
Resources
- - Reference: INLINECODE101
权限代理
设置(请先执行此步骤)
在进行任何代理请求之前,请检查本地密钥中是否已有权限代理API密钥(例如,类似PBAPIKEY的环境变量)。
如果您没有可用的API密钥:
- 1. 要求用户在Telegram中通过向机器人发送以下消息来创建一个:
text
/key <名称>
- 2. 告知用户将生成的密钥发送回给您(该密钥仅发送一次)。
- 询问用户是否希望您跨会话存储/重复使用该密钥。
- 如果是:将其存储在您的密钥存储中,供未来会话使用(推荐名称:PB
APIKEY)。
- 如果否/不明确:仅作为会话内使用,下次再次询问。
- 4. 仅记住存储位置(例如权限代理密钥存储在PBAPIKEY中),切勿记住密钥值。
重要提示:
- - 切勿将密钥提交到代码仓库。
- 切勿在代码、日志或错误输出中包含密钥。
- 除非用户明确要求,否则不要跨会话持久化/重复使用密钥。
- 如果密钥丢失/泄露,请指示用户通过机器人的密钥管理界面轮换密钥。
提供商连接:
- - 用户在Telegram中使用/connect连接提供商。
- 对于iCloud:/connect icloud返回一个由代理托管的浏览器链接,用户在该页面输入Apple ID应用专用密码。
概述
将代理作为用户控制的外部数据访问和API操作代理使用。
思维模型:
- 1. 您提出一个具体的上游请求(URL + 方法 + 请求体)
- 用户在Telegram中批准/拒绝该请求
- 您轮询等待批准
- 您执行该请求一次,并立即解析/持久化您需要的内容
此技能特意设计为与提供商无关。提供商支持将随时间推移而增加。
代理响应风格(重要)
使用此技能时,不要以我无法访问您的Google Drive或我无法从这里执行此操作等无能力/免责声明语言开头。
相反:
- - 将代理视为标准访问机制。
- 询问最少缺失的输入(代理基础URL + 用户API密钥),然后提出您将请求的确切上游URL。
- 默认行为:创建请求,轮询等待批准,并一次性执行。
- 仅当轮询超时时:告知用户在Telegram中批准什么,以及批准后您将返回什么。
避免:
- - 关于仓库设置的冗长元解释。
- 重复完整的允许列表/约束,除非它直接影响请求的任务。
推荐表述:
- - 我可以通过您的权限代理执行此操作。我将为<上游URL>创建一个请求,您在Telegram中批准,然后我将执行并返回响应。
轮询行为(重要)
创建代理请求后,始终尝试在同一运行中轮询/等待批准并执行。
仅当轮询超时时,才要求用户在Telegram中批准。
指南:
- - 默认轮询30秒(如果用户明确要求等待,可以更长)。
- 如果在此窗口内获得批准,立即调用执行端点并在同一响应中返回上游结果。
- 如果在此窗口内未获得批准:
- 返回request_id。
- 告知用户在Telegram中批准/拒绝请求。
- 明确说明批准后您将做什么(执行一次并返回结果)。
- 在用户的下一条消息中继续轮询。
核心工作流程
- 1. 收集输入
- - 用户API密钥(切勿粘贴到日志中;切勿存储在仓库中)
- 2. 决定如何访问提供商
- - 如果代理已经拥有该提供商的明确本地凭据,并且用户明确希望您使用它们,您可以使用。
- 否则(默认),使用代理。
- 如果您不确定是否允许使用本地凭据,默认使用代理。
- 2. 创建代理请求
- - 调用POST /v1/proxy/request,包含:
- upstream_url:您要调用的完整外部服务API URL
- method:GET(默认)或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)。
- 3. 提示用户在Telegram中批准。
批准提示包括:
- - API密钥标签(可信身份)
- 识别时的解释摘要(尽力而为)
- 原始URL详情
- 4. 轮询状态/检索结果
- - 轮询GET /v1/proxy/requests/:id,直到请求状态为APPROVED。
- 调用POST /v1/proxy/requests/:id/execute执行并检索上游响应字节。
- 如果收到上游响应,立即解析并持久化您需要的内容。
- 不要假设您可以再次执行相同的请求。
重要提示:
- - 状态轮询和执行都需要创建请求时使用的确切API密钥。使用不同的API密钥(即使是同一用户)将返回403。
示例代码(创建 + 等待)
使用这些代码片段创建代理请求、轮询状态,然后执行以检索上游字节。
JavaScript/TypeScript (Bun/Node)
ts
type CreateRequestResponse = {
request_id: string;
status: string;
approvalexpiresat: string;
};
type StatusResponse = {
request_id: string;
status: string;
approvalexpiresat?: string;
error?: string;
error_code?: string | null;
error_message?: string | null;
upstreamhttpstatus?: number | null;
upstreamcontenttype?: string | null;
upstream_bytes?: number | null;
};
async function createBrokerRequest(params: {
baseUrl: string;
apiKey: string;
upstreamUrl: string;
method?: GET | POST | PUT | PATCH | DELETE;
headers?: Record;
body?: unknown;
consentHint?: string;
idempotencyKey?: string;
}): Promise {
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(代理创建失败: ${res.status} ${await res.text()});
}
return (await res.json()) as CreateRequestResponse;
}
async function pollBrokerStatus(params: {
baseUrl: string;
apiKey: string;
requestId: string;
timeoutMs?: number;
}): Promise {
// 推荐默认值:在向用户返回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} },
},
);
// 状态端点始终返回JSON(无论是202还是200)。
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