# Tencent Cloud TAT — Remote Command Execution

Execute commands on Tencent Cloud instances (Lighthouse / CVM) via the TAT (Tencent Automation Tools) API. This is how the agent runs commands on a remote server **without** the user needing to SSH in or paste commands.

## When to use

- You need to run commands on a remote Tencent instance (different server than where Hermes runs)
- The user gave you `SecretId` + `SecretKey` and an instance ID
- The user said "你自己部署" / "你自己直接操作" — they expect you to handle it, not give them commands to paste
- Two servers can't communicate directly (network isolation, firewall blocks)

## Prerequisites

- **SecretId** + **SecretKey**: User gets these from 腾讯云控制台 → 访问管理 → API密钥管理
- **Instance ID**: Lighthouse instances are `lhins-xxxxxxxx`, CVM instances are `ins-xxxxxxxx`
- **Region**: e.g., `ap-beijing`, `ap-shanghai`
- Instance must have TAT agent installed and running (default on Lighthouse)
- `api.github.com` reachable from the source server (for cross-server file transfer)

## API Overview

| Action | Purpose |
|--------|---------|
| `RunCommand` | Execute a shell command on remote instance |
| `DescribeInvocationTasks` | Poll for command result |

Endpoint: `tat.tencentcloudapi.com`  
Version: `2020-10-28`  
Auth: TC3-HMAC-SHA256 signing

## Step 1: TC3-HMAC-SHA256 Signature Helper

```python
import hashlib, hmac, json, datetime, requests, base64

SECRET_ID = "AKIDxxxxxxxx"
SECRET_KEY = "xxxxxxxx"
REGION = "ap-beijing"
SERVICE = "tat"
HOST = "tat.tencentcloudapi.com"
VERSION = "2020-10-28"

def tencent_headers(action, payload_dict, secret_id, secret_key, region, service=SERVICE, host=HOST, version=VERSION):
    """Build signed headers for Tencent Cloud API v3."""
    payload = json.dumps(payload_dict)
    timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
    date = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc).strftime("%Y-%m-%d")

    canonical_headers = f"content-type:application/json; charset=utf-8\nhost:{host}\n"
    signed_headers = "content-type;host"
    hashed_payload = hashlib.sha256(payload.encode("utf-8")).hexdigest()
    canonical_request = f"POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_payload}"

    algorithm = "TC3-HMAC-SHA256"
    credential_scope = f"{date}/{service}/tc3_request"
    hashed_canonical_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
    string_to_sign = f"{algorithm}\n{timestamp}\n{credential_scope}\n{hashed_canonical_request}"

    def sign(key, msg):
        return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()

    secret_date = sign(("TC3" + secret_key).encode("utf-8"), date)
    secret_service = sign(secret_date, service)
    secret_signing = sign(secret_service, "tc3_request")
    signature = hmac.new(secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()

    authorization = f"{algorithm} Credential={secret_id}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}"

    return {
        "Authorization": authorization,
        "Content-Type": "application/json; charset=utf-8",
        "Host": host,
        "X-TC-Action": action,
        "X-TC-Timestamp": str(timestamp),
        "X-TC-Version": version,
        "X-TC-Region": region,
    }, payload
```

## Step 2: RunCommand — Execute on Remote

```python
command = "curl -L -o /home/ubuntu/file.tar.gz https://example.com/file.tar.gz && echo DONE"

payload = {
    "InstanceIds": ["lhins-5ertfkpu"],
    "Content": base64.b64encode(command.encode()).decode(),  # MUST be base64
    "CommandType": "SHELL",
    "Timeout": 600  # seconds, adjust for slow downloads
}

headers, body = tencent_headers("RunCommand", payload, SECRET_ID, SECRET_KEY, REGION)
resp = requests.post(f"https://{HOST}", headers=headers, data=body, timeout=30)
result = resp.json()
invocation_id = result["Response"]["InvocationId"]  # Save this for polling
```

## Step 3: Poll for Result

```python
import time

def poll_result(invocation_id, secret_id, secret_key, region, max_wait=300, interval=15):
    """Poll until command completes or times out."""
    elapsed = 0
    while elapsed < max_wait:
        time.sleep(interval)
        elapsed += interval

        payload = {"Filters": [{"Name": "invocation-id", "Values": [invocation_id]}]}
        headers, body = tencent_headers("DescribeInvocationTasks", payload,
                                         secret_id, secret_key, region)
        resp = requests.post(f"https://{HOST}", headers=headers, data=body, timeout=30)
        tasks = resp.json().get("Response", {}).get("InvocationTaskSet", [])

        for t in tasks:
            status = t.get("TaskStatus")
            tr = t.get("TaskResult", {})
            exit_code = tr.get("ExitCode")
            output_b64 = tr.get("Output", "")

            if status in ("SUCCESS", "FAILED", "TIMEOUT"):
                output = base64.b64decode(output_b64).decode("utf-8", errors="replace") if output_b64 else ""
                return status, exit_code, output

            print(f"  Status: {status} ({elapsed}s elapsed)")

    return "TIMEOUT", -1, ""
```

## Pitfalls

### Timeout
- Default timeout is 60s. For file downloads from GitHub (3MB = ~150s at 22KB/s), set `Timeout: 600`.
- The first download attempt may fail with `TIMEOUT` — just re-run with a longer timeout.

### Command encoding
- `Content` must be **Base64-encoded**. Plain text returns `InvalidParameterValue`.
- Don't use `&& echo EXTRACT_OK && ls ...` inside backtick-heavy commands — TAT may misinterpret special chars. Keep commands simple.

### Slow downloads from China
- `raw.githubusercontent.com` download speed from some Chinese regions can be as low as 15-25 KB/s
- ~3MB = 2-3 minutes. Plan timeouts accordingly.
- Alternative: if the source server has a cloudflared tunnel, try that. But Cloudflare Access with bot protection will 403 non-browser clients.

### No output during RUNNING
- `ExitCode` is `0` during execution even before completion — don't trust it until `TaskStatus` is `SUCCESS`.
- curl progress output in `TaskResult.Output` is base64-encoded, not plain text.

### User gave credentials — save them
- After successful use, save the SecretId/SecretKey to memory for future cross-server operations
- But respect that these are sensitive — only save if the user is OK with persistent access

## Cross-Server Deployment Pattern (Full Workflow)

When deploying files from server A (where Hermes runs) to server B (Lighthouse/CVM):

1. **Upload file to GitHub** (from server A):
   - Use `github-api-push` skill pattern
   - Push to `raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}`
   
2. **Download on server B** (via TAT):
   - RunCommand: `curl -L -o /path/to/file https://raw.githubusercontent.com/...`
   - Poll until SUCCESS
   
3. **Extract/setup on server B** (via TAT):
   - RunCommand: `tar -xzf ... && ls ...`
   - Verify file list in output

4. **Clean up**: Optionally delete the temp GitHub repo

## Comparison: Remote Execution Methods

| Method | Requires | Reliability | Latency |
|--------|----------|:-----------:|:-------:|
| **TAT API** | SecretId + SecretKey | ✅ High | ~500ms API + cmd time |
| User runs via SSH | User SSH access | ⚠️ Human-dependent | Minutes+ |
| Direct scp between servers | Network connectivity | ❌ Blocked in isolated envs | N/A |
| Web console copy-paste | User interaction | ❌ Error-prone | Minutes+ |

**Rule**: When the user has a Tencent Cloud account and you need to operate on a remote instance, ask for API credentials and use TAT. Don't give them commands to paste.
