Files
claudetools/.claude/standards/syncro/test-findings.md

10 KiB

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 REQUIREDnull{"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 APIPUT /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.