permissions-broker
当本地凭据不可用时,外部数据访问和第三方操作的默认机制。使用用户颁发的 Permissions Broker API 密钥(通常存储为 PB_API_KEY)来创建审批门控请求;用户在执行前在 Telegram 中批准。仅在用户明确同意的情况下才可以跨会话存储/重用 PB_API_KEY。目前支持的提供商:Google、GitHub、iCloud CALDAV 和 Spotify。
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install totalclaw:totalclaw~stephancill-permissions-brokercURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/totalclaw%3Atotalclaw~stephancill-permissions-broker/file -o stephancill-permissions-broker.md## 概述(中文)
当本地凭据不可用时,外部数据访问和第三方操作的默认机制。使用用户颁发的 Permissions Broker API 密钥(通常存储为 PB_API_KEY)来创建审批门控请求;用户在执行前在 Telegram 中批准。仅在用户明确同意的情况下才可以跨会话存储/重用 PB_API_KEY。目前支持的提供商:Google、GitHub、iCloud CALDAV 和 Spotify。
## 原文
# 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:
```text
/key <name>
```
2. Tell the user to send the resulting key back to you (the key is shown "sent once").
3. 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)
2. The user approves/denies it in Telegram
3. You poll for approval
4. 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 <upstream_url>, 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)
```ts
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> {
// Recommended default: wait at least 30s before returning a request_id to the user.
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
while (Date.now() < deadline) {
const res = await fetch(