{
  "schemaVersion": "1.0",
  "item": {
    "slug": "permissions-broker",
    "name": "Permissions Broker",
    "source": "tencent",
    "type": "skill",
    "category": "效率提升",
    "sourceUrl": "https://clawhub.ai/stephancill/permissions-broker",
    "canonicalUrl": "https://clawhub.ai/stephancill/permissions-broker",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/permissions-broker",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=permissions-broker",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md",
      "references/caldav.md",
      "references/api_reference.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/permissions-broker"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/permissions-broker",
    "agentPageUrl": "https://openagent3.xyz/skills/permissions-broker/agent",
    "manifestUrl": "https://openagent3.xyz/skills/permissions-broker/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/permissions-broker/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Setup (Do This First)",
        "body": "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).\n\nIf you do NOT have an API key available:\n\nAsk the user to create one in Telegram by messaging the bot:\n\n/key <name>\n\nTell the user to send the resulting key back to you (the key is shown \"sent once\").\nAsk whether they want you to store/reuse it across sessions.\n\nIf yes: store it in your secrets store for future sessions (recommended name: PB_API_KEY).\nIf no/unclear: treat it as session-only and ask for it again next time.\n\n\nRemember only the storage location (e.g. \"Permissions Broker key is stored in PB_API_KEY\"), never the secret value.\n\nImportant:\n\nNever commit the key to the repo.\nNever include the key in code, logs, or error output.\nDo not persist/reuse the key across sessions unless the user explicitly asks you to.\nIf the key is lost/compromised, instruct the user to rotate it via the bot's key management UI.\n\nProvider connections:\n\nThe user links providers in Telegram using /connect.\nFor iCloud: /connect icloud returns a browser link to a broker-hosted form where the user enters an Apple ID app-specific password."
      },
      {
        "title": "Overview",
        "body": "Use the broker as a user-controlled proxy for external data access and API actions.\n\nThe mental model:\n\nYou propose a specific upstream request (URL + method + body)\nThe user approves/denies it in Telegram\nYou poll for approval\nYou execute it exactly once and immediately parse/persist what you need\n\nThis skill is intentionally provider-agnostic. Provider support grows over time."
      },
      {
        "title": "Agent Response Style (Important)",
        "body": "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\".\n\nInstead:\n\nTreat the broker as the standard mechanism for access.\nAsk for the minimum missing inputs (broker base URL + user API key) and then propose the exact upstream URL(s) you will request.\nDefault behavior: create the request, poll for approval, and execute in one go.\nOnly if polling times out: tell the user what to approve in Telegram and what you'll return after approval.\n\nAvoid:\n\nLong meta explanations about the repo setup.\nRe-stating the full allowlist/constraints unless it directly affects the requested task.\n\nPreferred framing:\n\n\"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.\""
      },
      {
        "title": "Polling Behavior (Important)",
        "body": "After creating a proxy request, always attempt to poll/await approval and execute in the same run.\nOnly ask the user to approve in Telegram if polling times out.\n\nGuidelines:\n\nDefault to 30 seconds of polling (or longer if the user explicitly asks you to wait).\nIf approval happens within that window, call the execute endpoint immediately and return the upstream result in the same response.\nIf approval has not happened within that window:\n\nReturn the request_id.\nTell the user to approve/deny the request in Telegram.\nState exactly what you will do once it's approved (execute once and return the result).\nContinue polling on the next user message."
      },
      {
        "title": "Core Workflow",
        "body": "Collect inputs\n\nUser API key (never paste into logs; never store in repo)\n\nDecide how to access the provider\n\nIf the agent already has explicit, local credentials for the provider and the user explicitly wants you to use them, you may.\nOtherwise (default), use the broker.\nIf you're unsure whether you're allowed to use local creds, default to broker.\n\nCreate a proxy request\n\nCall POST /v1/proxy/request with:\n\nupstream_url: the full external service API URL you want to call\nmethod: GET (default) or POST/PUT/PATCH/DELETE\nheaders (optional): request headers to forward (never include authorization)\nbody (optional): request body\n\nthe broker stores request body bytes and interprets them based on headers.content-type\nJSON (application/json or +json): body can be an object/array OR a JSON string\nText (text/*, application/x-www-form-urlencoded, XML): body must be a string\nOther content types (binary): body must be a base64 string representing raw bytes\n\nBase64 format: standard RFC 4648 (+//), not base64url.\nInclude padding (=) when in doubt.\nDo not include data:...;base64, prefixes.\n\n\n\n\noptional 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.\noptional idempotency_key: reuse request id on retries\n\nNotes on forwarded headers:\n\nThe broker injects upstream Authorization using the linked account; any caller-provided authorization header is ignored.\nThe broker forwards only a small allowlist of headers; unknown headers are silently dropped.\n\nBroker-only rendering hints (not forwarded upstream):\n\nheaders[\"x-pb-timezone\"]: IANA timezone name to render human-friendly times in approvals (e.g. America/Los_Angeles).\n\nThe user is prompted to approve in Telegram.\nThe approval prompt includes:\n\nAPI key label (trusted identity)\ninterpreted summary when recognized (best-effort)\nraw URL details\n\nPoll for status / retrieve result\n\nPoll GET /v1/proxy/requests/:id until the request is APPROVED.\nCall POST /v1/proxy/requests/:id/execute to execute and retrieve the upstream response bytes.\nIf you receive the upstream response, parse and persist what you need immediately.\nDo not assume you can execute the same request again.\n\nImportant:\n\nBoth 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."
      },
      {
        "title": "Sample Code (Create + Await)",
        "body": "Use these snippets to create a broker request, poll status, then execute to retrieve upstream bytes.\n\nJavaScript/TypeScript (Bun/Node)\n\ntype CreateRequestResponse = {\n  request_id: string;\n  status: string;\n  approval_expires_at: string;\n};\n\ntype StatusResponse = {\n  request_id: string;\n  status: string;\n  approval_expires_at?: string;\n  error?: string;\n  error_code?: string | null;\n  error_message?: string | null;\n  upstream_http_status?: number | null;\n  upstream_content_type?: string | null;\n  upstream_bytes?: number | null;\n};\n\nasync function createBrokerRequest(params: {\n  baseUrl: string;\n  apiKey: string;\n  upstreamUrl: string;\n  method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n  headers?: Record<string, string>;\n  body?: unknown;\n  consentHint?: string;\n  idempotencyKey?: string;\n}): Promise<CreateRequestResponse> {\n  const res = await fetch(`${params.baseUrl}/v1/proxy/request`, {\n    method: \"POST\",\n    headers: {\n      authorization: `Bearer ${params.apiKey}`,\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      upstream_url: params.upstreamUrl,\n      method: params.method ?? \"GET\",\n      headers: params.headers,\n      body: params.body,\n      consent_hint: params.consentHint,\n      idempotency_key: params.idempotencyKey,\n    }),\n  });\n\n  if (!res.ok) {\n    throw new Error(`broker create failed: ${res.status} ${await res.text()}`);\n  }\n\n  return (await res.json()) as CreateRequestResponse;\n}\n\nasync function pollBrokerStatus(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n  timeoutMs?: number;\n}): Promise<StatusResponse> {\n  // Recommended default: wait at least 30s before returning a request_id to the user.\n  const deadline = Date.now() + (params.timeoutMs ?? 30_000);\n\n  while (Date.now() < deadline) {\n    const res = await fetch(\n      `${params.baseUrl}/v1/proxy/requests/${params.requestId}`,\n      {\n        headers: { authorization: `Bearer ${params.apiKey}` },\n      },\n    );\n\n    // Status endpoint always returns JSON for both 202 and 200.\n    const data = (await res.json()) as StatusResponse;\n\n    // APPROVED is returned with HTTP 202, so we must check the JSON.\n    if (data.status === \"APPROVED\") return data;\n\n    if (res.status === 202) {\n      await new Promise((r) => setTimeout(r, 1000));\n      continue;\n    }\n\n    // Terminal or actionable state (status-only JSON).\n    if (!res.ok && res.status !== 403 && res.status !== 408) {\n      throw new Error(`broker status failed: ${res.status} ${JSON.stringify(data)}`);\n    }\n\n    return data;\n  }\n\n  throw new Error(\"timed out waiting for approval\");\n}\n\nasync function awaitApprovalThenExecute(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n  timeoutMs?: number;\n}): Promise<Response> {\n  const status = await pollBrokerStatus({\n    baseUrl: params.baseUrl,\n    apiKey: params.apiKey,\n    requestId: params.requestId,\n    timeoutMs: params.timeoutMs,\n  });\n\n  if (status.status !== \"APPROVED\") {\n    throw new Error(`request not approved yet (status=${status.status})`);\n  }\n\n  return executeBrokerRequest({\n    baseUrl: params.baseUrl,\n    apiKey: params.apiKey,\n    requestId: params.requestId,\n  });\n}\n\nasync function getBrokerStatusOnce(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n}): Promise<StatusResponse> {\n  const res = await fetch(`${params.baseUrl}/v1/proxy/requests/${params.requestId}`, {\n    headers: { authorization: `Bearer ${params.apiKey}` },\n  });\n\n  // Always JSON (even for 202).\n  return (await res.json()) as StatusResponse;\n}\n\nasync function executeBrokerRequest(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n}): Promise<Response> {\n  const res = await fetch(\n    `${params.baseUrl}/v1/proxy/requests/${params.requestId}/execute`,\n    {\n      method: \"POST\",\n      headers: { authorization: `Bearer ${params.apiKey}` },\n    },\n  );\n\n  // Terminal: upstream bytes (2xx/4xx/5xx) or broker error JSON (403/408/409/410/etc).\n  // IMPORTANT:\n  // - execution is one-time; subsequent calls return 410.\n  // - the broker mirrors upstream HTTP status and content-type, and adds X-Proxy-Request-Id.\n  // - upstream non-2xx is still returned to the caller as bytes, but the broker will persist status=FAILED.\n  return res;\n}\n\n// Suggested control flow:\n// - Start polling for ~30 seconds.\n// - If still pending, return a user-facing message with request_id and what to approve.\n// - On the next user message, poll again (or recreate if expired/consumed).\n\n// Example usage\n// const baseUrl = \"https://permissions-broker.steer.fun\"\n// const apiKey = process.env.PB_API_KEY!\n// const upstreamUrl = \"https://www.googleapis.com/drive/v3/files?pageSize=5&fields=files(id,name)\"\n// const created = await createBrokerRequest({ baseUrl, apiKey, upstreamUrl, consentHint: \"List a few Drive files.\" })\n// Tell user: approve request in Telegram\n// const execRes = await awaitApprovalThenExecute({ baseUrl, apiKey, requestId: created.request_id, timeoutMs: 30_000 })\n// const bodyText = await execRes.text()\n\n// GitHub example (create PR)\n// const created = await createBrokerRequest({\n//   baseUrl,\n//   apiKey,\n//   upstreamUrl: \"https://api.github.com/repos/OWNER/REPO/pulls\",\n//   method: \"POST\",\n//   headers: { \"content-type\": \"application/json\" },\n//   body: {\n//     title: \"My PR\",\n//     head: \"feature-branch\",\n//     base: \"main\",\n//     body: \"Opened via Permissions Broker\",\n//   },\n//   consentHint: \"Open a PR for feature-branch\"\n// })"
      },
      {
        "title": "Supported Providers (Today)",
        "body": "The broker enforces an allowlist and chooses which linked account (OAuth token)\nto use based on the upstream hostname.\n\nCurrently supported:\n\nGoogle\n\nHosts: docs.googleapis.com, www.googleapis.com, sheets.googleapis.com\nTypical uses: Drive listing/search, Docs reads, Sheets range reads\n\n\nGitHub\n\nHost: api.github.com\nTypical uses: PRs/issues/comments/labels and other GitHub actions\n\n\niCloud (CalDAV)\n\nHosts: discovered on connect (starts at caldav.icloud.com)\nTypical uses: Calendar events (VEVENT) and Reminders/tasks (VTODO)\n\n\nSpotify\n\nHost: api.spotify.com\nTypical uses: read profile, list playlists/tracks, control playback\n\nIf you need a provider that isn't supported yet:\n\nStill use the broker pattern in your plan (propose the upstream call + consent text).\nThen tell the user which host(s) need to be enabled/implemented.\n\nFor iCloud CalDAV request templates, see skills/permissions-broker/references/caldav.md."
      },
      {
        "title": "Git Operations (Smart HTTP Proxy)",
        "body": "The broker can also proxy Git operations (clone/fetch/pull/push) via Git Smart HTTP.\n\nThis is separate from /v1/proxy.\n\nHigh-level flow:\n\nCreate a git session (POST /v1/git/sessions).\nThe user approves/denies the session in Telegram.\nPoll session status (GET /v1/git/sessions/:id) until approved.\nFetch a session-scoped remote URL (GET /v1/git/sessions/:id/remote).\nRun git clone / git push against that remote URL.\n\nImportant behavior:\n\nClone/fetch sessions may require multiple git-upload-pack POSTs during a single clone.\nPush sessions are single-use and may become unusable after the first git-receive-pack.\nPush protections are enforced by the broker:\n\ntag pushes are rejected\nref deletes are rejected\ndefault-branch pushes may be blocked unless explicitly allowed in the approval"
      },
      {
        "title": "Endpoints",
        "body": "Auth for all git session endpoints:\n\nAuthorization: Bearer <USER_API_KEY>\n\nCreate session\n\nPOST /v1/git/sessions\nJSON body:\n\noperation: \"clone\", \"fetch\", \"pull\", or \"push\"\nrepo: \"owner/repo\" (GitHub)\noptional consent_hint: requester note shown to the user in Telegram. Always include the reason for the session (what you're doing and why).\n\n\nResponse: { \"session_id\": \"...\", \"status\": \"PENDING_APPROVAL\", \"approval_expires_at\": \"...\" }\n\nPoll status\n\nGET /v1/git/sessions/:id (status JSON)\n\nGet remote URL\n\nGET /v1/git/sessions/:id/remote\nResponse: { \"remote_url\": \"https://...\" }"
      },
      {
        "title": "Example: Clone",
        "body": "Create session:\n\n{\n  \"operation\": \"clone\",\n  \"repo\": \"OWNER/REPO\",\n  \"consent_hint\": \"Clone repo to inspect code\"\n}"
      },
      {
        "title": "Example: Fetch",
        "body": "Use fetch when you already have a repo locally and just need to update refs.\n\nCreate session:\n\n{\n  \"operation\": \"fetch\",\n  \"repo\": \"OWNER/REPO\",\n  \"consent_hint\": \"Fetch latest refs to update local checkout\"\n}\n\nPoll until approved.\n\n\nGet remote_url, then:\n\ngit fetch \"<remote_url>\" --prune"
      },
      {
        "title": "Example: Pull",
        "body": "git pull is a fetch plus a local merge/rebase. The broker only proxies the network portion.\n\ngit pull \"<remote_url>\" main\n\nPoll until status == \"APPROVED\".\n\n\nGet remote_url, then:\n\ngit clone \"<remote_url>\" ./repo"
      },
      {
        "title": "Example: Push New Branch (Recommended)",
        "body": "Create session:\n\n{\n  \"operation\": \"push\",\n  \"repo\": \"OWNER/REPO\",\n  \"consent_hint\": \"Push branch feature-x for a PR\"\n}\n\nPoll until approved.\n\n\nGet remote_url, add as a remote, then push to a non-default branch:\n\ngit remote add broker \"<remote_url>\"\ngit push broker \"HEAD:refs/heads/feature-x\"\n\nNotes:\n\nPrefer creating a new branch name (e.g. pb/<task>/<timestamp>) rather than pushing to main.\nIf the broker session becomes USED, create a new push session.\n\nPython (requests)\n\nimport time\nimport requests\n\ndef create_request(base_url, api_key, upstream_url, consent_hint=None, idempotency_key=None):\n  # Optional: method/headers/body for non-GET requests.\n  r = requests.post(\n    f\"{base_url}/v1/proxy/request\",\n    headers={\"Authorization\": f\"Bearer {api_key}\"},\n    json={\n      \"upstream_url\": upstream_url,\n      # \"method\": \"POST\",\n      # \"headers\": {\"accept\": \"application/vnd.github+json\"},\n      # \"headers\": {\"content-type\": \"application/json\"},\n      # \"body\": {\"title\": \"...\", \"head\": \"...\", \"base\": \"main\"},\n      \"consent_hint\": consent_hint,\n      \"idempotency_key\": idempotency_key,\n    },\n    timeout=30,\n  )\n  r.raise_for_status()\n  return r.json()\n\ndef await_result(base_url, api_key, request_id, timeout_s=120):\n  deadline = time.time() + timeout_s\n  while time.time() < deadline:\n    r = requests.get(\n      f\"{base_url}/v1/proxy/requests/{request_id}\",\n      headers={\"Authorization\": f\"Bearer {api_key}\"},\n      timeout=30,\n    )\n    if r.status_code == 202:\n      time.sleep(1)\n      continue\n\n    # Terminal response (status-only JSON).\n    return r.json()\n\n  raise TimeoutError(\"timed out waiting for approval\")\n\ndef execute_request(base_url, api_key, request_id):\n  # IMPORTANT: execution is one-time; read and store now.\n  return requests.post(\n    f\"{base_url}/v1/proxy/requests/{request_id}/execute\",\n    headers={\"Authorization\": f\"Bearer {api_key}\"},\n    timeout=60,\n  )\n\ndef await_approval_then_execute(base_url, api_key, request_id, timeout_s=30):\n  status = await_result(base_url, api_key, request_id, timeout_s=timeout_s)\n  if status.get(\"status\") != \"APPROVED\":\n    raise RuntimeError(f\"request not approved yet (status={status.get('status')})\")\n  return execute_request(base_url, api_key, request_id)"
      },
      {
        "title": "Constraints You Must Respect",
        "body": "Upstream scheme: HTTPS only.\nUpstream host allowlist: provider-defined (the request must target a supported host).\nUpstream methods: GET/POST/PUT/PATCH/DELETE.\nUpstream response size cap: 1 MiB.\nUpstream request body cap: 256 KiB.\nOne-time execution: after executing a request, you cannot execute it again."
      },
      {
        "title": "Sheets Note (Without Drama)",
        "body": "The broker supports the Google Sheets API host (sheets.googleapis.com).\n\nPreferred approach for reading spreadsheet data:\n\nUse Drive search/list to find the spreadsheet file.\nUse Sheets values read to fetch only the range you need.\n\nFallback:\n\nUse Drive export to fetch contents as CSV when that is sufficient.\n\nNote: large exports can exceed the broker's 1 MiB upstream response cap.\nIf an export fails due to size, narrow the scope (smaller range, fewer tabs, or fewer rows/columns)."
      },
      {
        "title": "Handling Common Terminal States",
        "body": "202: request is still actionable; JSON includes status (often PENDING_APPROVAL, APPROVED, or EXECUTING).\n\nIf status == APPROVED, execute immediately.\nOtherwise keep polling.\n\n\n403: denied by user.\n403: forbidden (wrong API key or request not accessible) is also possible; inspect {error: ...}.\n408: approval expired (user did not decide in time).\n409: already executing; retry shortly.\n410: already executed; recreate the request if you still need it."
      },
      {
        "title": "How To Build Upstream URLs (Google example)",
        "body": "Prefer narrow reads so approvals are understandable and responses are small.\n\nDrive search/list files: https://www.googleapis.com/drive/v3/files?...\n\nUse q, pageSize, and fields to minimize payload.\n\n\nDrive export file contents: https://www.googleapis.com/drive/v3/files/{fileId}/export?mimeType=...\n\nUseful for Google Docs/Sheets export to text/plain or text/csv.\n\n\nDocs structured doc read: https://docs.googleapis.com/v1/documents/{documentId}?fields=...\n\nSee references/api_reference.md for endpoint details and a Google URL cheat sheet."
      },
      {
        "title": "How To Build Upstream URLs (GitHub examples)",
        "body": "Create PR: POST https://api.github.com/repos/<owner>/<repo>/pulls\n\nJSON body: { \"title\": \"...\", \"head\": \"branch\", \"base\": \"main\", \"body\": \"...\" }\n\n\nCreate issue: POST https://api.github.com/repos/<owner>/<repo>/issues\n\nJSON body: { \"title\": \"...\", \"body\": \"...\" }"
      },
      {
        "title": "Data Handling Rules",
        "body": "Treat the user's API key as secret."
      },
      {
        "title": "Resources",
        "body": "Reference: references/api_reference.md"
      }
    ],
    "body": "Permissions Broker\nSetup (Do This First)\n\nBefore 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).\n\nIf you do NOT have an API key available:\n\nAsk the user to create one in Telegram by messaging the bot:\n/key <name>\n\nTell the user to send the resulting key back to you (the key is shown \"sent once\").\nAsk whether they want you to store/reuse it across sessions.\nIf yes: store it in your secrets store for future sessions (recommended name: PB_API_KEY).\nIf no/unclear: treat it as session-only and ask for it again next time.\nRemember only the storage location (e.g. \"Permissions Broker key is stored in PB_API_KEY\"), never the secret value.\n\nImportant:\n\nNever commit the key to the repo.\nNever include the key in code, logs, or error output.\nDo not persist/reuse the key across sessions unless the user explicitly asks you to.\nIf the key is lost/compromised, instruct the user to rotate it via the bot's key management UI.\n\nProvider connections:\n\nThe user links providers in Telegram using /connect.\nFor iCloud: /connect icloud returns a browser link to a broker-hosted form where the user enters an Apple ID app-specific password.\nOverview\n\nUse the broker as a user-controlled proxy for external data access and API actions.\n\nThe mental model:\n\nYou propose a specific upstream request (URL + method + body)\nThe user approves/denies it in Telegram\nYou poll for approval\nYou execute it exactly once and immediately parse/persist what you need\n\nThis skill is intentionally provider-agnostic. Provider support grows over time.\n\nAgent Response Style (Important)\n\nWhen 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\".\n\nInstead:\n\nTreat the broker as the standard mechanism for access.\nAsk for the minimum missing inputs (broker base URL + user API key) and then propose the exact upstream URL(s) you will request.\nDefault behavior: create the request, poll for approval, and execute in one go.\nOnly if polling times out: tell the user what to approve in Telegram and what you'll return after approval.\n\nAvoid:\n\nLong meta explanations about the repo setup.\nRe-stating the full allowlist/constraints unless it directly affects the requested task.\n\nPreferred framing:\n\n\"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.\"\nPolling Behavior (Important)\n\nAfter 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.\n\nGuidelines:\n\nDefault to 30 seconds of polling (or longer if the user explicitly asks you to wait).\nIf approval happens within that window, call the execute endpoint immediately and return the upstream result in the same response.\nIf approval has not happened within that window:\nReturn the request_id.\nTell the user to approve/deny the request in Telegram.\nState exactly what you will do once it's approved (execute once and return the result).\nContinue polling on the next user message.\nCore Workflow\nCollect inputs\nUser API key (never paste into logs; never store in repo)\nDecide how to access the provider\nIf the agent already has explicit, local credentials for the provider and the user explicitly wants you to use them, you may.\nOtherwise (default), use the broker.\nIf you're unsure whether you're allowed to use local creds, default to broker.\nCreate a proxy request\nCall POST /v1/proxy/request with:\nupstream_url: the full external service API URL you want to call\nmethod: GET (default) or POST/PUT/PATCH/DELETE\nheaders (optional): request headers to forward (never include authorization)\nbody (optional): request body\nthe broker stores request body bytes and interprets them based on headers.content-type\nJSON (application/json or +json): body can be an object/array OR a JSON string\nText (text/*, application/x-www-form-urlencoded, XML): body must be a string\nOther content types (binary): body must be a base64 string representing raw bytes\nBase64 format: standard RFC 4648 (+//), not base64url.\nInclude padding (=) when in doubt.\nDo not include data:...;base64, prefixes.\noptional 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.\noptional idempotency_key: reuse request id on retries\n\nNotes on forwarded headers:\n\nThe broker injects upstream Authorization using the linked account; any caller-provided authorization header is ignored.\nThe broker forwards only a small allowlist of headers; unknown headers are silently dropped.\n\nBroker-only rendering hints (not forwarded upstream):\n\nheaders[\"x-pb-timezone\"]: IANA timezone name to render human-friendly times in approvals (e.g. America/Los_Angeles).\nThe user is prompted to approve in Telegram. The approval prompt includes:\nAPI key label (trusted identity)\ninterpreted summary when recognized (best-effort)\nraw URL details\nPoll for status / retrieve result\nPoll GET /v1/proxy/requests/:id until the request is APPROVED.\nCall POST /v1/proxy/requests/:id/execute to execute and retrieve the upstream response bytes.\nIf you receive the upstream response, parse and persist what you need immediately.\nDo not assume you can execute the same request again.\n\nImportant:\n\nBoth 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.\nSample Code (Create + Await)\n\nUse these snippets to create a broker request, poll status, then execute to retrieve upstream bytes.\n\nJavaScript/TypeScript (Bun/Node)\n\ntype CreateRequestResponse = {\n  request_id: string;\n  status: string;\n  approval_expires_at: string;\n};\n\ntype StatusResponse = {\n  request_id: string;\n  status: string;\n  approval_expires_at?: string;\n  error?: string;\n  error_code?: string | null;\n  error_message?: string | null;\n  upstream_http_status?: number | null;\n  upstream_content_type?: string | null;\n  upstream_bytes?: number | null;\n};\n\nasync function createBrokerRequest(params: {\n  baseUrl: string;\n  apiKey: string;\n  upstreamUrl: string;\n  method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n  headers?: Record<string, string>;\n  body?: unknown;\n  consentHint?: string;\n  idempotencyKey?: string;\n}): Promise<CreateRequestResponse> {\n  const res = await fetch(`${params.baseUrl}/v1/proxy/request`, {\n    method: \"POST\",\n    headers: {\n      authorization: `Bearer ${params.apiKey}`,\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      upstream_url: params.upstreamUrl,\n      method: params.method ?? \"GET\",\n      headers: params.headers,\n      body: params.body,\n      consent_hint: params.consentHint,\n      idempotency_key: params.idempotencyKey,\n    }),\n  });\n\n  if (!res.ok) {\n    throw new Error(`broker create failed: ${res.status} ${await res.text()}`);\n  }\n\n  return (await res.json()) as CreateRequestResponse;\n}\n\nasync function pollBrokerStatus(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n  timeoutMs?: number;\n}): Promise<StatusResponse> {\n  // Recommended default: wait at least 30s before returning a request_id to the user.\n  const deadline = Date.now() + (params.timeoutMs ?? 30_000);\n\n  while (Date.now() < deadline) {\n    const res = await fetch(\n      `${params.baseUrl}/v1/proxy/requests/${params.requestId}`,\n      {\n        headers: { authorization: `Bearer ${params.apiKey}` },\n      },\n    );\n\n    // Status endpoint always returns JSON for both 202 and 200.\n    const data = (await res.json()) as StatusResponse;\n\n    // APPROVED is returned with HTTP 202, so we must check the JSON.\n    if (data.status === \"APPROVED\") return data;\n\n    if (res.status === 202) {\n      await new Promise((r) => setTimeout(r, 1000));\n      continue;\n    }\n\n    // Terminal or actionable state (status-only JSON).\n    if (!res.ok && res.status !== 403 && res.status !== 408) {\n      throw new Error(`broker status failed: ${res.status} ${JSON.stringify(data)}`);\n    }\n\n    return data;\n  }\n\n  throw new Error(\"timed out waiting for approval\");\n}\n\nasync function awaitApprovalThenExecute(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n  timeoutMs?: number;\n}): Promise<Response> {\n  const status = await pollBrokerStatus({\n    baseUrl: params.baseUrl,\n    apiKey: params.apiKey,\n    requestId: params.requestId,\n    timeoutMs: params.timeoutMs,\n  });\n\n  if (status.status !== \"APPROVED\") {\n    throw new Error(`request not approved yet (status=${status.status})`);\n  }\n\n  return executeBrokerRequest({\n    baseUrl: params.baseUrl,\n    apiKey: params.apiKey,\n    requestId: params.requestId,\n  });\n}\n\nasync function getBrokerStatusOnce(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n}): Promise<StatusResponse> {\n  const res = await fetch(`${params.baseUrl}/v1/proxy/requests/${params.requestId}`, {\n    headers: { authorization: `Bearer ${params.apiKey}` },\n  });\n\n  // Always JSON (even for 202).\n  return (await res.json()) as StatusResponse;\n}\n\nasync function executeBrokerRequest(params: {\n  baseUrl: string;\n  apiKey: string;\n  requestId: string;\n}): Promise<Response> {\n  const res = await fetch(\n    `${params.baseUrl}/v1/proxy/requests/${params.requestId}/execute`,\n    {\n      method: \"POST\",\n      headers: { authorization: `Bearer ${params.apiKey}` },\n    },\n  );\n\n  // Terminal: upstream bytes (2xx/4xx/5xx) or broker error JSON (403/408/409/410/etc).\n  // IMPORTANT:\n  // - execution is one-time; subsequent calls return 410.\n  // - the broker mirrors upstream HTTP status and content-type, and adds X-Proxy-Request-Id.\n  // - upstream non-2xx is still returned to the caller as bytes, but the broker will persist status=FAILED.\n  return res;\n}\n\n// Suggested control flow:\n// - Start polling for ~30 seconds.\n// - If still pending, return a user-facing message with request_id and what to approve.\n// - On the next user message, poll again (or recreate if expired/consumed).\n\n// Example usage\n// const baseUrl = \"https://permissions-broker.steer.fun\"\n// const apiKey = process.env.PB_API_KEY!\n// const upstreamUrl = \"https://www.googleapis.com/drive/v3/files?pageSize=5&fields=files(id,name)\"\n// const created = await createBrokerRequest({ baseUrl, apiKey, upstreamUrl, consentHint: \"List a few Drive files.\" })\n// Tell user: approve request in Telegram\n// const execRes = await awaitApprovalThenExecute({ baseUrl, apiKey, requestId: created.request_id, timeoutMs: 30_000 })\n// const bodyText = await execRes.text()\n\n// GitHub example (create PR)\n// const created = await createBrokerRequest({\n//   baseUrl,\n//   apiKey,\n//   upstreamUrl: \"https://api.github.com/repos/OWNER/REPO/pulls\",\n//   method: \"POST\",\n//   headers: { \"content-type\": \"application/json\" },\n//   body: {\n//     title: \"My PR\",\n//     head: \"feature-branch\",\n//     base: \"main\",\n//     body: \"Opened via Permissions Broker\",\n//   },\n//   consentHint: \"Open a PR for feature-branch\"\n// })\n\nSupported Providers (Today)\n\nThe broker enforces an allowlist and chooses which linked account (OAuth token) to use based on the upstream hostname.\n\nCurrently supported:\n\nGoogle\nHosts: docs.googleapis.com, www.googleapis.com, sheets.googleapis.com\nTypical uses: Drive listing/search, Docs reads, Sheets range reads\nGitHub\nHost: api.github.com\nTypical uses: PRs/issues/comments/labels and other GitHub actions\niCloud (CalDAV)\nHosts: discovered on connect (starts at caldav.icloud.com)\nTypical uses: Calendar events (VEVENT) and Reminders/tasks (VTODO)\nSpotify\nHost: api.spotify.com\nTypical uses: read profile, list playlists/tracks, control playback\n\nIf you need a provider that isn't supported yet:\n\nStill use the broker pattern in your plan (propose the upstream call + consent text).\nThen tell the user which host(s) need to be enabled/implemented.\n\nFor iCloud CalDAV request templates, see skills/permissions-broker/references/caldav.md.\n\nGit Operations (Smart HTTP Proxy)\n\nThe broker can also proxy Git operations (clone/fetch/pull/push) via Git Smart HTTP.\n\nThis is separate from /v1/proxy.\n\nHigh-level flow:\n\nCreate a git session (POST /v1/git/sessions).\nThe user approves/denies the session in Telegram.\nPoll session status (GET /v1/git/sessions/:id) until approved.\nFetch a session-scoped remote URL (GET /v1/git/sessions/:id/remote).\nRun git clone / git push against that remote URL.\n\nImportant behavior:\n\nClone/fetch sessions may require multiple git-upload-pack POSTs during a single clone.\nPush sessions are single-use and may become unusable after the first git-receive-pack.\nPush protections are enforced by the broker:\ntag pushes are rejected\nref deletes are rejected\ndefault-branch pushes may be blocked unless explicitly allowed in the approval\nEndpoints\n\nAuth for all git session endpoints:\n\nAuthorization: Bearer <USER_API_KEY>\n\nCreate session\n\nPOST /v1/git/sessions\nJSON body:\noperation: \"clone\", \"fetch\", \"pull\", or \"push\"\nrepo: \"owner/repo\" (GitHub)\noptional consent_hint: requester note shown to the user in Telegram. Always include the reason for the session (what you're doing and why).\nResponse: { \"session_id\": \"...\", \"status\": \"PENDING_APPROVAL\", \"approval_expires_at\": \"...\" }\n\nPoll status\n\nGET /v1/git/sessions/:id (status JSON)\n\nGet remote URL\n\nGET /v1/git/sessions/:id/remote\nResponse: { \"remote_url\": \"https://...\" }\nExample: Clone\nCreate session:\n{\n  \"operation\": \"clone\",\n  \"repo\": \"OWNER/REPO\",\n  \"consent_hint\": \"Clone repo to inspect code\"\n}\n\nExample: Fetch\n\nUse fetch when you already have a repo locally and just need to update refs.\n\nCreate session:\n{\n  \"operation\": \"fetch\",\n  \"repo\": \"OWNER/REPO\",\n  \"consent_hint\": \"Fetch latest refs to update local checkout\"\n}\n\n\nPoll until approved.\n\nGet remote_url, then:\n\ngit fetch \"<remote_url>\" --prune\n\nExample: Pull\n\ngit pull is a fetch plus a local merge/rebase. The broker only proxies the network portion.\n\ngit pull \"<remote_url>\" main\n\n\nPoll until status == \"APPROVED\".\n\nGet remote_url, then:\n\ngit clone \"<remote_url>\" ./repo\n\nExample: Push New Branch (Recommended)\nCreate session:\n{\n  \"operation\": \"push\",\n  \"repo\": \"OWNER/REPO\",\n  \"consent_hint\": \"Push branch feature-x for a PR\"\n}\n\n\nPoll until approved.\n\nGet remote_url, add as a remote, then push to a non-default branch:\n\ngit remote add broker \"<remote_url>\"\ngit push broker \"HEAD:refs/heads/feature-x\"\n\n\nNotes:\n\nPrefer creating a new branch name (e.g. pb/<task>/<timestamp>) rather than pushing to main.\nIf the broker session becomes USED, create a new push session.\n\nPython (requests)\n\nimport time\nimport requests\n\ndef create_request(base_url, api_key, upstream_url, consent_hint=None, idempotency_key=None):\n  # Optional: method/headers/body for non-GET requests.\n  r = requests.post(\n    f\"{base_url}/v1/proxy/request\",\n    headers={\"Authorization\": f\"Bearer {api_key}\"},\n    json={\n      \"upstream_url\": upstream_url,\n      # \"method\": \"POST\",\n      # \"headers\": {\"accept\": \"application/vnd.github+json\"},\n      # \"headers\": {\"content-type\": \"application/json\"},\n      # \"body\": {\"title\": \"...\", \"head\": \"...\", \"base\": \"main\"},\n      \"consent_hint\": consent_hint,\n      \"idempotency_key\": idempotency_key,\n    },\n    timeout=30,\n  )\n  r.raise_for_status()\n  return r.json()\n\ndef await_result(base_url, api_key, request_id, timeout_s=120):\n  deadline = time.time() + timeout_s\n  while time.time() < deadline:\n    r = requests.get(\n      f\"{base_url}/v1/proxy/requests/{request_id}\",\n      headers={\"Authorization\": f\"Bearer {api_key}\"},\n      timeout=30,\n    )\n    if r.status_code == 202:\n      time.sleep(1)\n      continue\n\n    # Terminal response (status-only JSON).\n    return r.json()\n\n  raise TimeoutError(\"timed out waiting for approval\")\n\ndef execute_request(base_url, api_key, request_id):\n  # IMPORTANT: execution is one-time; read and store now.\n  return requests.post(\n    f\"{base_url}/v1/proxy/requests/{request_id}/execute\",\n    headers={\"Authorization\": f\"Bearer {api_key}\"},\n    timeout=60,\n  )\n\ndef await_approval_then_execute(base_url, api_key, request_id, timeout_s=30):\n  status = await_result(base_url, api_key, request_id, timeout_s=timeout_s)\n  if status.get(\"status\") != \"APPROVED\":\n    raise RuntimeError(f\"request not approved yet (status={status.get('status')})\")\n  return execute_request(base_url, api_key, request_id)\n\nConstraints You Must Respect\nUpstream scheme: HTTPS only.\nUpstream host allowlist: provider-defined (the request must target a supported host).\nUpstream methods: GET/POST/PUT/PATCH/DELETE.\nUpstream response size cap: 1 MiB.\nUpstream request body cap: 256 KiB.\nOne-time execution: after executing a request, you cannot execute it again.\nSheets Note (Without Drama)\n\nThe broker supports the Google Sheets API host (sheets.googleapis.com).\n\nPreferred approach for reading spreadsheet data:\n\nUse Drive search/list to find the spreadsheet file.\nUse Sheets values read to fetch only the range you need.\n\nFallback:\n\nUse Drive export to fetch contents as CSV when that is sufficient.\n\nNote: 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).\n\nHandling Common Terminal States\n202: request is still actionable; JSON includes status (often PENDING_APPROVAL, APPROVED, or EXECUTING).\nIf status == APPROVED, execute immediately.\nOtherwise keep polling.\n403: denied by user.\n403: forbidden (wrong API key or request not accessible) is also possible; inspect {error: ...}.\n408: approval expired (user did not decide in time).\n409: already executing; retry shortly.\n410: already executed; recreate the request if you still need it.\nHow To Build Upstream URLs (Google example)\n\nPrefer narrow reads so approvals are understandable and responses are small.\n\nDrive search/list files: https://www.googleapis.com/drive/v3/files?...\nUse q, pageSize, and fields to minimize payload.\nDrive export file contents: https://www.googleapis.com/drive/v3/files/{fileId}/export?mimeType=...\nUseful for Google Docs/Sheets export to text/plain or text/csv.\nDocs structured doc read: https://docs.googleapis.com/v1/documents/{documentId}?fields=...\n\nSee references/api_reference.md for endpoint details and a Google URL cheat sheet.\n\nHow To Build Upstream URLs (GitHub examples)\nCreate PR: POST https://api.github.com/repos/<owner>/<repo>/pulls\nJSON body: { \"title\": \"...\", \"head\": \"branch\", \"base\": \"main\", \"body\": \"...\" }\nCreate issue: POST https://api.github.com/repos/<owner>/<repo>/issues\nJSON body: { \"title\": \"...\", \"body\": \"...\" }\nData Handling Rules\nTreat the user's API key as secret.\nResources\nReference: references/api_reference.md"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/stephancill/permissions-broker",
    "publisherUrl": "https://clawhub.ai/stephancill/permissions-broker",
    "owner": "stephancill",
    "version": "1.0.9",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/permissions-broker",
    "downloadUrl": "https://openagent3.xyz/downloads/permissions-broker",
    "agentUrl": "https://openagent3.xyz/skills/permissions-broker/agent",
    "manifestUrl": "https://openagent3.xyz/skills/permissions-broker/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/permissions-broker/agent.md"
  }
}