Skill: michael-stokoe/statamic-ai-gateway (version 0.2.0)

--- name: statamic-ai-gateway description: Manage Statamic content through a tool execution gateway (composer require stokoe/ai-gateway). version: 0.0.8 metadata: openclaw: requires: env: - AI_GATEWAY_SITES_CONFIG bins: - curl - jq config: - ~/.config/ai-gateway/sites.json primaryEnv: AI_GATEWAY_SITES_CONFIG emoji: "🛡️" homepage: https://github.com/stokoe/ai-gateway --- # AI Gateway — Agent Skill Manage Statamic content through a safe, authenticated tool execution gateway. Supports managing multiple Statamic sites from a single agent installation. Before first use, follow the setup in [INSTALL.md](./INSTALL.md). For the endpoint contract, see [references/api.md](./references/api.md). ## Site Registry Credentials are stored in `~/.config/ai-gateway/sites.json` (override with `AI_GATEWAY_SITES_CONFIG`). ```json { "sites": { "marketing": { "base_url": "https://marketing.example.com", "token": "token-aaa..." }, "docs": { "base_url": "https://docs.example.com", "token": "token-bbb..." } } } ``` Look up `base_url` and `token` by site name before every request. ## Endpoints | Method | Path | Purpose | |--------|-----------------------------------|----------------------------------------| | POST | `/ai-gateway/execute` | Execute a tool | | GET | `/ai-gateway/capabilities` | List all tools and their enabled state | | GET | `/ai-gateway/capabilities/{tool}` | Get full usage docs for a specific tool | All requests require `Authorization: Bearer {token}`. ## Discovery-First Workflow Do not guess tool arguments. Always discover before executing: 1. `GET /capabilities` — see which tools are enabled on this site 2. `GET /capabilities/{tool.name}` — get the argument schema, validation rules, example request/response, allowed targets, denied fields, and behavioral notes for that tool 3. Use the returned information to construct your `/execute` request This is the primary way to learn how to use any tool. The capabilities endpoints are the source of truth. ## Request Envelope ```json { "tool": "tool.name", "arguments": { }, "request_id": "optional-tracking-id", "idempotency_key": "optional-dedup-key", "confirmation_token": "optional-if-confirming" } ``` ## Response Envelope Success: `{ "ok": true, "tool": "...", "result": { ... }, "meta": { ... } }` Error: `{ "ok": false, "tool": "...", "error": { "code": "...", "message": "..." }, "meta": { ... } }` ## Rules > **⛔ CRITICAL — Structured field values are READ-ONLY structures.** > Bard, Replicator, Grid, and similar fields store values as deeply nested ProseMirror/TipTap JSON. When reading these back from `entry.get` or `global.get`, you will see arrays of node objects with `type`, `attrs`, `content`, and `marks` keys. > > **You MUST NOT alter the structure.** Never add, remove, reorder, or rename nodes, attributes, or marks. You may **only** change the literal `text` strings inside leaf nodes — nothing else. > > To update a rich-text field: (1) fetch with `entry.get`/`global.get`, (2) change only `text` values, (3) send back structurally identical. Violating this corrupts content. 1. Look up `base_url` and `token` from `sites.json` before every request. 2. **Discover before executing.** Call `/capabilities` then `/capabilities/{tool}` before using any tool for the first time on a site. 3. Only call tools where `enabled: true`. Only target allowlisted resources. `forbidden` means off-limits. 4. `data` must be a JSON object, never an array or string. Don't send unknown argument keys. 5. Prefer `entry.upsert` over `entry.create` — safer and idempotent. 6. `navigation.update` is a full tree replacement. Always fetch with `navigation.get` first. 7. **Confirmation-gated tools require user approval.** If `requires_confirmation: true` in capabilities: (1) send the request, (2) receive `confirmation_required` with a token, (3) show the user the `operation_summary` and ask permission, (4) only if approved, resend with `confirmation_token`. **Never auto-confirm.** 8. If `rate_limited`, back off and retry. 9. Include the site name in `request_id` (e.g. `marketing:upsert-about`). 10. After bulk content changes, consider warming caches: `stache.warm` rebuilds content indexes, `static.warm` regenerates static pages. ## Error Codes | Code | HTTP | Action | |------------------------|------|-------------------------------------------------| | `unauthorized` | 401 | Check token in `sites.json` | | `forbidden` | 403 | Target not in allowlist | | `tool_not_found` | 404 | Check name against `/capabilities` | | `tool_disabled` | 403 | Tool is off on this site | | `validation_failed` | 422 | Read `error.message` and `error.details` | | `resource_not_found` | 404 | Collection/entry/global/nav/taxonomy missing | | `conflict` | 409 | Entry exists — use `entry.upsert` | | `rate_limited` | 429 | Wait and retry | | `confirmation_required`| 200 | Resend with `confirmation_token` (after user OK)| | `execution_failed` | 500 | Retry or report | | `internal_error` | 500 | Retry or report |

path: _meta.json
content: ownerId: kn73968bdhsez3zdw6hgcjqnq584tarc
slug: statamic-ai-gateway
version: 0.0.6
publishedAt: 1776240884069

path: references/api.md
content: # AI Gateway — API Reference

Endpoint contract for the AI Gateway tool execution interface. For tool-specific argument schemas, examples, and behavioral notes, use the `/capabilities/{tool}` endpoint — it is the source of truth.

## Site Credentials

```bash
SITE_URL=$(jq -r '.sites.SITE_NAME.base_url' ~/.config/ai-gateway/sites.json)
SITE_TOKEN=$(jq -r '.sites.SITE_NAME.token' ~/.config/ai-gateway/sites.json)
```

## Endpoints

| Method | Path                              | Purpose                                |
|--------|-----------------------------------|----------------------------------------|
| POST   | `/ai-gateway/execute`             | Execute a tool                         |
| GET    | `/ai-gateway/capabilities`        | List all tools and their enabled state |
| GET    | `/ai-gateway/capabilities/{tool}` | Full usage docs for a specific tool    |

All requests require `Authorization: Bearer {token}`. POST requires `Content-Type: application/json`.

---

## Discovery

### List all tools

```bash
curl -s -H "Authorization: Bearer $SITE_TOKEN" \
  $SITE_URL/ai-gateway/capabilities | jq .
```

Returns each tool's `enabled` state, `target_type`, and `requires_confirmation` flag.

### Get tool usage

```bash
curl -s -H "Authorization: Bearer $SITE_TOKEN" \
  $SITE_URL/ai-gateway/capabilities/entry.upsert | jq .
```

Returns: argument schema with types/required/defaults, validation rules, example request and response, allowed targets, denied fields, and behavioral notes. **Always call this before using a tool for the first time on a site.**

---

## Request Envelope

```json
{
    "tool": "string (required)",
    "arguments": { "(required, tool-specific)" },
    "request_id": "string (optional — echoed in response)",
    "idempotency_key": "string (optional — logged for dedup)",
    "confirmation_token": "string (optional — for confirming gated ops)"
}
```

## Response Envelopes

### Success

```json
{ "ok": true, "tool": "entry.upsert", "result": { "status": "created", ... }, "meta": { "request_id": "..." } }
```

### Error

```json
{ "ok": false, "tool": "...", "error": { "code": "...", "message": "...", "details": { } }, "meta": { } }
```

`error.details` is present only for `validation_failed`.

### Confirmation Required

```json
{
    "ok": false,
    "tool": "cache.clear",
    "error": { "code": "confirmation_required", "message": "..." },
    "confirmation": { "token": "...", "expires_at": "...", "operation_summary": { "tool": "...", "target": "...", "environment": "..." } },
    "meta": {}
}
```

To confirm: resend the exact same request with `"confirmation_token": "..."` added at the envelope top level. Token expires after 60s and is bound to the exact tool + arguments.

---

## Error Codes

| Code                   | HTTP | Meaning                              |
|------------------------|------|--------------------------------------|
| `unauthorized`         | 401  | Missing or invalid bearer token      |
| `forbidden`            | 403  | Target not in allowlist              |
| `tool_not_found`       | 404  | Tool name not registered             |
| `tool_disabled`        | 403  | Tool registered but not enabled      |
| `validation_failed`    | 422  | Bad envelope or tool arguments       |
| `resource_not_found`   | 404  | Target resource doesn't exist        |
| `conflict`             | 409  | Entry already exists                 |
| `rate_limited`         | 429  | Too many requests — back off         |
| `confirmation_required`| 200  | Confirmation token issued            |
| `execution_failed`     | 500  | Tool error — retry or report         |
| `internal_error`       | 500  | Server error — retry or report       |

## Constraints

- `data` must be a JSON object, never an array or primitive.
- Unknown argument keys are rejected with `validation_failed`.
- Default rate limit: 30 req/min on execute, 60 req/min on capabilities.
- Default max request body: 64KB.
- Each site has independent config — capabilities, allowlists, rate limits, and tokens are all per-site.


path: INSTALL.md
content: # AI Gateway — Installation & Setup

## Prerequisites

You need `curl` and `jq` available in your environment:

```bash
curl --version
jq --version
```

If missing:
- macOS: `brew install curl jq`
- Ubuntu/Debian: `apt install curl jq`

## Create the Site Registry

The skill uses a `sites.json` file to store credentials for each Statamic site you manage. Create it at the default location:

```bash
mkdir -p ~/.config/ai-gateway
```

```bash
cat > ~/.config/ai-gateway/sites.json << 'EOF'
{
  "sites": {
    "marketing": {
      "base_url": "https://marketing.example.com",
      "token": "your-marketing-site-token"
    }
  }
}
EOF
```

Protect the file since it contains secrets:

```bash
chmod 600 ~/.config/ai-gateway/sites.json
```

To use a different path, set the `AI_GATEWAY_SITES_CONFIG` environment variable:

```bash
export AI_GATEWAY_SITES_CONFIG="/path/to/your/sites.json"
```

## Site Registry Format

```json
{
  "sites": {
    "site-name": {
      "base_url": "https://your-site.com",
      "token": "the-AI_GATEWAY_TOKEN-for-this-site"
    }
  }
}
```

| Field      | Description                                    | Example                              |
|------------|------------------------------------------------|--------------------------------------|
| key        | Short name you'll use to reference this site   | `marketing`, `docs`, `portal`        |
| `base_url` | The site's URL, no trailing slash              | `https://marketing.example.com`      |
| `token`    | The `AI_GATEWAY_TOKEN` configured on that site | `a1b2c3d4e5f6...`                    |

The site operator provides both the base URL and token. Each Statamic site has its own unique token.

If you're also the site operator and need to generate a token:

```bash
openssl rand -hex 32
```

Set the output as `AI_GATEWAY_TOKEN` in the site's `.env` and as the `token` value in your `sites.json`.

## Adding Multiple Sites

Add as many sites as you need:

```json
{
  "sites": {
    "marketing": {
      "base_url": "https://marketing.example.com",
      "token": "token-aaa..."
    },
    "docs": {
      "base_url": "https://docs.example.com",
      "token": "token-bbb..."
    },
    "portal": {
      "base_url": "https://portal.example.com",
      "token": "token-ccc..."
    }
  }
}
```

Each site is fully independent — its own token, its own enabled tools, its own allowlists, its own rate limits. A `403 forbidden` on one site doesn't mean the same target is forbidden on another.

## First-Run Smoke Test

Pick a site and verify the connection:

```bash
# Read credentials for the "marketing" site
SITE_URL=$(jq -r '.sites.marketing.base_url' ~/.config/ai-gateway/sites.json)
SITE_TOKEN=$(jq -r '.sites.marketing.token' ~/.config/ai-gateway/sites.json)

# Check capabilities
curl -s -H "Authorization: Bearer $SITE_TOKEN" \
  $SITE_URL/ai-gateway/capabilities | jq .
```

### Expected: success

A JSON response with `ok: true` listing each tool's `enabled` state, `target_type`, and `requires_confirmation` flag. The exact tools vary per site.

### Troubleshooting

| Response           | Meaning                              | Fix                                                  |
|--------------------|--------------------------------------|------------------------------------------------------|
| `401 unauthorized` | Token is wrong or missing            | Check the `token` in `sites.json` matches the site's `.env` |
| `404` (HTML)       | Addon is not enabled on the site     | Site operator needs `AI_GATEWAY_ENABLED=true`        |
| Connection refused | Site is unreachable                  | Check `base_url` in `sites.json`                     |
| `429 rate_limited` | Too many requests                    | Wait a minute and retry                              |

### Test each site

Repeat the smoke test for every site in your registry. Each site may have different capabilities:

```bash
for site in $(jq -r '.sites | keys[]' ~/.config/ai-gateway/sites.json); do
  url=$(jq -r ".sites.$site.base_url" ~/.config/ai-gateway/sites.json)
  tok=$(jq -r ".sites.$site.token" ~/.config/ai-gateway/sites.json)
  echo "--- $site ($url) ---"
  curl -s -H "Authorization: Bearer $tok" $url/ai-gateway/capabilities | jq '.result.capabilities | to_entries[] | select(.value.enabled) | .key'
done
```

## Test a Write Operation

Once capabilities look good, test a simple upsert on one site:

```bash
SITE_URL=$(jq -r '.sites.marketing.base_url' ~/.config/ai-gateway/sites.json)
SITE_TOKEN=$(jq -r '.sites.marketing.token' ~/.config/ai-gateway/sites.json)

curl -s -X POST \
  -H "Authorization: Bearer $SITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "entry.upsert",
    "arguments": {
      "collection": "pages",
      "slug": "setup-test",
      "data": { "title": "Setup Test" }
    },
    "request_id": "marketing:install-smoke-test"
  }' \
  $SITE_URL/ai-gateway/execute | jq .
```

Expected: `ok: true` with `status: "created"` or `status: "updated"`.

If you get `403 forbidden`, the collection isn't in that site's allowlist — ask the site operator to add it.

## Token Rotation

To rotate a token for a single site, update the `token` value in `sites.json` and update the corresponding `AI_GATEWAY_TOKEN` in that site's `.env`. Other sites are unaffected.

## You're Ready

Once the smoke tests pass, the skill is operational. Refer to [SKILL.md](./SKILL.md) for the quick reference and [references/api.md](./references/api.md) for full endpoint documentation.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)