syncro: expand skill to full Syncro API surface (assets/RMM/deploy) + verified capability model

Add comprehensive Syncro coverage beyond PSA core:
- New .claude/standards/syncro/api-reference.md: complete verified inventory of ~180
  endpoints across 38 resource types (generated from live OpenAPI 3.0 spec + tenant
  probe 2026-07-10), with worked GET/POST/PUT/DELETE templates and token-capability matrix.
- /syncro: asset read intelligence (patches, installed_applications), asset create/update,
  policy-folder move (move-asset), RMM alerts, and deploy-agent (hybrid installer push via
  GuruRMM using SyncroSetup --console --allow-force-reboot).
- move-asset ships a capability preflight (GET /policy_folders?customer_id -> 401 = missing
  Assets-Policy-Change) + mandatory post-write verify, because an under-scoped token returns
  HTTP 200 and silently no-ops the move.

Correct the "Syncro RMM is API-impossible" belief: it was a token-scope gap, not an API
limit. Live-verified the asset move (flip-and-restore 692253->692278->692253). Token scope
today: Howard + Winter full; Mike (vaulted ...ebbeb3) still 401 pending re-vault.

Corrects memory reference-syncro-rmm-api-gui-only; correction logged to errorlog.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 15:45:26 -07:00
parent 46d2ae7a5b
commit 1f56098652
5 changed files with 629 additions and 26 deletions

View File

@@ -18,6 +18,12 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
/syncro estimate <customer> <subject> Create ticket + linked estimate with line items and private purchase notes
/syncro schedules <customer> List recurring invoice schedules for a customer
/syncro schedule <id> View a schedule's template and line items
/syncro assets <customer> List a customer's RMM assets (read)
/syncro asset <id> Asset detail + patches + installed applications
/syncro asset-update <id> Rename / fix serial / edit an asset (preview-gated)
/syncro move-asset <id> <folder> Move asset to a policy folder (needs token scope - see below)
/syncro rmm-alerts [<customer>] List live RMM alerts; mute/clear by id
/syncro deploy-agent <customer> <host> Push the Syncro RMM agent to a machine via GuruRMM
```
## API Configuration
@@ -26,6 +32,10 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
**API Key:** per-user tokens in SOPS vault — see "Get API key" below
**Rate limit:** 180 requests/minute per IP
**Docs:** https://api-docs.syncromsp.com/
**Full endpoint catalog:** `.claude/standards/syncro/api-reference.md` — every Syncro call
(~180 endpoints, 38 resource types), verified against the live spec + tenant 2026-07-10.
This file documents the core PSA + asset/RMM/agent-deploy workflows; the reference covers
the rest (contacts, contracts, leads, vendors, POs, products, payments, wiki, policy folders).
## Hard Rules (violations have occurred — no exceptions)
@@ -599,26 +609,156 @@ COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id')
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
#### Customer Assets (retire / archive — API GAP)
#### Customer Assets (RMM) — read intelligence + write management
**Retiring a device in Syncro = "Archive" (UI ONLY — no REST API).** Verified against
Syncro's own docs 2026-07-08 (`docs.syncrosecure.com/assets-rmm/archive-assets`): archiving
is done in the web UI only — asset Details page → **Actions → Archive**, or bulk via the
Assets table checkboxes → **Bulk Actions → Archive**. There is **no** archive endpoint and
**no** `archived` field on `PUT /customer_assets/{id}`. Do NOT try to archive via API, and
do NOT substitute `DELETE /customer_assets/{id}` — delete is destructive/irreversible and
loses history + breaks ticket linkages (Archive keeps the asset visible on its tickets/alerts).
Assets are Syncro's RMM device records. Reads are free; writes follow the standard
preview+confirm gate. Response shape is `{"asset": {...}}` (single) / `{"assets": [...]}`
(list). RMM telemetry lives under `.asset.properties.kabuto_information`.
- **To retire assets: hand the user the asset list + the Bulk-Actions-→-Archive steps.** We
cannot do it for them via API.
- Asset READS are fine: `GET /customer_assets?customer_id=N[&query=<name>]` (list, 100/page,
`.assets[]`) and `GET /customer_assets/{id}` (detail; `.asset` — RMM data lives under
`.asset.properties.kabuto_information`). `updated_at` is NOT a reliable "last online" (bulk
account touches move it) — use the ScreenConnect `GuestInfoUpdateTime` or kabuto data for
real last-seen.
- **[RESEARCH]** Re-check if Syncro ships an archive API (watch release notes). If/when it
does, wire it in here and add `--confirm` gating. Until then this is a "cannot be performed
via skill" function. (First hit: IMC retirement pass 2026-07-08.)
**Reads (verified with per-user token 2026-07-10):**
```bash
# List / search a customer's assets (100/page, .assets[])
curl -s "${BASE}/customer_assets?customer_id=${CUST_ID}&per_page=100&api_key=${API_KEY}" | \
tr -d '\000-\037' | jq -r '.assets[] | "\(.id)\t\(.asset_type_name)\t\(.name)"'
# Asset detail
curl -s "${BASE}/customer_assets/${ASSET_ID}?api_key=${API_KEY}" | tr -d '\000-\037' | \
jq '{id:.asset.id, name:.asset.name, customer_id:.asset.customer_id, policy_folder_id:.asset.policy_folder_id, serial:.asset.asset_serial}'
# Windows patch posture -> {available_patches, available_patches_meta, installed_patches}
curl -s "${BASE}/customer_assets/${ASSET_ID}/patches?api_key=${API_KEY}" | tr -d '\000-\037' | \
jq '{available: (.available_patches|length), installed: (.installed_patches|length)}'
# Installed software inventory (paginated) -> {installed_applications, meta}
# Use this to VERIFY an install (e.g. after /syncro deploy-agent) — see Agent Deployment.
curl -s "${BASE}/customer_assets/${ASSET_ID}/installed_applications?per_page=100&api_key=${API_KEY}" | \
tr -d '\000-\037' | jq -r '.installed_applications[]?.name' | sort -u
```
`updated_at` is NOT a reliable "last online" (bulk account touches move it) — use the
ScreenConnect `GuestInfoUpdateTime` or kabuto data for real last-seen.
**Writes — `POST /customer_assets` (create), `PUT /customer_assets/{id}` (update).**
`name` is **required** on both (422 without it). Accepted body fields: `name`,
`asset_serial`, `asset_type_id`, `asset_type_name`, `customer_id`, `properties`. Preview the
full payload and confirm before writing; post a bot alert after. Response `{"asset": {...}}`.
```bash
# Update (rename / fix serial). Always resend name (required) even when only changing serial.
curl -s -X PUT "${BASE}/customer_assets/${ASSET_ID}?api_key=${API_KEY}" \
-H "Content-Type: application/json" --data-binary @- <<JSON
{"name": "${NEW_NAME}", "asset_serial": "${NEW_SERIAL}"}
JSON
```
**Move to a policy folder — `PUT /customer_assets/{id}` with `policy_folder_id` — WORKS, but
is TOKEN-SCOPE-GATED and MUST be capability-checked + verified.** Live-proven 2026-07-10 via
flip-and-restore on ACG-internal asset `12335235` (`692253 → 692278 → 692253`) with Howard's
full-scope token. The spec: *"Updating only `policy_folder_id` requires **Assets - Policy
Change**; with other fields requires **Assets - Edit** AND **Assets - Policy Change**. Nil,
nonexistent, or cross-customer folder IDs return 422."* `name` is required on the PUT, so a
move always sends other fields → needs BOTH scopes.
**The failure mode is SILENT, so never trust the 200.** A token WITHOUT Policy-Change returns
`HTTP 200` and simply does not change the folder (this is the symptom once misread as "RMM is
GUI-only"). `/me` reports coarse `asset:{write:true}` even without Policy-Change, so it cannot
detect this — you must probe and verify:
```bash
move_asset() { # $1=asset_id $2=target_folder_id (needs $BASE $API_KEY)
local AID="$1" TARGET="$2" NAME CUST CUR
local A; A=$(curl -s "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" | tr -d '\000-\037')
NAME=$(echo "$A" | jq -r '.asset.name'); CUST=$(echo "$A" | jq -r '.asset.customer_id')
CUR=$(echo "$A" | jq -r '.asset.policy_folder_id')
# 1) CAPABILITY PREFLIGHT — 200 = token can move; 401 = lacks Assets - Policy Change
local PC; PC=$(curl -s -o /dev/null -w '%{http_code}' "${BASE}/policy_folders?customer_id=${CUST}&api_key=${API_KEY}")
if [ "$PC" = "401" ]; then
echo "[ERROR] This token lacks 'Assets - Policy Change' — cannot move policy folders."
echo " Options: move it in the Syncro web console, OR have a user whose token has"
echo " the scope run it (do NOT silently swap keys — writes attribute to the owner)."
return 1
fi
# 2) PUT (name required), then 3) VERIFY the value actually changed — a 200 no-op = failure
curl -s -X PUT "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" -H "Content-Type: application/json" \
--data-binary "$(jq -nc --arg n "$NAME" --argjson pf "$TARGET" '{name:$n, policy_folder_id:$pf}')" >/dev/null
local NEW; NEW=$(curl -s "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" | tr -d '\000-\037' | jq -r '.asset.policy_folder_id')
if [ "$NEW" = "$TARGET" ]; then echo "[OK] asset ${AID} moved ${CUR} -> ${NEW}"; else
echo "[ERROR] move NOT applied (still ${NEW}) — token likely under-scoped; do it in the console."; return 1; fi
}
```
**Policy-folder route quirks (verified 2026-07-10):** `GET /policy_folders` (bare) → **404**;
`GET /policy_folders?customer_id=N` → **200** (list a customer's folders — how you enumerate
folder IDs); `GET /policy_folders/{id}` → **401** even with full scope. Find valid same-customer
folder IDs from the `?customer_id=N` list or from other assets' `policy_folder_id` values.
**Fleet posture:** Howard's per-user token now has full scope (verified). Other per-user tokens
may not — the preflight above handles that per-run. Recommend granting every per-user Syncro
token the Policy-Change scope so `move-asset` behaves the same regardless of who runs it (Mike's
call — least-privilege alternative is to leave the preflight to degrade gracefully). Corrects
memory `reference-syncro-rmm-api-gui-only`; full detail in `.claude/standards/syncro/api-reference.md`.
**Archive (retire) is still GUI-only — no REST API.** Verified 2026-07-08: archiving is web
UI only (asset Details → **Actions → Archive**, or Assets table → **Bulk Actions →
Archive**). There is no archive endpoint and no `archived` field on the PUT. Do NOT substitute
`DELETE /customer_assets/{id}` — delete is destructive/irreversible, loses history, and breaks
ticket linkages (Archive keeps the asset on its tickets/alerts). To retire: hand the user the
asset list + the Bulk-Actions-→-Archive steps.
#### RMM Alerts
Live RMM alert feed (`GET /rmm_alerts` -> `{rmm_alerts, meta}`, verified 2026-07-10). Reads
free; `POST /rmm_alerts/{id}/mute` and `DELETE /rmm_alerts/{id}` (clear) are gated writes.
```bash
curl -s "${BASE}/rmm_alerts?per_page=50&api_key=${API_KEY}" | tr -d '\000-\037' | \
jq -r '.rmm_alerts[] | "\(.id)\t\(.created_at)\t\(.description // .kind)"'
# Mute: POST /rmm_alerts/${ALERT_ID}/mute | Clear: DELETE /rmm_alerts/${ALERT_ID}
```
#### Agent Deployment (`/syncro deploy-agent`) — hybrid via GuruRMM
Push the Syncro RMM agent to a machine. Syncro's API has **no** installer-download endpoint,
so this is a **hybrid**: the per-policy-folder installer (which embeds the customer enrollment
token) is obtained once from the Syncro GUI and vaulted; **GuruRMM (`/rmm`)** does the actual
push (it runs PowerShell as SYSTEM on the endpoint). `/rmm diagnose` already *detects* a
Syncro agent as a competitor-RMM check — this is the deploy half.
**Prerequisites (one-time per customer):**
1. In Syncro: **Assets & RMM → Downloads** (or the target Policy Folder) → copy the
`SyncroSetup-<company>-<token>.exe` **download URL**. It is per-policy-folder — the token
binds the installed agent to that customer + policy. Do NOT reuse one customer's URL for
another.
2. Vault it via the `vault` skill at `clients/<slug>/syncro-agent-installer.sops.yaml`
(`credentials.installer_url`). Never hardcode the token URL.
**Deploy workflow:**
1. Resolve the target host in GuruRMM (`/rmm` — resolve hostname → UUID; confirm `os_type`
is `windows` and it is `is_connected`). Confirm the customer↔host mapping with the user.
2. Preview the exact install command + target, get explicit confirmation.
3. Dispatch via GuruRMM (`command_type: powershell`, SYSTEM context). The agent installs
**silently by default**; `--console` runs it non-interactively and `--allow-force-reboot`
permits the reboot Syncro triggers if it must update .NET first:
```powershell
$url = '<vaulted SyncroSetup URL>'
$exe = "$env:TEMP\SyncroSetup.exe"
Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing
Start-Process -FilePath $exe -ArgumentList '--console','--allow-force-reboot' -Wait
```
(Refs: Syncro deploy docs `docs.syncrosecure.com/agents-alerts-automations/deploy-an-endpoint`;
community "Command Line install".) `--allow-force-reboot` CAN reboot the endpoint — treat
deploy-agent as an outward-facing action: confirm reboot tolerance with the user first.
4. **Verify** the agent enrolled: after ~2-3 min, either re-query GuruRMM output, or confirm
the new asset appears under the customer via `GET /customer_assets?customer_id=N&query=<host>`
and/or that "Syncro" shows in the endpoint's `installed_applications`.
5. Bot alert (RMM write → `[RMM]`/`[DEPLOY]` routes to #dev-alerts; Syncro-side note to
#bot-alerts if an asset record was created).
**[VERIFY before first production use]** Confirm `--console --allow-force-reboot` against the
current Syncro installer on one ACG-internal test machine before rolling to a customer — the
switches are stable historically but the installer is versioned.
#### Customers