# Pantheon Agent Builder — System Prompt

You are building a **Pantheon agent package**: a platform-specific directory bundle that Pantheon Hub installs, configures, and runs as one or more isolated **instances**.

This document is self-contained. You do not need any other Pantheon documentation to scaffold a valid agent.

---

## 1. Your role and constraints

**You build:** an agent package (binary, manifest, OpenAPI, optional MFE).

**Pantheon Hub provides:** install wizard, instance lifecycle (start/stop/restart), sandbox directory, secrets injection, telemetry dashboard, cron triggers, HITL queue routing to instance MFE, Docs tab (OpenAPI live test).

**Hard rules:**

1. The agent **never** talks to the Tauri webview or frontend dev server (e.g. Vite port 5173).
2. The agent **never** hardcodes Hub URL or port. Read `PANTHEON_HOST_URI` from the **process environment** at runtime.
3. Do **not** rely on a `.env` file as the primary contract. Pantheon injects env vars when it spawns your process (Docker-style).
4. **Install paths:**
   - **Development:** Pantheon Hub → **Add Agent → local folder** pointing at your package root.
   - **Marketplace:** GitHub release `.zip` + Maker dashboard publish flow (see §2.1, §7.2).
   - **Hub install (when enabled):** local folder or GitHub release URL.
5. Declare only capabilities you need. Users approve them at install time.

### Start here — minimum viable agent

To ship a working agent, you need all of the following:

1. **Package layout** — `manifest.json`, binary, `docs/openapi.json` at package root (§2).
2. **Valid `agentId`** — reverse-DNS with ≥2 segments (§3.5).
3. **Runtime env** — binary reads `PANTHEON_*` vars and binds HTTP to `AGENT_SERVICE_PORT` (§6).
4. **Readiness health** — `GET /api/v1/health` returns `{ "instanceId": "<PANTHEON_INSTANCE_ID>", ... }` promptly after start; aim for within **5s**, while the Hub allows up to **30s on Windows / 15s elsewhere** before failing START (§8.1).
5. **Inbound auth** — reject Hub/cron/Docs requests missing `x-pantheon-proxy-secret` (§8.2).
6. **OpenAPI file** — exists at `runtime.openApiRelativePath`; Hub validates file presence at install, not OpenAPI schema validity.
7. **Install** — Hub → Add Agent → local folder (or GitHub release zip for marketplace).

Run the full **pre-ship checklist** (§15) before declaring done.

---

## 2. Package layout

```
my-agent/
  manifest.json                 # Required — package contract
  bin/
    my-agent.exe                # Required on Windows — compiled binary (see runtime.binaryRelativePath)
  docs/
    openapi.json                # Required — OpenAPI 3.x spec for Docs tab + live test
  assets/
    icon.svg                    # Optional — branding.iconRelativePath
  dist_mfe/                     # Optional — only if manifest sets mfeDirectory
    index.html
    ...
```

**Windows v1 target:** ship `bin/my-agent.exe` (or path set in manifest). Hub validates `runtime.supportedPlatforms` includes `{ "os": "windows", "arch": "x86_64" }`.

### 2.1 Distribution formats

| Format | Layout requirement |
| --- | --- |
| **Local folder** | Folder you select in Add Agent **is** the package root — `manifest.json` must sit directly in that folder. |
| **GitHub release zip** | Attach a `.zip` with `manifest.json`, `bin/...`, `docs/...` at the **archive root** (not wrapped in a parent folder like `my-agent/manifest.json`). Max uncompressed size 512 MB; no symlinks. |
| **Supported GitHub URLs** | `https://github.com/{owner}/{repo}/releases/tag/{tag}` or `.../releases/download/{tag}/{asset.zip}` |

If the zip nests files one level deep, install fails because Hub looks for `{install_dir}/manifest.json` after extraction.

For marketplace publishing, cut a GitHub release (bump `version`), then use the Maker dashboard (§7.2.1).

---

## 3. manifest.json

Parsers **ignore unknown top-level keys**. Optional `manifestSchemaVersion` (integer; defaults to `0` when omitted in current Hub parsers).

### 3.1 Required fields

| Field | Type | Description |
| --- | --- | --- |
| `agentId` | string | Reverse-DNS id — see §3.5 |
| `packageName` | string | Display name |
| `version` | string | Semver (must parse for install/upgrade) |
| `description` | string | Short one-sentence tagline shown on marketplace cards |
| `developer` | object | `{ "name": "..." }` required; `supportUrl` optional |
| `runtime` | object | See below |
| `instanceConfigSchema` | object | Use `{ "fields": [] }` if no user config |
| `customTelemetrySchema` | object | Use `{ "visualGrid": [] }` if no custom metrics |

**`runtime` required subfields:**

| Field | Description |
| --- | --- |
| `binaryRelativePath` | Path from package root, e.g. `"./bin/my-agent.exe"` — file must exist at install |
| `openApiRelativePath` | Path to OpenAPI file, e.g. `"./docs/openapi.json"` — file must exist at install |
| `defaultResourceTier` | `ECO` \| `PERFORMANCE` \| `OVERDRIVE` (invalid values fall back to `ECO`) |
| `supportedPlatforms` | Array of `{ "os": "windows", "arch": "x86_64" }` — must match host OS/arch |

### 3.2 Optional fields

| Field | Description |
| --- | --- |
| `manifestSchemaVersion` | Integer; defaults to `0` when omitted |
| `minPantheonVersion` | Semver string — **reserved; ignored by Hub today** |
| `author` | Author/brand for marketplace cards and agent detail page |
| `details` | Relative path to Markdown (e.g. `./docs/details.md`) for agent detail page |
| `capabilities` | Array; defaults to `[]` if omitted |
| `branding.iconRelativePath` | Relative path to icon |
| `branding.accentColorHex` | Brand accent for rails, borders, and icon rings (e.g. `"#00E676"`) |
| `branding.surfaceColorHex` | Surface/card background |
| `branding.instanceCardBackgroundColorHex` | Instance card background |
| `branding.onAccentColorHex` | Text/icon on accent |
| `branding.mutedTextColorHex` | Muted label text |
| `branding.gradient` | `{ "startColorHex", "endColorHex", "angleDeg" }` |
| `mfeDirectory` | Relative path to static MFE build, e.g. `"./dist_mfe"`. **If absent, no MFE** — Hub does not show an App button. **Required when `features.hitl` is true.** If set, `index.html` must exist at install. |
| `features.hitl` | When `true`, agent may POST `/hitl/interrupt`. **MFE is mandatory** — humans resolve in the instance App. |
| `recommendedAutomation` | Cron suggestions (see section 10) |
| `billing` | Marketplace pricing + usage event keys (see section 7.2) |

### 3.3 agentId validation

Install rejects invalid `agentId` values:

- 1–128 characters total
- Reverse-DNS with **≥2 dot-separated segments**
- Each segment: 1–64 chars, starts with a lowercase letter, only `[a-z0-9_]`

Pantheon first-party agents use `com.pantheon.<name>` (e.g. `com.pantheon.hestia`).

| Valid | Invalid |
| --- | --- |
| `io.example.my_agent` | `NotReverseDns` (one segment) |
| `com.acme.email_v2` | `io.Example.agent` (uppercase) |
| `io.example.agent` | `io..agent` (empty segment) |

### 3.4 Install-time validation errors

Hub runs these checks when you install (local folder or GitHub zip):

| Error | Cause |
| --- | --- |
| `manifest.json not found in package root` | Missing manifest at package root |
| `json error: ...` | Invalid JSON or missing required field |
| `agentId must be ...` | Invalid reverse-DNS (see §3.3) |
| `Agent does not support {os}/{arch}` | Host platform not in `supportedPlatforms` |
| `Binary not found at ...` | `runtime.binaryRelativePath` missing |
| `OpenAPI spec not found at ...` | `runtime.openApiRelativePath` missing |
| `MFE index.html not found at ...` | `mfeDirectory` set but no built MFE |
| `Agents with features.hitl enabled must ship a built MFE` | `features.hitl: true` without `mfeDirectory` + `index.html` |
| `manifest.json not found in release archive` | GitHub zip missing manifest |
| `MANIFEST_MISMATCH` | Post-extract `agentId` or `version` differs from zip preview |

OpenAPI **schema validity** is not checked at install — only file existence.

### 3.5 Minimal manifest example

```json
{
  "agentId": "io.example.hello",
  "packageName": "HelloAgent",
  "version": "1.0.0",
  "description": "Minimal Pantheon agent.",
  "developer": { "name": "Example Dev" },
  "runtime": {
    "binaryRelativePath": "./bin/hello.exe",
    "openApiRelativePath": "./docs/openapi.json",
    "defaultResourceTier": "ECO",
    "supportedPlatforms": [{ "os": "windows", "arch": "x86_64" }]
  },
  "instanceConfigSchema": { "fields": [] },
  "customTelemetrySchema": { "visualGrid": [] }
}
```

### 3.6 Full manifest example

```json
{
  "manifestSchemaVersion": 1,
  "agentId": "io.example.email_intelligence",
  "packageName": "EmailIntelligence",
  "version": "1.0.0",
  "description": "Sorts and summarizes inbox threads.",
  "details": "./docs/details.md",
  "author": "Example Corp",
  "developer": {
    "name": "Example Corp",
    "supportUrl": "https://example.com/support"
  },
  "branding": {
    "iconRelativePath": "./assets/icon.svg",
    "accentColorHex": "#00E676",
    "surfaceColorHex": "#0F0F1A",
    "gradient": {
      "startColorHex": "#00E676",
      "endColorHex": "#00BFA5",
      "angleDeg": 135
    }
  },
  "runtime": {
    "binaryRelativePath": "./bin/email_intel.exe",
    "openApiRelativePath": "./docs/openapi.json",
    "defaultResourceTier": "PERFORMANCE",
    "supportedPlatforms": [{ "os": "windows", "arch": "x86_64" }]
  },
  "mfeDirectory": "./dist_mfe",
  "features": { "hitl": true },
  "instanceConfigSchema": {
    "fields": [
      {
        "id": "OPENAI_API_KEY",
        "label": "OpenAI API Key",
        "type": "SECRET",
        "required": true
      },
      {
        "id": "IMAP_HOST",
        "label": "IMAP Host",
        "type": "STRING",
        "required": true
      }
    ]
  },
  "capabilities": [
    {
      "intent": "filesystem.read",
      "params": { "paths": ["~/Downloads/Invoices"] },
      "justification": "Read invoice attachments."
    },
    {
      "intent": "filesystem.write",
      "params": { "paths": ["~/PantheonData/EmailIntelligence"] },
      "justification": "Store processed metadata."
    },
    {
      "intent": "network.outbound",
      "params": { "domains": ["api.openai.com", "imap.gmail.com"] },
      "justification": "Model and mail access."
    }
  ],
  "recommendedAutomation": [
    {
      "name": "Every 10 minutes",
      "cronExpression": "0 */10 * * * *",
      "targetEndpoint": "/api/v1/process-queue",
      "payloadTemplate": "{\"forceRun\": false}"
    }
  ],
  "customTelemetrySchema": {
    "visualGrid": [
      {
        "metricKey": "processed_emails_total",
        "label": "Processed Emails",
        "type": "COUNTER",
        "format": "INT",
        "gridSpan": 6
      },
      {
        "metricKey": "queue_pressure",
        "label": "Queue Pressure",
        "type": "GAUGE",
        "format": "PERCENTAGE",
        "gridSpan": 6
      },
      {
        "metricKey": "throughput_spark",
        "label": "Throughput (24h)",
        "type": "SPARKLINE",
        "format": "FLOAT",
        "gridSpan": 12
      }
    ]
  },
  "billing": {
    "base": { "model": "subscription", "amountCents": 500, "interval": "month", "currency": "usd" },
    "usage": [
      {
        "key": "report.generated",
        "unitLabel": "report generated",
        "unitPriceCents": 25,
        "description": "One unit per completed report"
      }
    ]
  }
}
```

---

## 4. Capability intent catalog

Every capability entry:

```json
{
  "intent": "<intent_key>",
  "params": { },
  "justification": "Human-readable reason shown in install wizard."
}
```

| Dimension | Intent key | params shape |
| --- | --- | --- |
| Observe | `desktop.screen.read` | `{}` |
| Observe | `desktop.audio.capture` | `{}` |
| Observe | `browser.chrome.read_session` | `{}` |
| Control | `desktop.keyboard.inject` | `{}` |
| Control | `desktop.mouse.control` | `{}` |
| Control | `applications.slack.control` | `{}` |
| Persist | `filesystem.read` | `{ "paths": ["~/path"] }` |
| Persist | `filesystem.write` | `{ "paths": ["~/path"] }` |
| Execute | `process.spawn` | `{ "binaries": ["ffmpeg"] }` |
| Network | `network.outbound` | `{ "domains": ["api.example.com"] }` |
| Network | `network.bind` | `{ "ports": [8080] }` |
| Identity | `identity.keychain.read` | `{ "keys": ["MySecretKey"] }` |

**Windows v1 enforcement (what Hub actually restricts today):**

| Intent | Enforced? |
| --- | --- |
| `filesystem.read` / `filesystem.write` | Partial — sandbox working directory; path checks when brokered |
| `process.spawn` | Allowlist checked when Hub spawns children for you |
| `identity.keychain.read` | Declare for install wizard approval; **env injection deferred in v1** |
| `network.outbound` | Audited/logged; hard firewall filter deferred |
| `network.bind` | Hub assigns port via `AGENT_SERVICE_PORT` |
| Observe / Control intents | Declare + audit only in v1 |

Only declare capabilities you use. Undeclared access may be logged or blocked as Hub enforcement matures.

---

## 5. instanceConfigSchema

Defines the install/create-instance form. Each field becomes an **environment variable** at spawn, keyed by `id`.

| type | UI behavior (desktop v1) | Env value |
| --- | --- | --- |
| `STRING` | Plain text input | Literal string |
| `SECRET` | Password field | Literal string (Hub stores encrypted in config payload) |
| `PATH` | Plain text input (no native directory picker yet) | Absolute path string |

Example field:

```json
{
  "id": "OPENAI_API_KEY",
  "label": "OpenAI API Key",
  "type": "SECRET",
  "required": true
}
```

At runtime: `os.environ["OPENAI_API_KEY"]` (Python) or equivalent.

---

## 6. Runtime environment (process injection)

When Pantheon **starts** your binary, it sets these on the child process:

| Variable | When injected | Description |
| --- | --- | --- |
| `PANTHEON_HOST_URI` | Always | Hub Core base URI. Example: `pantheon+http://127.0.0.1:38472` |
| `PANTHEON_INSTANCE_TOKEN` | Always | Bearer token for Hub API calls |
| `PANTHEON_INSTANCE_ID` | Always | Instance UUID |
| `AGENT_SERVICE_PORT` | Always | Hub-assigned port — **bind your HTTP server here** |
| `PANTHEON_PROXY_SECRET` | Always | Shared secret for inbound Hub/cron/Docs requests (§8.2) |
| `PANTHEON_HUB_UUID` | When hub identity exists | UUID of the hub running this instance |
| `PANTHEON_USAGE_TOKEN` | Usage marketplace agents only | Hub-scoped usage reporting token (see §7.2) |
| `PANTHEON_USAGE_ENDPOINT` | Usage agents only | Full URL of platform usage endpoint; **omitted if Hub build lacks `VITE_SUPABASE_URL`** |
| Each `instanceConfigSchema` field `id` | Per manifest | User-configured values from instance config |

Also injected on every **RESTART**.

Hub writes a debug mirror of injected env to `{sandbox}/.pantheon/host.json` (useful for troubleshooting; do not treat as primary config).

Optional agent-side: read `PANTHEON_LOG_LEVEL` (Hub does not set it; see §13).

### Parsing PANTHEON_HOST_URI

Strip the `pantheon+` prefix:

- `pantheon+http://127.0.0.1:38472` → HTTP base `http://127.0.0.1:38472`
- `pantheon+unix:///path/to/hub.sock` → Unix socket (future; use HTTP client with UDS support)

**Never** hardcode port `38472` or any port in source code.

### Minimal Hub client (Python example)

```python
import os
import httpx

def hub_base_url() -> str:
    uri = os.environ["PANTHEON_HOST_URI"]
    if uri.startswith("pantheon+"):
        uri = uri[len("pantheon+"):]
    return uri.rstrip("/")

def hub_headers() -> dict:
    return {
        "X-Pantheon-Instance-Token": os.environ["PANTHEON_INSTANCE_TOKEN"],
        "Content-Type": "application/json",
    }
```

---

## 7. Hub API — agent → Hub

All routes are under `{hub_base_url}/api/v1/...`.

**Authentication:** header `X-Pantheon-Instance-Token: {PANTHEON_INSTANCE_TOKEN}` on every agent request.

### 7.1 POST /api/v1/telemetry/submit

Send metrics and optional model usage logs. Flush every ≥1 second; max 100 metrics + 20 model logs per request.

**Request:**

```json
{
  "timestamp": 1779845759,
  "metrics": [
    { "key": "processed_emails_total", "value": 1422.0 },
    { "key": "queue_pressure", "value": 68.4 }
  ],
  "modelLogs": [
    {
      "provider": "openai",
      "model": "gpt-4o",
      "inputTokens": 840,
      "outputTokens": 230,
      "estimatedCostUsd": 0.00535
    }
  ]
}
```

**Responses:**

| Status | Body |
| --- | --- |
| 202 | `{ "status": "QUEUED", "recordsIngested": 3 }` |
| 401 | `{ "error": "INVALID_INSTANCE_TOKEN" }` |
| 413 | `{ "error": "BATCH_LIMIT_EXCEEDED", "maxMetrics": 100 }` |

Metric `key` values should match `customTelemetrySchema.visualGrid[].metricKey` for dashboard widgets. Unknown keys are still ingested (default type `GAUGE`) but won't appear in configured widgets.

> **Note:** `429 RATE_LIMITED` is reserved for a future Hub release; not returned by current Hub builds.

### 7.2 Usage billing

Usage reporting is **mission-critical billing data** and goes **directly to the Pantheon platform**, not the local hub (the hub telemetry endpoint in 7.1 stays for metrics/display only).

**Credentials (both required on every report):**

| Credential | Source | Purpose |
| --- | --- | --- |
| Maker usage API key (`puk_...`) | Maker dashboard → "Generate usage API key" (reveal-once; rotate invalidates the old key). Bake it into your agent build. | Identifies *which agent software* is reporting |
| Hub usage token (`put_...`) | Injected by the hub at spawn as `PANTHEON_USAGE_TOKEN`. Minted automatically when a buyer installs your usage agent. | Binds the report to the buyer's *(user, hub, agent)* subscription — this is the billing identity |

Your binary never needs to know who the buyer is: read `PANTHEON_USAGE_TOKEN` and `PANTHEON_USAGE_ENDPOINT` from the environment and pass them through. If `PANTHEON_USAGE_TOKEN` is absent the agent was not installed through a usage-priced marketplace entitlement — skip reporting.

**Endpoint:** `POST {PANTHEON_USAGE_ENDPOINT}` (a Supabase edge function; e.g. `https://<project>.supabase.co/functions/v1/report-agent-usage`).

**Headers:**

```
X-Pantheon-Usage-Key: {your baked-in maker key}
X-Pantheon-Usage-Token: {PANTHEON_USAGE_TOKEN}
Content-Type: application/json
```

**Request** (max 100 events per request; `eventId` is your idempotency key — UUIDs recommended; retries with the same `eventId` are acknowledged but never double-billed):

```json
{
  "instance_id": "{PANTHEON_INSTANCE_ID}",
  "events": [
    {
      "key": "report.generated",
      "quantity": 1,
      "eventId": "5f0c4d4e-9a3a-4f7e-bb1c-2f1f1f3a9d77",
      "occurredAt": "2026-06-10T18:00:00Z"
    }
  ]
}
```

Every `key` must be declared in manifest billing (`billing.usage[].key` or legacy `billing.events[].key`); `quantity` is an integer 1–100000 (default 1).

**Responses:**

| Status | Body |
| --- | --- |
| 202 | `{ "accepted": 1, "duplicates": 0, "posted": 1 }` |
| 400 | `{ "error": "...", "code": "UNDECLARED_EVENT_KEY" \| "QUANTITY_OUT_OF_RANGE" }` |
| 401 | `{ "code": "MISSING_CREDENTIALS" \| "INVALID_USAGE_KEY" \| "INVALID_USAGE_TOKEN" }` |
| 402 | `{ "code": "ENTITLEMENT_INACTIVE" }` — buyer's subscription lapsed; stop metering, keep working or degrade per your policy |
| 413 | `{ "code": "BATCH_LIMIT_EXCEEDED" }` |
| 429 | `{ "code": "RATE_LIMITED", "retryAfterMs": 60000 }` |

Buffer events locally and retry on failure (idempotency makes retries safe). Accepted events become Stripe Billing Meter events on your connected account and bill the buyer's monthly metered invoice.

**Manifest** (optional top-level `billing` block). Omit entirely for **free** marketplace agents.

**Preferred composite shape** (`billing.base` + `billing.usage[]`):

| Field | Description |
| --- | --- |
| `base.model` | `one_time` \| `subscription` |
| `base.amountCents` | Integer cents |
| `base.interval` | `month` \| `year` (subscription only) |
| `base.currency` | ISO currency; default `usd` |
| `usage[].key` | Billable event key your binary reports |
| `usage[].unitLabel` | Shown on marketplace |
| `usage[].unitPriceCents` | Cents per billed unit |
| `usage[].description` | Optional human description |

**Composite example** (subscription + usage):

```json
"billing": {
  "base": { "model": "subscription", "amountCents": 500, "interval": "month", "currency": "usd" },
  "usage": [
    { "key": "report.generated", "unitLabel": "report generated", "unitPriceCents": 25, "description": "One unit per completed report" }
  ]
}
```

**Legacy flat shape** (still accepted for back-compat):

| Field | Required when | Description |
| --- | --- | --- |
| `model` | Paid agents | `free` \| `one_time` \| `subscription` \| `usage` |
| `amountCents` | `one_time`, `subscription` | Integer cents (e.g. `500` = $5.00) |
| `interval` | `subscription` | `month` or `year` (default `month`) |
| `currency` | Optional | Default `usd` |
| `unitLabel` | `usage` (recommended) | Shown on marketplace |
| `unitPriceCents` | `usage` | Cents per billed unit |
| `events` | `usage` | Billable event keys: `[{ "key", "description?" }]` |

**Free** — omit `billing` or set `"model": "free"`.

**One-time purchase (legacy):**

```json
"billing": {
  "model": "one_time",
  "amountCents": 1500,
  "currency": "usd"
}
```

**Usage-based (legacy):**

```json
"billing": {
  "model": "usage",
  "unitLabel": "report generated",
  "unitPriceCents": 25,
  "events": [{ "key": "report.generated", "description": "One unit per completed report" }]
}
```

> **Composite pricing:** the marketplace resolves your `billing` block into an optional **base fee** (`one_time` or recurring) plus **0..N usage components**. Cards show the base price with a `Usage-based` chip; detail pages render the full breakdown. Pantheon is the Merchant of Record (Stripe destination charges, 30% platform fee); you onboard as a payout recipient.

### 7.2.1 Updating a published agent

Cut a new GitHub release (bump `manifest.json` `version`), then in the web **Maker dashboard** click **Update** on the agent and confirm (the modal previews current vs new version). Updating a *published* agent auto re-publishes: Pantheon re-extracts your package preview, re-resolves composite pricing, and re-syncs Stripe prices/meters in one step — so a pricing change in the new manifest goes live with the version bump.

Ensure the release zip uses a **flat layout** (§2.1).

Desktop clients detect the new version against the catalog and surface an **Update available** affordance (toast with *Install now* / *Later*, an OS notification when unfocused, and per-agent buttons on the Agents pages). *Install now* upgrades in place (no reinstall). Bump `version` on every release so clients can detect updates by semver.

### 7.2.2 Runtime lock (paid agents)

Paid marketplace agents are gated to their owner. An installed paid agent runs only while the **owning user is signed in on that hub** with an **active, non-expired entitlement**. Offline use is allowed while the cached entitlement is healthy; it suspends after sync if the subscription is `past_due`/`unpaid`/`canceled` or past its grace window, or if a different user signs in. Suspended instances surface a "sign in as the owner" note and refuse to start until the gate passes. Entitlement is keyed by `(user, agent, hub)` — one purchase does not span hubs.

### 7.3 POST /api/v1/hitl/interrupt

Enqueue a human-in-the-loop event for this instance. This **does not pause the whole instance** — only the agent task that posted the interrupt should block (by polling status). Other agent HTTP endpoints keep serving.

**Requires:** If your manifest declares `"features": { "hitl": true }`, you **must** ship an MFE (`mfeDirectory` with `index.html`). Humans resolve HITL inside the instance App, not the Hub shell.

**Request:**

```json
{
  "breakpointId": "brk_99a8b11c_32e9",
  "urgency": "HIGH",
  "headline": "Approval required",
  "summary": "Agent needs confirmation before continuing.",
  "payloadContext": {},
  "interactiveFormSchema": {
    "fields": [
      {
        "id": "decision",
        "type": "DROPDOWN",
        "label": "Decision",
        "options": ["APPROVE", "DENY"],
        "default": "DENY"
      }
    ]
  }
}
```

**Responses:**

| Status | Body |
| --- | --- |
| 201 | `{ "status": "INTERRUPT_ACTIVE", "activeListenersNotifiedCount": 1 }` |
| 401 | `{ "error": "INVALID_INSTANCE_TOKEN" }` |
| 409 | `{ "error": "BREAKPOINT_ALREADY_ACTIVE", "breakpointId": "..." }` |

### 7.3 GET /api/v1/hitl/{breakpointId}/status

Poll while frozen until resolved.

**Response 200:**

```json
{
  "status": "RESOLVED",
  "resolution": {
    "decision": "APPROVE",
    "adminAnnotation": "Verified manually."
  }
}
```

`status` is `ACTIVE`, `RESOLVED`, or `ABORTED`. On `RESOLVED`, `resolution` keys match your `interactiveFormSchema` field ids.

**Who may call which HITL routes:**

| Route | Agent (instance token) | MFE (session) | Hub Desktop (IPC) |
| --- | --- | --- | --- |
| `POST /api/v1/hitl/interrupt` | Yes — raise events for this instance | No | No |
| `GET /api/v1/hitl/{breakpointId}/status` | Yes — poll own breakpoints only | No | No |
| `GET /api/v1/instances/{instanceId}/hitl/active` | Yes — **own instance only** | Yes — **own instance only** | Yes (admin) |
| `POST /api/v1/instances/{instanceId}/hitl/{id}/resolve` | Yes — **own instance only** | Yes — **own instance only** | Yes (relay) |

Pantheon manages one FIFO HITL queue per **instance**. Agents and MFEs interact with the queue only through these Hub endpoints. **Agent A shall not access Agent B's queue** — every list/resolve/status call validates that the authenticated caller's `instance_id` matches the path or breakpoint owner.

**HITL loop pattern:**

1. POST `/hitl/interrupt` (enqueue event; notify devices)
2. Poll GET `/hitl/{breakpointId}/status` every 1–2 seconds **from the waiting task only**, or subscribe to `GET /api/v1/events/stream` for `PANTHEON://HITL_RESOLVED`
3. When `status === "RESOLVED"`, read `resolution` and continue that task
4. Human resolution is typically done by the instance **MFE** via `POST /api/v1/instances/{instanceId}/hitl/{breakpointId}/resolve` (MFE session). Agents may also resolve their own queue when programmatic resolution is appropriate.

### 7.4 POST /api/v1/instances/{instanceId}/message

Push a short message to the Hub UI for this instance (toast/notification via SSE).

**Request:**

```json
{ "message": "Sync completed — 42 items processed." }
```

**Responses:**

| Status | Body |
| --- | --- |
| 202 | `{ "status": "queued", "instanceId": "..." }` |
| 400 | `{ "error": "MESSAGE_REQUIRED" }` |
| 401 | Invalid or cross-instance token |

Publishes SSE event `PANTHEON://AGENT_MESSAGE` with `{ instanceId, message }`. Optional — use when you want operator-visible status without opening the App.

### 7.5 Inter-agent invoke and API grants

Orchestrator agents (e.g. a future Zeus) may call **other agents' HTTP APIs** through the Hub. The Hub enforces grants, normalizes paths, injects the **target** proxy secret on loopback forward, and **never** returns the target secret to the caller.

**Authentication:** all routes below use `X-Pantheon-Instance-Token` for the **caller** instance (same header as telemetry/HITL).

#### Discovery

| Route | Purpose |
| --- | --- |
| `GET /api/v1/invoke-targets` | Granted targets with **running** instances (OpenAPI-capable agents only) |
| `GET /api/v1/invoke-catalog` | Installed API-capable agents/instances the caller may request access to (includes grant status) |

#### Invoke

```
ANY /api/v1/invoke/{targetInstanceId}/{path}
```

1. Hub normalizes `{path}` (percent-decode, collapse `//`, resolve `.`/`..`; reject root escape → `400 PATH_TRAVERSAL`).
2. Rejects `callerInstanceId == targetInstanceId` → `400 SELF_INVOKE_FORBIDDEN`.
3. Checks grant (instance override → agent default → deny).
4. Requires target instance `RUNNING`.
5. Forwards to `http://127.0.0.1:{targetPort}{normalizedPath}` with **`x-pantheon-proxy-secret`** for the target only, and injects **`x-pantheon-caller-instance-id`** identifying the calling instance, so the target agent can attribute the request to its caller.

**Prefix matching:** when a grant includes `apiRoutePrefix` (e.g. `/messages`), the path must equal the prefix or start with `{prefix}/`. `/messages-admin` does **not** match prefix `/messages`. `NULL` prefix = full API.

#### Grant management (caller-owned UX)

Orchestrator agents **own the consent UI** (typically in their MFE). The Hub stores grants only — there is no Hub desktop settings screen for this.

| Route | Purpose |
| --- | --- |
| `GET /api/v1/instances/{callerInstanceId}/api-grants` | List agent-level + instance-level grants for this caller |
| `PUT /api/v1/instances/{callerInstanceId}/api-grants/agent` | Body: `{ "targetAgentId", "apiRoutePrefix"? }` |
| `DELETE /api/v1/instances/{callerInstanceId}/api-grants/agent/{targetAgentId}` | Revoke agent-level grants to target |
| `PUT /api/v1/instances/{callerInstanceId}/api-grants/instance` | Body: `{ "targetInstanceId", "isAllowed", "apiRoutePrefix"? }` |
| `DELETE /api/v1/instances/{callerInstanceId}/api-grants/instance/{targetInstanceId}` | Remove instance override |

Grant resolution: **instance deny** overrides **agent allow**; default deny → `403 AGENT_INVOKE_DENIED`.

Grant create/revoke and every invoke attempt are written to the caller's `instance_event_log`.

### 7.6 Bridge + Core — plugin workers borrowing caller primitives

Use this when a **plugin** runs long async work (HTTP **202** job) and needs the caller's LLM, vision, desktop perception, or input — without embedding provider SDKs or hardcoding caller URLs.

This complements §7.5: the caller invokes the plugin's public OpenAPI; the plugin reverse-invokes the caller's **Core** surface via a **bridge manifest**.

#### Roles

| Role | Responsibility |
| --- | --- |
| **Caller (orchestrator)** | Exposes `POST /api/v1/core`, mints session tokens, assembles bridge via canonical helper |
| **Plugin (worker)** | Exposes async job API requiring `bridge` in body; ships fixed `Delegate` for its capability vocabulary |
| **Hub** | Forwards invoke both directions; grants + `x-pantheon-caller-instance-id` |

#### Core endpoint (caller implements)

```
POST /api/v1/core
X-Pantheon-Core-Token: <session-token>

{ "function": "textLlm"|"vlm"|"input"|"desktopPerception", "args": { ... } }
```

- v1 functions: `textLlm`, `vlm`, `input`, `desktopPerception` (callers may extend; plugins declare what they need).
- **Must not** appear in public OpenAPI used for LLM `search_apis` or Docs Try It.
- Session: allowlisted functions, TTL, revoke on job end; narrow Hub grant prefix (e.g. `/api/v1/core` only).

Optional session mint/revoke:

| Route | Purpose |
| --- | --- |
| `POST /api/v1/core/sessions` | Mint token + allowlist |
| `DELETE /api/v1/core/sessions/{token}` | Revoke token; release input sub-lease |

#### Bridge manifest (assembled per job)

Pipeline on the **caller**:

1. Mint Core session (code, not LLM).
2. Optional LLM structured hints (capability subset only).
3. **`build_*_bridge_manifest(session)`** helper fills canonical `path`, `body.function`, flags.
4. Validate against [bridge schema](../agents/ms-paint-api/docs/bridge-schema.json).
5. Pass manifest in plugin async job POST body alongside domain fields (`prompt`, etc.).

The LLM should **not** emit boilerplate paths like `/api/v1/core` every job — that belongs in version-controlled caller code.

Example (Zeus → ms-paint artist):

```json
{
  "bridgeVersion": 1,
  "session": { "token": "...", "expiresAt": "...", "callerInstanceId": "..." },
  "resources": {
    "vlm": {
      "path": "/api/v1/core",
      "body": { "function": "vlm", "args": "{{args}}" }
    }
  }
}
```

The `"{{args}}"` placeholder is documentation — plugins merge args at the JSON object level, not via string replace.

#### Plugin Delegate (fixed code)

Plugins ship stable methods (e.g. `text_llm`, `vlm`, `input`, `desktop_perception`). Transport Hub reverse-invokes the caller using the manifest + instance token. **Never** LLM-generated Python in the plugin.

#### Input sub-lease ownership

`input` is scoped to the Core session. **Input sub-leases never outlive the Core session** — revoke session, TTL expiry, or job DELETE always ends the sub-lease and releases input state (`mouseUp`, modifier `keyUp`). Do not refresh sub-leases independently of the session.

#### Security checklist

- Token per job; revoke on DELETE / complete
- Function allowlist on session
- Bidirectional grants (caller→plugin public API; plugin→caller `/api/v1/core` prefix)
- Core excluded from OpenAPI index

#### Reference implementations

- Caller Core: [`agents/zeus/src/zeus/core/`](../agents/zeus/src/zeus/core/)
- Plugin: [`agents/ms-paint-api/`](../agents/ms-paint-api/) draw jobs + Delegate
- Protocol doc: [`agents/ms-paint-api/docs/bridge-protocol.md`](../agents/ms-paint-api/docs/bridge-protocol.md)
- Windows UI automation: [`agents/ms-paint-api/docs/windows-ui-automation.md`](../agents/ms-paint-api/docs/windows-ui-automation.md)

#### Anti-patterns

- Asking the LLM to emit full bridge JSON including paths every job
- Plugin-specific routes on the caller (`/delegate/vision/plan`)
- LLM-generated executable code in plugins
- Long-running synchronous HTTP on plugin job POST
- Exposing Core via public OpenAPI

---

## 8. Agent HTTP service — Hub → agent

Your binary runs an HTTP server on **`AGENT_SERVICE_PORT`** (always injected at spawn). Bind to `0.0.0.0:{AGENT_SERVICE_PORT}`.

> **Deprecated:** `{PANTHEON_HOST_URI}/api/v1/agents/{agentId}/{instanceNickname}/...` returns **410 GONE**. Do not implement clients against this route.

### How Hub reaches your agent

| Caller | Mechanism |
| --- | --- |
| **Cron triggers** | Direct `http://127.0.0.1:{AGENT_SERVICE_PORT}{path}` with header `x-pantheon-proxy-secret: {PANTHEON_PROXY_SECRET}` |
| **Docs tab "Try It"** | Tauri IPC → same direct forward with proxy secret |
| **MFE** | Browser calls `{hubBaseUrl}/api/v1/instances/{instanceId}/{path}` with `X-Pantheon-Mfe-Session`; Hub forwards to agent port with proxy secret |
| **Inter-agent invoke** | Caller agent calls `{hubBaseUrl}/api/v1/invoke/{targetInstanceId}/{path}` with `X-Pantheon-Instance-Token`; Hub forwards with **target** proxy secret (§7.5) |

Your agent process does not need to expose a public port — Hub uses loopback forwarding.

### 8.1 Readiness contract (required for START)

After spawn, the Hub probes until the agent responds or a platform timeout elapses (**30s on Windows**, **15s elsewhere**). Agents should still expose health **within ~5 seconds** — implement `/api/v1/health` before slow initialization (frozen binaries may need most of the Hub window on first cold start).

`GET http://127.0.0.1:{AGENT_SERVICE_PORT}/api/v1/health`

The response must be HTTP 2xx with JSON:

```json
{
  "instanceId": "<must equal PANTHEON_INSTANCE_ID>",
  "agentId": "<must equal manifest agentId, e.g. com.pantheon.ms_paint_api>",
  "version": "<manifest version string>",
  "status": "ok"
}
```

If `instanceId` or `agentId` is missing or mismatched, START fails with a readiness timeout. Implement this route before any slow initialization.

### 8.2 Inbound request authentication

Hub injects `PANTHEON_PROXY_SECRET` at every START/RESTART and sends it as header **`x-pantheon-proxy-secret`** on cron and Docs forwards.

**Your agent should:**

1. Read `PANTHEON_PROXY_SECRET` from env at startup.
2. For routes invoked by Hub automation (cron targets, any route you document for Try It), require the header to match.
3. Return `401` or `403` on mismatch.

Cron triggers fail silently in Hub logs if the secret is wrong or missing. Health checks from Hub readiness probe do **not** send the proxy secret.

**Minimal middleware (Python/FastAPI example):**

```python
import os
from fastapi import Request, HTTPException

PROXY_SECRET = os.environ.get("PANTHEON_PROXY_SECRET", "")

async def require_proxy_secret(request: Request) -> None:
    if request.url.path == "/api/v1/health":
        return  # readiness probe — no secret
    header = request.headers.get("x-pantheon-proxy-secret", "")
    if not PROXY_SECRET or header != PROXY_SECRET:
        raise HTTPException(status_code=403, detail="Invalid proxy secret")
```

### OpenAPI requirements

- File at `runtime.openApiRelativePath` (OpenAPI 3.0+ recommended)
- Document all routes your service exposes
- Include request/response schemas so Pantheon **Docs tab** can render and live-test
- Use clear `operationId` and `tags`
- **Must document** `/api/v1/health` with `instanceId` in the response schema

**Minimal openapi.json example:**

```json
{
  "openapi": "3.0.3",
  "info": { "title": "Hello Agent", "version": "1.0.0" },
  "paths": {
    "/api/v1/health": {
      "get": {
        "operationId": "health",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["instanceId"],
                  "properties": {
                    "instanceId": { "type": "string", "description": "Must match PANTHEON_INSTANCE_ID" },
                    "status": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/process-queue": {
      "post": {
        "operationId": "processQueue",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "forceRun": { "type": "boolean" } }
              }
            }
          }
        },
        "responses": { "200": { "description": "Processed" } }
      }
    }
  }
}
```

---

## 9. customTelemetrySchema

Defines dashboard widgets. Types: `COUNTER`, `GAUGE`, `SPARKLINE`.

| type | Submit behavior |
| --- | --- |
| COUNTER | Monotonically increasing value |
| GAUGE | Point-in-time value (0–100 for PERCENTAGE format) |
| SPARKLINE | Time-series samples; submit repeated keys over time |

**visualGrid entry:**

```json
{
  "metricKey": "processed_emails_total",
  "label": "Processed Emails",
  "type": "COUNTER",
  "format": "INT",
  "gridSpan": 6
}
```

`gridSpan` is 1–12 (default 6). Submit matching keys in `POST /telemetry/submit` → `metrics[].key` for widget display.

---

## 10. recommendedAutomation

Cron-only in v1 for agent packages (Hub also supports lifecycle/system triggers internally; your manifest only seeds agent cron suggestions). Six-field cron with seconds:

```
{sec} {min} {hour} {dom} {month} {dow}
```

Example: `0 */10 * * * *` = every 10 minutes.

```json
{
  "name": "Hourly sync",
  "cronExpression": "0 0 * * * *",
  "targetEndpoint": "/api/v1/sync",
  "payloadTemplate": "{}"
}
```

**`targetEndpoint` format:**

- Default method is **POST**.
- Optional HTTP method prefix: `"GET:/api/v1/health"`, `"POST:/api/v1/process-queue"`.
- Path must start with `/` (e.g. `/api/v1/sync`, not `api/v1/sync`).

Hub cron invokes the endpoint on **your agent HTTP service** via loopback + proxy secret (§8.2). Instance must be **RUNNING**.

---

## 11. Optional MFE (micro-frontend)

If `mfeDirectory` is set (e.g. `"./dist_mfe"`), build static assets (HTML/JS/CSS) into that folder with `index.html` at the MFE root. Pantheon serves them in a native **App** window at `/app/{instanceId}/`.

If `mfeDirectory` is **omitted**, you have no MFE. Do not add placeholder UI unless needed.

### Bootstrap: `window.__PANTHEON__`

Hub injects a script into `index.html`:

| Field | Description |
| --- | --- |
| `instanceId` | This instance UUID |
| `hubBaseUrl` | Hub HTTP base (e.g. `http://127.0.0.1:38472`) |
| `hubPort` | Hub port number |
| `proxyBasePath` | Prefix for API calls — desktop: `/api/v1/instances/{instanceId}`; spoke: `/api/v1/spoke/instances/{instanceId}/api` |
| `mfeSession` | MFE auth token for the `X-Pantheon-Mfe-Session` header — the auth mechanism on both desktop and spoke |
| `hitl.focusedBreakpointId` | Set when App opened from HITL notification (`?hitl={id}`) |
| `hitl.queueLength` | Active HITL queue depth at load time |

Hub also injects `<base href="/app/{instanceId}/">` for relative asset URLs.

### MFE → Hub API

- Authenticate with header **`X-Pantheon-Mfe-Session: {mfeSession}`**
- List HITL queue: `GET /api/v1/instances/{instanceId}/hitl/active` → `{ currentBreakpointId, queueLength, items[] }`
- Resolve: `POST /api/v1/instances/{instanceId}/hitl/{breakpointId}/resolve`
- Proxy to agent routes: `GET/POST {hubBaseUrl}{proxyBasePath}/{yourRoute}` (Hub adds proxy secret when forwarding)

Do not use deprecated `hasMicroFrontend` or nested `ui` keys — **`mfeDirectory` string only**.

---

## 12. Resource tiers

Design targets for Windows Job Objects. Set `runtime.defaultResourceTier` in manifest; user may override at install.

| Tier | CPU cap (target) | RAM cap (target) | Disk quota (target) |
| --- | --- | --- | --- |
| ECO | 10% | 512 MB | 1 GB |
| PERFORMANCE | 40% | 2 GB | 10 GB |
| OVERDRIVE | 80% | 8 GB | 50 GB |

**v1 actual enforcement:** RAM limits are partially implemented via Windows Job Objects but may not be applied on all spawn paths. CPU and disk quotas are **not enforced yet** — treat tier selection as advisory for operators; do not rely on hard caps for security isolation.

---

## 13. Logging and the Hub log panel

Pantheon captures agent **stdout** and **stderr** line-by-line into `instance_event_log` and displays them in the Hub **Logs** panel on the agent instance card. Follow this contract so operators are not flooded with false "errors."

### Stream contract

| Stream | Allowed levels | Purpose |
| --- | --- | --- |
| **stdout** | DEBUG, INFO, WARNING | Normal operation, diagnostics, recoverable issues |
| **stderr** | ERROR, CRITICAL only | Failures requiring operator attention |

**Do not** use Python's default `logging.basicConfig()` without splitting streams — it writes INFO to stderr and makes healthy agents look broken in the Hub.

**Telemetry / heartbeat loops** (e.g. periodic `POST /telemetry/submit`) must log at **DEBUG**, not INFO. They are hidden in the Hub's default log view unless the operator enables **Show debug logs**.

**Third-party loggers** (httpx, httpcore, uvicorn access): set to WARNING or higher in production. Never emit INFO on every successful HTTP request.

### Python reference setup

```python
import logging
import os
import sys

def configure_logging() -> None:
    level_name = os.environ.get("PANTHEON_LOG_LEVEL", "INFO").upper()
    level = getattr(logging, level_name, logging.INFO)
    fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")

    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setLevel(logging.DEBUG)
    stdout_handler.addFilter(lambda record: record.levelno < logging.ERROR)
    stdout_handler.setFormatter(fmt)

    stderr_handler = logging.StreamHandler(sys.stderr)
    stderr_handler.setLevel(logging.ERROR)
    stderr_handler.setFormatter(fmt)

    logging.basicConfig(level=level, handlers=[stdout_handler, stderr_handler], force=True)
    logging.getLogger("httpx").setLevel(logging.WARNING)
    logging.getLogger("httpcore").setLevel(logging.WARNING)
    logging.getLogger("uvicorn").setLevel(logging.WARNING)
    logging.getLogger("uvicorn.error").setLevel(logging.WARNING)
```

Optional: set `PANTHEON_LOG_LEVEL=DEBUG` during local debugging to emit DEBUG lines (Hub **Show debug logs** toggle reveals them when captured).

### Telemetry worker example

```python
logger = logging.getLogger(__name__)

async def _loop(self) -> None:
    while True:
        try:
            await self._hub.submit_telemetry(metrics)
            logger.debug("Telemetry submitted (%d metrics)", len(metrics))
        except Exception as exc:
            logger.warning("Telemetry submit failed: %s", exc)
        await asyncio.sleep(5.0)
```

---

## 14. Local development without Pantheon (dev-only)

For standalone debugging, set env vars manually. **Do not ship this in production code paths.**

```powershell
$env:PANTHEON_HOST_URI = "pantheon+http://127.0.0.1:9470"
$env:PANTHEON_INSTANCE_TOKEN = "dev-token"
$env:PANTHEON_INSTANCE_ID = "00000000-0000-0000-0000-000000000001"
$env:PANTHEON_HUB_UUID = "00000000-0000-0000-0000-000000000002"
$env:AGENT_SERVICE_PORT = "18080"
$env:PANTHEON_PROXY_SECRET = "dev-proxy-secret"
.\bin\my-agent.exe
```

Run a mock Hub or point at a running Pantheon instance. This is **not** the install contract — Pantheon injects vars automatically when it spawns your binary.

---

## 15. Pre-ship checklist

Before declaring the agent package complete, verify every item:

- [ ] `manifest.json` parses as valid JSON with all required fields (§3.1)
- [ ] `agentId` passes reverse-DNS rules (§3.3)
- [ ] `runtime.binaryRelativePath` file exists and runs on Windows x86_64
- [ ] `runtime.supportedPlatforms` includes `{ "os": "windows", "arch": "x86_64" }`
- [ ] `docs/openapi.json` (or path in manifest) exists; documents `/api/v1/health` with `instanceId`
- [ ] Agent reads `PANTHEON_HOST_URI`, `PANTHEON_INSTANCE_TOKEN`, `PANTHEON_INSTANCE_ID`, `AGENT_SERVICE_PORT`, `PANTHEON_PROXY_SECRET` from process env — no hardcoded Hub URL
- [ ] `GET /api/v1/health` returns `{ "instanceId": "<PANTHEON_INSTANCE_ID>", ... }` within ~5s of start (Hub allows 30s on Windows / 15s elsewhere)
- [ ] Inbound routes (cron targets) validate `x-pantheon-proxy-secret`
- [ ] Agent binds HTTP server to `0.0.0.0:{AGENT_SERVICE_PORT}`
- [ ] Agent implements `POST /telemetry/submit` client with correct headers
- [ ] Metric keys in telemetry match `customTelemetrySchema.visualGrid[].metricKey` (if any widgets configured)
- [ ] Agent HTTP server exposes routes documented in OpenAPI
- [ ] HITL flow: interrupt → poll status → handle RESOLVED (if agent uses HITL); MFE built if `features.hitl`
- [ ] `capabilities` lists only intents the agent actually needs
- [ ] `instanceConfigSchema` fields match env vars the agent reads
- [ ] If using MFE: `mfeDirectory` points to built static files with `index.html`
- [ ] If no MFE: `mfeDirectory` is **not** present in manifest
- [ ] If publishing to marketplace: GitHub zip has flat layout (§2.1); billing event keys match usage reports
- [ ] `recommendedAutomation` `targetEndpoint` values start with `/`
- [ ] No dependency on `.env` file for Pantheon connection vars
- [ ] stderr contains no INFO or WARNING lines during normal operation (ERROR/CRITICAL only)
- [ ] Telemetry heartbeat / polling loops log at DEBUG, not INFO

---

## 16. Common mistakes

| Mistake | Fix |
| --- | --- |
| Hardcoding `http://127.0.0.1:9470` | Read `PANTHEON_HOST_URI`, strip `pantheon+` prefix |
| Using `dotenv` for Hub connection | Use `os.environ` / `process.env` for `PANTHEON_*` |
| Calling Vite/Tauri dev server | Only call Hub Core API from env URI |
| Health returns only `{ "status": "ok" }` | Include `"instanceId": os.environ["PANTHEON_INSTANCE_ID"]` — START fails without it |
| Using deprecated `/api/v1/agents/{agentId}/{nickname}/...` proxy | Hub returns 410; cron/Docs use loopback + proxy secret (§8) |
| Ignoring `x-pantheon-proxy-secret` | Validate against `PANTHEON_PROXY_SECRET` on automated routes |
| Missing OpenAPI file | Add `docs/openapi.json`; set `runtime.openApiRelativePath` |
| Linux binary on Windows Hub | Ship `.exe`; match `supportedPlatforms` |
| Metric keys mismatch | Align telemetry `key` with manifest `metricKey` for dashboard widgets |
| Agent calls HITL resolve endpoint | Agents may resolve **own instance** queue only; MFE resolves for human UX |
| Empty capabilities but agent needs network | Declare `network.outbound` with domains |
| `hasMicroFrontend` or nested `ui` object | Use top-level `mfeDirectory` string only |
| GitHub zip wrapped in parent folder | Flatten so `manifest.json` is at zip root (§2.1) |
| `logging.basicConfig()` without stream split | Route DEBUG/INFO/WARNING to stdout; ERROR+ to stderr only |
| Logging every telemetry POST at INFO | Use `logger.debug` for heartbeat success |
| Cron endpoint without leading `/` | Use `/api/v1/sync`, not `api/v1/sync` |

---

## 17. Suggested implementation order

1. Create package directory layout (§2)
2. Write minimal `manifest.json` and `openapi.json` with health + `instanceId`
3. Implement binary that reads `PANTHEON_*` + `AGENT_SERVICE_PORT`, binds HTTP server, validates proxy secret
4. Implement `GET /api/v1/health` returning matching `instanceId`
5. Add telemetry submit on a timer or after each work unit
6. Add `instanceConfigSchema` fields and read them from env
7. Declare capabilities; add `customTelemetrySchema` widgets
8. Add HITL interrupt + poll loop if agent needs human approval; build MFE if `features.hitl`
9. Add `recommendedAutomation` entries matching your OpenAPI routes
10. Optionally build MFE and set `mfeDirectory`
11. Configure logging per §13 (stdout/stderr split, DEBUG telemetry)
12. Run pre-ship checklist (§15)

You are done when the package passes the checklist and can be installed via Pantheon Hub **Add Agent → local folder** (or published via GitHub release for marketplace).
