Rebrand: rename ondeck->horizon in package.json, docker-compose, SERVER.md, docs; production stack at /opt/stacks/horizon

This commit is contained in:
lorentz 2026-04-12 23:59:20 +00:00
parent 77f23e12cd
commit bbdbe1ddc0
8 changed files with 464 additions and 46 deletions

View file

@ -29,7 +29,12 @@
"Read(//usr/lib/**)",
"Bash(ls /usr/bin/pg*)",
"Bash(dpkg -l)",
"Bash(docker exec dev-db:*)"
"Bash(docker exec dev-db:*)",
"Skill(pangolin)",
"Bash(curl -s 'https://api.pangolin.wulfconsulting.cloud/v1/org/seubert-and-associates/domains?limit=1000' -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/target/48 -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rules -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
"Bash(curl -s -X PUT https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rule -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62' -H 'Content-Type: application/json' -d '{:*)"
]
}
}

View file

@ -0,0 +1,387 @@
---
name: pangolin
description: Manage Pangolin reverse proxy — sites, resources (HTTP/TCP services), targets (backends), domains, roles, users, clients, and access control via the Pangolin Integration API. Use when the user asks about their Pangolin tunnels, reverse proxy resources, exposed services, or site connectivity.
argument-hint: [action] [resource] [options]
allowed-tools: Bash(curl *) Read
---
# Pangolin API Skill
Manage a self-hosted Pangolin tunneled reverse proxy via the Integration API.
## Authentication
The API key is stored in `PANGOLIN_API_KEY` and base URL in `PANGOLIN_URL`.
```
Authorization: Bearer $PANGOLIN_API_KEY
```
The key format is `{apiKeyId}.{apiKeySecret}`.
## Base URL
```
https://api.$PANGOLIN_URL/v1
```
The Integration API lives at `api.` prefixed to the Pangolin domain, path prefix `/v1`.
## Standard Headers
```bash
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json"
```
## How to Make Requests
```bash
curl -s "https://api.$PANGOLIN_URL/v1/org/$PANGOLIN_ORG/sites" \
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json" | jq .
```
## Organization
The org ID for this instance is: **`wulf-consulting`**
Always use `PANGOLIN_ORG="wulf-consulting"` in requests.
## Response Format
```json
{
"data": {
"resources": [ ... ],
"pagination": {
"total": 42,
"limit": 25,
"offset": 0
}
},
"success": true,
"error": false,
"message": "Human-readable message",
"status": 200
}
```
Always check the `success` field.
## Pagination
List endpoints return a `pagination` object inside `data` with `total`, `limit`, and `offset` fields. The default limit is typically 25 or 1000 depending on the endpoint.
To paginate, add `?limit=N&offset=N` query parameters:
```bash
# First page
curl -s "$BASE/org/$ORG/resources?limit=25&offset=0" ...
# Second page
curl -s "$BASE/org/$ORG/resources?limit=25&offset=25" ...
```
**Always check `pagination.total` against the number of items returned.** If `total` exceeds the current page size, fetch additional pages until all items are retrieved. Example pattern:
```bash
# Fetch all pages of a resource list
OFFSET=0
LIMIT=100
while true; do
RESP=$(curl -s "$BASE/org/$ORG/resources?limit=$LIMIT&offset=$OFFSET" \
-H "Authorization: Bearer $PANGOLIN_API_KEY")
# Process items from this page...
TOTAL=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['pagination']['total'])")
OFFSET=$((OFFSET + LIMIT))
if [ $OFFSET -ge $TOTAL ]; then break; fi
done
```
When listing resources, sites, targets, users, etc., always use `?limit=1000` to minimize round trips, and paginate if `total` exceeds the returned count.
## Important: HTTP Method Conventions
Pangolin uses non-standard HTTP method semantics:
- **PUT** = Create a new resource
- **POST** = Update an existing resource
- **DELETE** = Delete
- **GET** = Read/list
This is the opposite of typical REST conventions. Be careful!
## API Endpoints Reference
### Organizations
| Action | Method | Endpoint |
|--------|--------|----------|
| Check org ID | GET | `/org/checkId` |
| Create org | PUT | `/org` |
| List orgs | GET | `/orgs` (root key only) |
| Get org | GET | `/org/:orgId` |
| Update org | POST | `/org/:orgId` |
| Delete org | DELETE | `/org/:orgId` |
### Sites (Tunnel Endpoints)
| Action | Method | Endpoint |
|--------|--------|----------|
| List sites | GET | `/org/:orgId/sites` |
| Create site | PUT | `/org/:orgId/site` |
| Get site defaults | GET | `/org/:orgId/pick-site-defaults` |
| Get site by niceId | GET | `/org/:orgId/site/:niceId` |
| Get site by ID | GET | `/site/:siteId` |
| Update site | POST | `/site/:siteId` |
| Delete site | DELETE | `/site/:siteId` |
Site types: `"newt"` (tunnel client), `"wireguard"`, `"local"`
### Resources (Public HTTP/TCP Services)
| Action | Method | Endpoint |
|--------|--------|----------|
| List resources | GET | `/org/:orgId/resources` |
| List resource names | GET | `/org/:orgId/resource-names` |
| Create resource | PUT | `/org/:orgId/resource` |
| Create for site | PUT | `/org/:orgId/site/:siteId/resource` |
| Get resource | GET | `/resource/:resourceId` |
| Get by niceId | GET | `/org/:orgId/resource/:niceId` |
| Update resource | POST | `/resource/:resourceId` |
| Delete resource | DELETE | `/resource/:resourceId` |
| List site resources | GET | `/site/:siteId/resources` |
Create HTTP resource body:
```json
{
"name": "My App",
"subdomain": "app",
"http": true,
"protocol": "tcp",
"domainId": "domain-id",
"stickySession": false
}
```
Update resource fields: `name`, `subdomain`, `ssl`, `sso`, `blockAccess`, `enabled`, `emailWhitelistEnabled`, `domainId`, `stickySession`, `tlsServerName`, `setHostHeader`
### Targets (Backends/Upstreams)
| Action | Method | Endpoint |
|--------|--------|----------|
| List targets | GET | `/resource/:resourceId/targets` |
| Create target | PUT | `/resource/:resourceId/target` |
| Get target | GET | `/target/:targetId` |
| Update target | POST | `/target/:targetId` |
| Delete target | DELETE | `/target/:targetId` |
Target body:
```json
{
"siteId": 1,
"ip": "192.168.1.100",
"port": 8080,
"method": "round-robin",
"enabled": true
}
```
Health check fields: `hcEnabled`, `hcPath`, `hcScheme`, `hcInterval` (min 5s), `hcTimeout` (min 1s), `hcStatus`
### Resource Rules
| Action | Method | Endpoint |
|--------|--------|----------|
| List rules | GET | `/resource/:resourceId/rules` |
| Create rule | PUT | `/resource/:resourceId/rule` |
| Update rule | POST | `/resource/:resourceId/rule/:ruleId` |
| Delete rule | DELETE | `/resource/:resourceId/rule/:ruleId` |
### Resource Access Control
| Action | Method | Endpoint |
|--------|--------|----------|
| List roles | GET | `/resource/:resourceId/roles` |
| List users | GET | `/resource/:resourceId/users` |
| Set roles | POST | `/resource/:resourceId/roles` |
| Set users | POST | `/resource/:resourceId/users` |
| Add role | POST | `/resource/:resourceId/roles/add` |
| Remove role | POST | `/resource/:resourceId/roles/remove` |
| Add user | POST | `/resource/:resourceId/users/add` |
| Remove user | POST | `/resource/:resourceId/users/remove` |
| Set password auth | POST | `/resource/:resourceId/password` |
| Set pincode auth | POST | `/resource/:resourceId/pincode` |
| Set header auth | POST | `/resource/:resourceId/header-auth` |
| Set email whitelist | POST | `/resource/:resourceId/whitelist` |
| Get email whitelist | GET | `/resource/:resourceId/whitelist` |
| Add to whitelist | POST | `/resource/:resourceId/whitelist/add` |
| Remove from whitelist | POST | `/resource/:resourceId/whitelist/remove` |
### Access Tokens
| Action | Method | Endpoint |
|--------|--------|----------|
| Generate token | POST | `/resource/:resourceId/access-token` |
| Delete token | DELETE | `/access-token/:accessTokenId` |
| List org tokens | GET | `/org/:orgId/access-tokens` |
| List resource tokens | GET | `/resource/:resourceId/access-tokens` |
### Domains
| Action | Method | Endpoint |
|--------|--------|----------|
| List domains | GET | `/org/:orgId/domains` |
| Create domain | PUT | `/org/:orgId/domain` |
| Get domain | GET | `/org/:orgId/domain/:domainId` |
| Update domain | PATCH | `/org/:orgId/domain/:domainId` |
| Delete domain | DELETE | `/org/:orgId/domain/:domainId` |
| Get DNS records | GET | `/org/:orgId/domain/:domainId/dns-records` |
| Restart cert | POST | `/org/:orgId/domain/:domainId/restart` |
Domain types: `"ns"`, `"cname"`, `"wildcard"`
### Users
| Action | Method | Endpoint |
|--------|--------|----------|
| Create user | PUT | `/org/:orgId/user` |
| List users | GET | `/org/:orgId/users` |
| Get user | GET | `/org/:orgId/user/:userId` |
| Get by username | GET | `/org/:orgId/user-by-username` |
| Update user | POST | `/org/:orgId/user/:userId` |
| Remove user | DELETE | `/org/:orgId/user/:userId` |
### Roles
| Action | Method | Endpoint |
|--------|--------|----------|
| List roles | GET | `/org/:orgId/roles` |
| Create role | PUT | `/org/:orgId/role` |
| Get role | GET | `/role/:roleId` |
| Update role | POST | `/role/:roleId` |
| Delete role | DELETE | `/role/:roleId` |
### Clients (VPN / Olm Devices)
| Action | Method | Endpoint |
|--------|--------|----------|
| List clients | GET | `/org/:orgId/clients` |
| Create client | PUT | `/org/:orgId/client` |
| Get client | GET | `/client/:clientId` |
| Get by niceId | GET | `/org/:orgId/client/:niceId` |
| Update client | POST | `/client/:clientId` |
| Delete client | DELETE | `/client/:clientId` |
| Archive | POST | `/client/:clientId/archive` |
| Unarchive | POST | `/client/:clientId/unarchive` |
| Block | POST | `/client/:clientId/block` |
| Unblock | POST | `/client/:clientId/unblock` |
### Site Resources (Private / VPN-accessible)
| Action | Method | Endpoint |
|--------|--------|----------|
| List all | GET | `/org/:orgId/site-resources` |
| List for site | GET | `/org/:orgId/site/:siteId/resources` |
| Create | PUT | `/org/:orgId/site-resource` |
| Get | GET | `/site-resource/:id` |
| Update | POST | `/site-resource/:id` |
| Delete | DELETE | `/site-resource/:id` |
### Invitations
| Action | Method | Endpoint |
|--------|--------|----------|
| List invitations | GET | `/org/:orgId/invitations` |
| Create invite | POST | `/org/:orgId/create-invite` |
| Remove invite | DELETE | `/org/:orgId/invitations/:inviteId` |
### Blueprints (IaC)
| Action | Method | Endpoint |
|--------|--------|----------|
| Apply blueprint | PUT | `/org/:orgId/blueprint` |
| List blueprints | GET | `/org/:orgId/blueprints` |
| Get blueprint | GET | `/org/:orgId/blueprint/:blueprintId` |
### Audit Logs
| Action | Method | Endpoint |
|--------|--------|----------|
| Query request logs | GET | `/org/:orgId/logs/request` |
| Export request logs | GET | `/org/:orgId/logs/request/export` |
| Query analytics | GET | `/org/:orgId/logs/analytics` |
### API Keys (Org-scoped)
| Action | Method | Endpoint |
|--------|--------|----------|
| List keys | GET | `/org/:orgId/api-keys` |
| Create key | PUT | `/org/:orgId/api-key` |
| Get key | GET | `/org/:orgId/api-key/:apiKeyId` |
| Delete key | DELETE | `/org/:orgId/api-key/:apiKeyId` |
| List actions | GET | `/org/:orgId/api-key/:apiKeyId/actions` |
| Set actions | POST | `/org/:orgId/api-key/:apiKeyId/actions` |
### Identity Providers
| Action | Method | Endpoint |
|--------|--------|----------|
| List IdPs | GET | `/idp` |
| Create OIDC IdP | PUT | `/idp/oidc` |
| Update IdP | POST | `/idp/:idpId/oidc` |
| Delete IdP | DELETE | `/idp/:idpId` |
### Health & Info
| Action | Method | Endpoint |
|--------|--------|----------|
| Health check | GET | `/` |
| Server info | GET | `/server-info` |
## Known Configuration
### Sites (10 total)
| ID | Name | Type | NiceId |
|----|------|------|--------|
| 1 | 215-node01 | newt | infamous-common-box-turtle |
| 2 | hq-dev01 | newt | great-scytodes-thoracica |
| 3 | Seubert-DC | newt | well-groomed-texas-coral-snake |
| 7 | kaercher5630 | newt | rapid-texas-lined-snake |
| 8 | WulfHQDev | newt | edible-forficula-auricularia |
| 9 | WulfHQProd | newt | yawning-campbells-dwarf-hamster |
| 10 | WulfExp | newt | flawed-indian-desert-jird |
| 11 | Seubert-HQ | newt | productive-chilean-rock-rat |
| 13 | wulfhqclaw | newt | flickering-common-box-turtle |
| 14 | robot01 | newt | impartial-savannah-forest-tree-frog |
### Domains
| ID | Domain | Type | Verified |
|----|--------|------|----------|
| domain1 | wulfconsulting.cloud | wildcard | yes |
| rkkbfx2qxp8bvi5 | wulf.cloud | wildcard | yes |
| 4zl519ktyb1acra | kaercherfamily.com | wildcard | yes |
## Common Workflows
### Check what's exposed publicly
1. List all resources: `GET /org/wulf-consulting/resources`
2. For each resource, list targets: `GET /resource/:resourceId/targets`
### Add a new service
1. Create resource: `PUT /org/wulf-consulting/resource` with subdomain, domain, name
2. Add target: `PUT /resource/:resourceId/target` with site, IP, port
3. Optionally configure SSO, whitelist, or password auth
### Check site connectivity
1. List sites: `GET /org/wulf-consulting/sites` — check `online` field
2. Get specific site: `GET /site/:siteId` for detailed status
### Manage access
1. Get resource: `GET /resource/:resourceId`
2. Set SSO: `POST /resource/:resourceId` with `{"sso": true}`
3. Add email whitelist: `POST /resource/:resourceId/whitelist/add` with `{"email": "..."}`
## Argument Handling
- `$ARGUMENTS` contains the full user request
- If no specific action given, show: sites (with online status), resources, and domains
- Parse natural language: "what's exposed", "list sites", "add service", "check tunnels"