diff --git a/.claude/SKILL_ROUTING.md b/.claude/SKILL_ROUTING.md index 2d780396..c7ab8104 100644 --- a/.claude/SKILL_ROUTING.md +++ b/.claude/SKILL_ROUTING.md @@ -23,7 +23,10 @@ | Run a command / investigate / script on an RMM agent | `/rmm` (find the host first with `rmm-search`) | | M365 investigation/remediation, breach check, mailbox/inbox-rule/forwarding audit, tenant sweep | `remediation-tool` | | Onboard a new M365 tenant to the remediation app suite | `onboard365` | -| Backblaze / B2 storage, buckets, backup storage cost | `b2` | +| Is client X backing up? backup status/failures/stale plans; MSP360 backup user/company/license provisioning | `msp360` (authoritative backup layer — check here, not bucket contents) | +| Backblaze / B2 storage, buckets, backup storage cost (destination side) | `b2` | +| Who backs up to Seafile / ownCloud / Datto Workplace (per-backend inventory + endpoint detection) | `seafile` / `owncloud` / `datto-workplace` | +| Provision/admin Seafile (SeaCloud) — create/deactivate/delete account, set quota, create/transfer/delete library, share/unshare | `seafile` (writes preview by default; `--confirm` to apply) | | Bitdefender / GravityZone AV — endpoints, sweeps, policies, quarantine, EDR | `bitdefender` | | Datto EDR / Datto AV — detections, isolate, scan, agent deploy | `datto-edr` | | VoIP — PacketDial / OITVOIP / NetSapiens domains, users, DIDs, queues, CDRs | `packetdial` | diff --git a/.claude/scripts/backup_common.py b/.claude/scripts/backup_common.py index e2a423ec..31bc34bf 100644 --- a/.claude/scripts/backup_common.py +++ b/.claude/scripts/backup_common.py @@ -24,6 +24,7 @@ import subprocess import sys import time import urllib.error +import urllib.parse import urllib.request from pathlib import Path from typing import Any, Optional @@ -93,18 +94,26 @@ def vault_get_field(entry: str, field: str) -> str: # --- minimal JSON HTTP -------------------------------------------------------- def http_json(method: str, url: str, *, headers: Optional[dict] = None, - body: Optional[dict] = None, timeout: float = HTTP_TIMEOUT, + body: Optional[dict] = None, form: Optional[dict] = None, + timeout: float = HTTP_TIMEOUT, basic: Optional[tuple[str, str]] = None) -> tuple[int, Any]: """Do an HTTP request and parse a JSON response. Returns (status, parsed). - 4xx/5xx are returned (not raised) so callers can inspect the error body. - Transport failures raise BackupError. + `body` sends a JSON payload; `form` sends application/x-www-form-urlencoded + (list values expand via doseq, e.g. repeated fields the Seafile admin share + endpoint reads with getlist). Pass at most one. 4xx/5xx are returned (not + raised) so callers can inspect the error body; transport failures raise. """ hdrs = dict(headers or {}) data = None + if body is not None and form is not None: + raise BackupError("http_json: pass body OR form, not both") if body is not None: data = json.dumps(body).encode("utf-8") hdrs.setdefault("Content-Type", "application/json") + elif form is not None: + data = urllib.parse.urlencode(form, doseq=True).encode("utf-8") + hdrs.setdefault("Content-Type", "application/x-www-form-urlencoded") if basic is not None: import base64 tok = base64.b64encode(f"{basic[0]}:{basic[1]}".encode()).decode("ascii") diff --git a/.claude/skills/owncloud/scripts/dry_test.sh b/.claude/skills/owncloud/scripts/dry_test.sh new file mode 100644 index 00000000..5fbb545e --- /dev/null +++ b/.claude/skills/owncloud/scripts/dry_test.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Non-destructive verification for the owncloud skill. +# - every WRITE command is run WITHOUT --confirm -> must refuse (rc 3), no SSH write +# - hard-fail guards are run (validation precedes the gate, so no execution) -> rc 2 +# - read-only commands must succeed (rc 0) and JSON reads must parse +# NOTHING here runs a state change on the server. +OC=".claude/skills/owncloud/scripts/owncloud.py" +pass=0; fail=0 +chk() { # chk