163 lines
11 KiB
Markdown
163 lines
11 KiB
Markdown
# Syncro Skill — Live Test Findings (running log)
|
|
|
|
Verified behaviors/gotchas discovered while testing `/syncro` against the live tenant on the
|
|
**Howard Test** sandbox customer (`customer_id 36118743`) + RMM-TEST-MACHINE. Newest on top.
|
|
Each entry is a rule the skill must not re-learn. Critical ones are also folded into
|
|
`syncro.md` (operating manual) and `api-reference.md` (breadth reference).
|
|
|
|
## 2026-07-10 — Sandbox cleanup (DELETE shapes + agent uninstall)
|
|
|
|
**`DELETE /customers/{id}` DISABLES instead of deleting when the customer has associated data**
|
|
→ `{"message":"Customer was disabled instead of deleted because they have associated data."}`
|
|
and a subsequent GET still returns 200 (disabled, not 404). Associated data that blocks a hard
|
|
delete includes assets + leads (both GUI-only to remove). To truly delete a customer: clear its
|
|
asset (GUI archive) + lead first, then DELETE.
|
|
|
|
**DELETE response shapes (verified):** invoice → `{"message":"<id>: We deleted # <num>. "}`;
|
|
estimate → same form; appointment → `{"message":"deleted"}`; schedule → `{"success":"deleted"}`;
|
|
contract → `{"success":true}`; ticket → `{"message":"Ticket was successfully deleted. "}`;
|
|
contact → `{"success":true}`.
|
|
|
|
**Uninstalling the Syncro agent from an endpoint (via GuruRMM):** find the Uninstall reg entry
|
|
whose `DisplayName -match 'Syncro'`, then `msiexec /x {ProductCode} /qn /norestart` (Syncro
|
|
registers a clean MSI product code). Verified on RMM-TEST-MACHINE: service gone, process gone,
|
|
0 reg entries, `C:\Program Files\RepairTech\Syncro` removed. Candidate future `deploy-agent
|
|
--uninstall` action. The Syncro-side asset then goes stale and must be archived in the GUI
|
|
(no asset DELETE endpoint).
|
|
|
|
## 2026-07-10 — Syncro API has NO remote device-control endpoint
|
|
|
|
**You cannot run commands / control an endpoint through the Syncro public API.** Searched all
|
|
116 paths for `script|command|execute|run|reboot|remote|action|task|shutdown|terminal` — none
|
|
exist. Syncro's live device control (Run Script, Reboot, Live Terminal, background tools) is
|
|
**GUI-only**. The `script.execute` token scope governs Syncro's *internal* scripting library
|
|
(GUI/policy/automated-remediation triggered), NOT an on-demand API command runner. For an
|
|
enrolled asset the API gives **read + manage only** (detail, patches, installed_applications,
|
|
rmm_alerts, rename/serial/policy-folder move) — no execution. **Execution goes through GuruRMM
|
|
(`/rmm`), not `/syncro`.** Do NOT probe guessed exec endpoints (junk-record risk).
|
|
|
|
## 2026-07-10 — Phase 2 (deploy-agent + asset/RMM on a REAL device)
|
|
|
|
**`deploy-agent` VERIFIED end-to-end on RMM-TEST-MACHINE.** Read the vaulted Howard Test MSI URL
|
|
→ pushed via GuruRMM (`msiexec /i "<msi>" /qn /norestart`, SYSTEM context, via `ps-encoded.sh`)
|
|
→ `msiexec` exit 0, service **`Syncro` Running** → the machine **auto-enrolled as a Syncro asset
|
|
under the correct client + the policy folder embedded in the installer token** (`5092561`) within
|
|
~1 min. Asset `12676769`. Downloaded ~7.3 MB MSI. This is the whole reason the installer URL is
|
|
per-policy-folder: the token binds the enrollment to that client + folder.
|
|
|
|
- **`installed_applications` populates within ~1 min** of enroll; **`patches` lag** (scan
|
|
pending → `0/0` immediately after enroll). Both endpoints work; patch data just isn't instant.
|
|
- **`POST /policy_folders` → `{"policy_folder":{...}}`** (`.policy_folder.id`). **`parent_id` is
|
|
REQUIRED** — `null` → `{"errors":["Parent is required"]}`. Create a client subfolder with
|
|
`parent_id` = the client's top-level folder id. `DELETE /policy_folders/{id}` works (empty body).
|
|
- **`move-asset` (PUT `policy_folder_id`) VERIFIED on the real asset**: `5092561 → 5092583 →
|
|
5092561`, both confirmed by GET. Needs a full-scope token (howard/winter have it).
|
|
- **`GET /rmm_alerts` works.** `ps-encoded.sh`/PowerShell returns CLIXML progress records on
|
|
stderr even on success — `exit_code == 0` is the truth; ignore the CLIXML noise.
|
|
|
|
## 2026-07-10 — Phase 2 prep: RMM installer URL (structure decoded)
|
|
|
|
**The Syncro RMM installer URL is a base64 token that partly encodes API-derivable IDs.**
|
|
A working install link is `https://rmm.syncromsp.com/dl/msi/<b64>` where `<b64>` base64-decodes
|
|
to `v1-{customer_id}-{A}-{B}-{policy_folder_id}`. For Howard Test:
|
|
`djEt...` → `v1-36118743-1819842526-48753-5092561` (customer_id 36118743, policy_folder_id
|
|
5092561 — both pulled from the API; `A=1819842526`, `B=48753` are NOT on the customer or
|
|
policy-folder objects). Host is `rmm.syncromsp.com` (Kabuto/RMM), separate from the PSA API.
|
|
|
|
- **RESOLVED (4-client sample):** `B=48753` is CONSTANT (our Syncro account id). `A` VARIES per
|
|
client (1819842526 / 1610167344 / 1552910968 / 1520216784) and is **not** `kabuto_customer_id`
|
|
nor anywhere in the customer/policy-folder objects — an unguessable per-client security token
|
|
(prevents URL forgery from sequential customer_ids). **Therefore the URL is NOT constructible**
|
|
— it must be harvested per client and vaulted as a secret. `customer_id` + `policy_folder_id`
|
|
ARE API-readable, but `A` is the blocker. Vaulted so far: cascades-tucson, grabb-durando,
|
|
reliant, howard-test at `clients/<slug>/syncro-agent-installer.sops.yaml`
|
|
(`credentials.installer_url`). `deploy-agent` reads from there.
|
|
- **Installer is an MSI** (`/dl/msi/`), so silent deploy uses `msiexec /i <file>.msi /qn
|
|
/norestart` (NOT the `.exe --console` path). Syncro also offers a `.exe` (`--console
|
|
--allow-force-reboot`); the download link determines which. Verify the exact MSI silent
|
|
behavior on RMM-TEST-MACHINE before any customer push.
|
|
- The token embeds an enrollment secret → it is a **credential**: vault it (per client), never
|
|
commit plaintext. `policy_folder_id` comes from `GET /policy_folders?customer_id=N`.
|
|
|
|
## 2026-07-10 — Phase 1b (breadth: leads / vendors / products / PO / worksheets)
|
|
|
|
**Response shapes — these are all WRAPPED (unlike contacts/contracts):** `POST /leads` →
|
|
`{"lead":{...}}` (`.lead.id`); `POST /vendors` → `{"vendor":{...}}`; `POST`/`PUT /products` →
|
|
`{"product":{...}}`; `POST /purchase_orders` → `{"purchase_order":{...}}`;
|
|
`POST /purchase_orders/{id}/create_po_line_item` → `{"po_line_item":{...}}`;
|
|
`POST /tickets/{id}/worksheet_results` → `{"worksheet_result":{...}}`;
|
|
`DELETE .../worksheet_results/{id}` → `{"message":"N: We deleted <title>."}`.
|
|
|
|
**PO line items require the product to have `maintain_stock:true`.** `create_po_line_item`
|
|
against a non-stock product returns `{"errors":"...ensure that the item ... is set to
|
|
'Maintain Stock'."}`. Flip the product (`PUT /products/{id}` `maintain_stock:true`) first.
|
|
|
|
**Worksheet templates are NOT a standalone endpoint — they live in `GET /tickets/settings` →
|
|
`.worksheet_templates[]`** (`{id,title,account_id}`). You need a valid `worksheet_template_id`
|
|
to create a worksheet result (e.g. "Virus Removal" = 11341).
|
|
|
|
**`PUT /products/{id}` requires `name` AND `description`** (same as create) — 422 without.
|
|
|
|
**No DELETE endpoint for leads, vendors, products, purchase_orders** — test records in those
|
|
GLOBAL catalogs PERSIST (can't be removed via API; archive/hide in the GUI). Mark test items
|
|
`[TEST]` and set products `disabled:true`. Created this run (need GUI cleanup): lead 43492446,
|
|
vendor 809317, product 23639304 (disabled), PO 2101221 (+ line 8918508).
|
|
|
|
**Minor:** `POST /leads` `business_name` didn't persist (came back null); `first_name`/
|
|
`last_name`/`email`/`customer_id` did. Re-check the correct field for a lead's company name.
|
|
|
|
## 2026-07-10 — Phase 1 (PSA CRUD)
|
|
|
|
**Emailing an invoice/estimate returns 200 "Email sent" even when it delivered to NOBODY.**
|
|
`POST /invoices/{id}/email` and `POST /estimates/{id}/email` take **no recipient param** —
|
|
Syncro picks the recipient from the record server-side. If `customer.email` is null (and no
|
|
contact is a flagged recipient), the API still returns `{"message":"Email sent"}` HTTP 200 but
|
|
nothing is sent. **NEVER report an email as delivered based on the 200 alone.** Ensure the
|
|
customer has a valid `email` (or a recipient contact) BEFORE emailing, and confirm real
|
|
delivery out-of-band when it matters.
|
|
|
|
**`customer.email` is UNIQUE across the tenant.** `PUT /customers/{id}` with an email already
|
|
used elsewhere returns `{"success":false,"message":["Email has already been taken"]}` and does
|
|
NOT set it. For a test customer that must reach a real person whose address is already in
|
|
Syncro, use plus-addressing (`user+tag@domain`) — unique in Syncro, still routes to the inbox
|
|
IF the mail tenant has plus-addressing enabled (M365: off by default in some tenants — verify).
|
|
|
|
**`PUT /customers/{id}` uses two different response shapes — always check for the error shape.**
|
|
Success → `{"customer":{...}}`. Validation failure → `{"success":false,"message":[...]}` with
|
|
the field unchanged. Parsing only `.customer.email` hides the failure (looks like a silent
|
|
no-op). Read the raw body / check `.success == false` on customer writes.
|
|
|
|
**Contacts return a FLAT object, not wrapped.** `POST /contacts` and `PUT /contacts/{id}`
|
|
return `{"id":N,"name":...,"email":...}` directly — NOT `{"contact":{...}}`. Parse `.id`, not
|
|
`.contact.id`. (The wrapped-parse returns null and looks like a failure when the write
|
|
succeeded — a blind retry then DUPLICATES the contact. GET-verify, never retry.)
|
|
|
|
**Contact recipient flags (`primary`, `receives_invoices`, `receives_reports`) are NOT settable
|
|
via the API** — `PUT /contacts/{id}` accepts them in the body but silently ignores them (stay
|
|
null). To designate an invoice-recipient contact, set it in the Syncro GUI.
|
|
|
|
**`POST /contracts` write succeeds but the response does not echo the created object** (fields
|
|
parse null). GET `/contracts` and match to confirm creation — never retry on the null.
|
|
|
|
**`GET /contracts?customer_id=N` does NOT filter server-side** — it returns ALL contracts.
|
|
Filter client-side by `.customer_id`.
|
|
|
|
**Reinforces the standing rule:** null/blank fields in a Syncro write response are NOT proof of
|
|
failure. Syncro has no idempotency, so a blind retry duplicates. Always GET the resource to
|
|
confirm true state before any follow-up action.
|
|
|
|
### Verified-working this phase (no gotcha, for coverage record)
|
|
POST/GET customer; POST contact; POST ticket (+ number); POST ticket comment (hidden);
|
|
POST add_line_item; POST invoice (from ticket); POST estimate + line_items + PUT recalc;
|
|
POST appointment; POST schedule (paused). All parse under the shapes documented in `syncro.md`.
|
|
|
|
## 2026-07-13 — Invoice line population is ASYNCHRONOUS after POST /invoices
|
|
`POST /invoices` returns immediately, but the invoice's `line_items` are populated from the
|
|
ticket's unbilled charges in the background. A `GET /invoices/{id}` fired in the same second
|
|
can return a PARTIAL line set (observed: 10 of 17 lines / 18.0h of 28.0h on ACG-internal
|
|
#32546, invoice 68040; a re-GET moments later showed all 17 / 28.0h). Verified the ticket held
|
|
all 17 lines throughout. Rule: if the post-invoice verification shows fewer lines than the
|
|
ticket, WAIT and re-GET before concluding lines were dropped — do NOT create a second invoice
|
|
(it would double-bill the late-arriving lines). Large line counts (17) make the window visible;
|
|
the usual 1-2 line invoices never showed it.
|