"""智能体侧:解密胶囊并按契约请求平台(前端不持有明文契约)。""" from __future__ import annotations import base64 import json from typing import Any import httpx from cryptography.hazmat.primitives.ciphers.aead import AESGCM PREFIX = "AJZ1" def decrypt_capsule(agent_key_b64: str, capsule: str) -> dict[str, Any]: parts = capsule.split(".") if len(parts) != 3 or parts[0] != PREFIX: raise ValueError("invalid capsule") key = base64.urlsafe_b64decode(pad(agent_key_b64)) nonce = base64.urlsafe_b64decode(pad(parts[1])) data = base64.urlsafe_b64decode(pad(parts[2])) # Go RawURLEncoding has no padding; Python needs pad aes = AESGCM(key) plain = aes.decrypt(nonce, data, None) return json.loads(plain) def pad(s: str) -> str: return s + "=" * (-len(s) % 4) class AgentClient: def __init__(self, access_token: str, agent_key: str, capsule: str): self.access_token = access_token self.desc = decrypt_capsule(agent_key, capsule) self.http = httpx.Client( base_url=self.desc["base_url"], headers={"Authorization": f"Bearer {access_token}"}, timeout=30.0, ) def resources(self) -> list[dict[str, Any]]: return self.desc.get("resources", []) def list(self, resource_name: str, **filters: str) -> dict[str, Any]: res = self._find(resource_name) params = {f"filter.{k}": v for k, v in filters.items()} r = self.http.get(res["path"], params=params) r.raise_for_status() return r.json() def create(self, resource_name: str, body: dict[str, Any]) -> dict[str, Any]: res = self._find(resource_name) r = self.http.post(res["path"], json=body) r.raise_for_status() return r.json() def get(self, resource_name: str, row_id: str) -> dict[str, Any]: res = self._find(resource_name) r = self.http.get(f"{res['path'].rstrip('/')}/{row_id}") r.raise_for_status() return r.json() def update(self, resource_name: str, row_id: str, body: dict[str, Any]) -> dict[str, Any]: res = self._find(resource_name) r = self.http.put(f"{res['path'].rstrip('/')}/{row_id}", json=body) r.raise_for_status() return r.json() def delete(self, resource_name: str, row_id: str) -> None: res = self._find(resource_name) r = self.http.delete(f"{res['path'].rstrip('/')}/{row_id}") if r.status_code not in (200, 204): r.raise_for_status() def _find(self, name: str) -> dict[str, Any]: for r in self.resources(): if r["name"] == name: return r raise KeyError(f"resource not in capsule: {name}") if __name__ == "__main__": import os import sys token = os.environ.get("AJZ_TOKEN", "") key = os.environ.get("AJZ_AGENT_KEY", "") capsule = os.environ.get("AJZ_CAPSULE", "") if not (token and key and capsule): print("Set AJZ_TOKEN / AJZ_AGENT_KEY / AJZ_CAPSULE", file=sys.stderr) sys.exit(1) client = AgentClient(token, key, capsule) print(json.dumps({"app": client.desc["app_slug"], "resources": [r["name"] for r in client.resources()]}, ensure_ascii=False)) if client.resources(): name = client.resources()[0]["name"] print(json.dumps(client.list(name), ensure_ascii=False, indent=2))