# agent-handoff — Getting Started Guide > End-to-end encrypted chat handoff between AI agents. > Server stores only ciphertext; encryption keys never reach the server. ## TL;DR 1. **A agent**: package conversation → upload → get `handoff_key` 2. **User**: copy `handoff_key` (e.g. `ah-7f3a9b2c.3VLmNI...`) 3. **B agent**: paste key + run install script → fetch + decrypt ## User intent → tool call mapping When a user says any of these natural-language requests, the LLM should: 1. Extract relevant `messages` from its own conversation history 2. Identify any `files` the user referenced 3. Call the appropriate tool | User says | Tool call | |---|---| | "把刚刚对话的 XXX 主题记录打包" | `package_chat_history(messages=[...filtered...], server_url=...)` | | "把前 N 轮对话打包" | `package_chat_history(messages=[...last N...], server_url=...)` | | "把刚才读过的 report.md 也一起打包" | `package_chat_history(messages=[...], files=["/path/to/report.md"], ...)` | | "把 handoff key `ah-xxx.yyy` 拉过来" | `fetch_chat_history(handoff_key="ah-xxx.yyy", server_url=...)` | | "校验一下 key `ah-xxx.yyy` 是不是合法" | `inspect_handoff_key(handoff_key="ah-xxx.yyy")` | The tools do **NOT** read your conversation context — you must construct `messages` yourself from your context. ## One-line install (for new agents) If a user wants to onboard a new agent: ```bash bash -c "$(curl -fsSL https://aishangai.shop/install)" ``` This installs the handoff skill to `~/.handoff/` including: - `SKILL.md` — this guide - `handoff.py` — CLI wrapper - `lib/agent_handoff_mcp/` — Python module - Auto-installs `httpx`, `cryptography` if missing - Writes `~/.handoff/config` with server URL After install, the agent can: ```bash handoff package --messages-json ./msgs.json --hint "topic" handoff fetch --handoff-key ah-xxx.yyy --output-dir ./out handoff inspect --handoff-key ah-xxx.yyy ``` ## Tool reference ### `package_chat_history(messages, server_url, files?, metadata?, hint?, expires_in?)` Pack messages + files, encrypt, upload, return handoff key. **Required**: - `messages`: list of `{role, content, ts?}`. You construct this from your context. - `server_url`: the handoff server URL (e.g. `https://aishangai.shop`) **Optional**: - `files`: list of file paths to include - `metadata`: extra info (e.g. `{"topic": "Q3 review"}`) - `hint`: short note shown in the server (e.g. `"Q3 finance handoff"`) - `expires_in`: TTL in seconds (default 604800 = 7 days, max 30 days) **Returns**: handoff_key string like `ah-7f3a9b2c.3VLmNI...` ### `fetch_chat_history(handoff_key, server_url, output_dir?)` Fetch a bundle by handoff key, decrypt, write to disk. **Required**: - `handoff_key`: the key like `ah-xxx.yyy` - `server_url`: server URL **Optional**: - `output_dir`: where to write (default `./handoff/{bundle_id}-{ts}`) **Returns**: `output_dir`, `metadata.json`, `messages.jsonl`, `files/` paths ### `inspect_handoff_key(handoff_key)` Validate key format (no network call). Just checks structure. ## Calling examples ### Example 1: User says "把刚刚对话的 财务主题记录和文件打包给我 key" ```python # 1. LLM extracts from its own context (semantic understanding) messages = [ {"role": "user", "content": "帮我看一下 Q3 营收数据"}, {"role": "assistant", "content": "Q3 营收 $42M,增长 20%"}, # ... LLM filters to only finance-related turns ] files = ["/Users/me/work/q3-finance.pdf"] # LLM finds this from context # 2. Call the tool result = package_chat_history( messages=messages, server_url="https://aishangai.shop", files=files, metadata={"topic": "Q3 财务"}, hint="Q3 财务相关对话 handoff", ) # 3. Show the key to the user # Output: ah-7f3a9b2c.3VLmNI... ``` ### Example 2: User says "把 handoff key `ah-xxx.yyy` 拉过来" ```python summary = fetch_chat_history( handoff_key="ah-xxx.yyy", server_url="https://aishangai.shop", output_dir="./handoff", ) # Read messages.jsonl and files/ in output_dir ``` ## Security model - **AES-256-GCM** end-to-end encryption - Bundle ID (16 bytes random) and encryption key (32 bytes random) are independent - Server stores ONLY: ciphertext, nonce, size, expires_at, hint - Server CANNOT decrypt without the key - Keys are base64url-encoded in `ah-{bundle_id}.{key}` format - AAD binds ciphertext to bundle_id (prevents cross-bundle replay) ## API endpoints (raw HTTP) If MCP / skill not available, use HTTP directly: ``` POST /api/v1/bundles { "id": "<32-char hex bundle id>", "ciphertext_b64": "...", "nonce_b64": "...", "expires_in": 604800, "hint": "optional" } GET /api/v1/bundles/{id} → 200: {id, ciphertext_b64, nonce_b64, hint, expires_at} → 404: not found → 410: expired or consumed GET /api/v1/health → 200: {status: "ok", storage: "local|tos", version: "..."} ``` ## Tips - Default TTL is 7 days. Plan accordingly for long-running handoffs. - One-time consume mode: set `ONE_TIME_CONSUME=true` server-side, GET once then bundle is gone. - Server has a 50MB default size cap per bundle (`MAX_BUNDLE_SIZE`). - File paths in `files` are read on the A-side and base64-embedded — they don't leak to server. - Reuse the same handoff_key for retries? NO — the key derives the encryption. Lost key = lost data. ## Open source https://github.com/qzpthuhhu/agent-handoff MIT License.