sync: auto-sync from GURU-5070 at 2026-06-15 11:20:33

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-15 11:20:33
This commit is contained in:
2026-06-15 11:20:52 -07:00
parent 5f4a2b1eca
commit 6e1c65877f
14 changed files with 585 additions and 7 deletions

View File

@@ -0,0 +1,25 @@
---
name: rmm-search
description: Cleanly find machines in the GuruRMM fleet with a flexible, client-aware search (no more grepping /api/agents and hitting the wrong client's box). Front door for locating an agent before acting on it via /rmm.
---
# /rmm-search — find GuruRMM machines on the first try
Thin entry point to the `rmm-search` skill. Engine: `.claude/scripts/rmm-search.sh`.
## Usage
```
/rmm-search <words...> [-c <client>] [--online] [--json] [-n N]
/rmm-search -c <client> List ALL machines for a client
/rmm-search --list-clients Show distinct client names
```
Every query word must match some field (hostname/client/site/OS/id), so words
**narrow**`hyperv valleywide` returns only Valley Wide's hyperv host, never
Dataforth's. Matching is normalized (case/space/hyphen-insensitive) with
prefix/substring/subsequence ranking; `-c` is an explicit hard client scope and
refuses to guess when the client name is ambiguous.
Use this to *find* an agent; pass the resulting hostname/id to `/rmm` to *act*.
Full matching rules + examples: `.claude/skills/rmm-search/SKILL.md`.

View File

@@ -12,6 +12,7 @@
- [Community Forum (Flarum)](reference_community_forum.md) — Flarum forum at community.azcomputerguru.com, API access, database, posting workflow.
- [Radio Show Website](reference_radio_website.md) — Astro static site at radio.azcomputerguru.com on IX server.
- [IX Server Access](reference_ix_server_access.md) — `ix.azcomputerguru.com` / 172.16.3.10. Reachable when Tailscale is on (no VPN). SSH currently uses sshpass with root password; key auth from GURU-5070 not configured yet (was CachyOS, now Win11 — verify).
- [Cloudflare access](reference_cloudflare_access.md) — Cloudflare API creds in SOPS `services/cloudflare.sops.yaml` (full DNS + account tokens; azcomputerguru zone_id 1beb9917...). azcomputerguru.com DNS is on Cloudflare (not IX) — edit via Cloudflare API, not whmapi1.
- [Matomo Analytics](reference_matomo_analytics.md) — Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites.
- [TickTick Integration](reference_ticktick_integration.md) — OAuth API integration, MCP server, SOPS vault creds, project/task CRUD.
- [Client Docs Structure](reference_client_docs_structure.md) — clients/<name>/docs/ layout (overview, network, servers, cloud, security, rmm). Template: clients/_client_template/.
@@ -39,6 +40,7 @@
- [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory.
- [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks.
- [DMARC rua INKY only when onboarded](feedback_dmarc_rua_inky_onboarded_only.md) — Don't point a client's DMARC rua at reports-sg.inkydmarc.com unless that client is onboarded to INKY (most aren't). Use plain `p=none` with no rua otherwise.
- [Use rmm-search to find machines](feedback_rmm_search_skill.md) — Find GuruRMM agents via the `rmm-search` skill (`rmm-search.sh <words> [-c client]`), never hand-grep /api/agents (it bleeds across clients). Then hand hostname/id to `/rmm`.
- [DM wrapped command lines to Mike](feedback_dm_wrapped_command_lines.md) — Long single-line output (consent links, URLs, one-liners) gets DM'd to Mike via the `discord-dm` skill so it's copy-pasteable, not terminal-wrapped. `discord-dm.sh mike "<link>"`.
- [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh.
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin.

View File

@@ -0,0 +1,12 @@
---
name: feedback-rmm-search-skill
description: Use the rmm-search skill to find GuruRMM machines, never grep /api/agents by hand
metadata:
type: feedback
---
To locate a machine/agent in GuruRMM, use the **`rmm-search`** skill (`bash .claude/scripts/rmm-search.sh <words> [-c <client>]`) — do NOT pull `/api/agents` and grep client-side.
**Why:** Hand-grepping bleeds across clients and picks the wrong box — e.g. searching `hyperv` returns both Valley Wide's and Dataforth's hyperv hosts, and it's easy to act on the wrong one. Mike built the UI Omnibox for this and asked for a CLI equivalent (2026-06-15). rmm-search treats every query word as a required filter across hostname/client/site/OS (so `hyperv valleywide` can only return Valley Wide's box), is normalized (case/space/hyphen-insensitive) with typo tolerance, and `-c <client>` hard-scopes (refuses to guess on ambiguous client names).
**How to apply:** `rmm-search.sh hyperv valleywide` or `... hyperv -c valleywide` to find; `--json | jq -r '.[0].id'` to get the agent id; then hand hostname/id to the [[reference_gururmm]] `rmm` skill to actually run commands. Online state is from last_seen (<5min), not the unreliable `is_connected` flag. Engine: `rmm-search.sh` + `rmm-search.py`.

View File

@@ -0,0 +1,14 @@
---
name: reference-cloudflare-access
description: Where the Cloudflare API credentials live (SOPS vault) — azcomputerguru.com DNS is on Cloudflare, not the IX nameservers
metadata:
type: reference
---
Cloudflare API access is in the SOPS vault at **`services/cloudflare.sops.yaml`** (account "Mike@azcomputerguru.com Account", account_id `44594c346617d918bd3302a00b07e122`). Fields under `credentials`:
- `api_token_full_account` — full-account token (`solitary-rain-773d`, added 2026-05-10, expires 2027-05-10)
- `api_token_full_dns` — full DNS-edit token (use this for DNS record changes)
- `api_token_legacy` — legacy token
- `zone_id_azcomputerguru` = `1beb9917c22b54be32e5215df2c227ce`
**azcomputerguru.com DNS is hosted on Cloudflare** (ns mckinley/amir.ns.cloudflare.com), NOT the IX/cPanel nameservers (ns1/ns2.acghosting.com) that most CLIENT domains use. So azcomputerguru.com zone edits go through the Cloudflare API, not `whmapi1`. Pattern: `curl -H "Authorization: Bearer <api_token_full_dns>" https://api.cloudflare.com/client/v4/zones/<zone_id>/dns_records`. (Used 2026-06-15 to add the cross-domain DMARC report-authorization record `cryoweave.com._report._dmarc.azcomputerguru.com TXT "v=DMARC1;"` so client DMARC reports can be sent to rua@azcomputerguru.com.) See [[reference_ix_server_access]] for client-domain DNS (cPanel).

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""rmm-search engine. Reads the GuruRMM agents JSON array on stdin; reads
QUERY/CLIENT/ONLINE/JSON/LISTC/LIMIT from the environment. Flexible, forgiving
multi-field search — see rmm-search.sh for usage. Kept as a sidecar (not a
heredoc) because the agents payload is too large to pass as a CLI argument."""
import sys, os, json, re
from datetime import datetime, timezone
raw = sys.stdin.read()
agents = json.loads(raw) if raw.strip() else []
query = os.environ.get("QUERY", "").strip()
client = os.environ.get("CLIENT", "").strip()
online_only = os.environ.get("ONLINE") == "1"
as_json = os.environ.get("JSON") == "1"
list_clients = os.environ.get("LISTC") == "1"
try:
limit = int(os.environ.get("LIMIT", "0") or 0)
except ValueError:
limit = 0
def norm(s):
return re.sub(r'[^a-z0-9]', '', (s or '').lower())
def is_subseq(t, v):
it = iter(v)
return all(c in it for c in t)
def field_score(val, term, allow_subseq):
"""How well a single query word matches one normalized field value."""
if not val or not term:
return 0
if val == term:
return 100
if val.startswith(term):
return 80
if term in val:
return 60
if allow_subseq and len(term) >= 3 and is_subseq(term, val):
return 25
return 0
# field -> (key, weight, allow_subsequence)
FIELDS = [
('hostname', 1.00, True),
('client_name', 0.75, False),
('site_name', 0.60, False),
('os_type', 0.55, False),
('id', 0.40, False),
]
def fresh(last_seen):
if not last_seen:
return False
try:
dt = datetime.fromisoformat(last_seen.replace('Z', '+00:00'))
return (datetime.now(timezone.utc) - dt).total_seconds() < 300
except Exception:
return False
if list_clients:
for n in sorted({(a.get('client_name') or 'Unassigned') for a in agents}):
print(n)
sys.exit(0)
# --- explicit hard client scope (normalized; "valleywide" -> "Valley Wide Plastering") ---
if client:
cn = norm(client)
matched = sorted({(a.get('client_name') or '') for a in agents
if cn and cn in norm(a.get('client_name'))} - {''})
if not matched:
print(f"[ERROR] no client matches '{client}'. Try: rmm-search.sh --list-clients", file=sys.stderr)
sys.exit(2)
if len(matched) > 1:
exact = [m for m in matched if norm(m) == cn]
if len(exact) == 1:
matched = exact
else:
print(f"[AMBIGUOUS] '{client}' matches {len(matched)} clients - narrow --client:", file=sys.stderr)
for m in matched:
print(" - " + m, file=sys.stderr)
sys.exit(3)
agents = [a for a in agents if (a.get('client_name') or '') == matched[0]]
if online_only:
agents = [a for a in agents if fresh(a.get('last_seen'))]
terms = [t for t in (norm(x) for x in query.split()) if t]
joined = norm(query)
def score_agent(a):
if not terms:
return 1 # no query (e.g. "all machines for client X")
nf = [(norm(a.get(k)), w, sub) for k, w, sub in FIELDS]
total = 0.0
for term in terms:
best = max(field_score(val, term, sub) * w for val, w, sub in nf)
if best == 0:
return 0 # AND: every word must hit some field
total += best
if joined and joined in norm(a.get('hostname')):
total += 60 # whole query lands in hostname -> clearly THE machine
return total
results = sorted(((score_agent(a), a) for a in agents),
key=lambda t: (-t[0], (t[1].get('hostname') or '').lower()))
results = [(s, a) for s, a in results if s > 0]
if limit > 0:
results = results[:limit]
if as_json:
print(json.dumps([{
'hostname': a.get('hostname'), 'id': a.get('id'), 'client': a.get('client_name'),
'site': a.get('site_name'), 'os': a.get('os_type'),
'online': fresh(a.get('last_seen')), 'last_seen': a.get('last_seen'), 'score': round(s, 1),
} for s, a in results], indent=2))
sys.exit(0)
scope = f" in '{(agents[0].get('client_name') if (client and agents) else client)}'" if client else ""
if not results:
print(f"No machines match '{query}'{scope}. (try fewer/looser words, or --list-clients)")
sys.exit(0)
hdr = f"{'HOSTNAME':<22} {'CLIENT':<26} {'SITE':<16} {'OS':<8} {'STAT':<7} ID"
print(f"{len(results)} match(es) for '{query}'{scope} (best first):")
print(hdr)
print("-" * len(hdr))
for s, a in results:
st = 'online' if fresh(a.get('last_seen')) else 'offline'
print(f"{(a.get('hostname') or '')[:21]:<22} {(a.get('client_name') or '?')[:25]:<26} "
f"{(a.get('site_name') or '')[:15]:<16} {(a.get('os_type') or '')[:7]:<8} {st:<7} {a.get('id')}")

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# rmm-search.sh — find machines in the GuruRMM fleet on the first try.
#
# Flexible, forgiving search: normalizes case/spaces/hyphens, matches across
# HOSTNAME + CLIENT + SITE + OS, and treats every query word as a required
# filter (AND). So a query naturally narrows and can't bleed across clients:
# rmm-search.sh hyperv valleywide -> only Valley Wide's hyperv host
# rmm-search.sh hyperv -> every hyperv box, each labeled by client
# rmm-search.sh hyperv -c valleywide -> hard-scope to one client, then search
#
# Usage:
# rmm-search.sh <words...> [-c|--client <name>] [--online] [--json] [-n N]
# rmm-search.sh -c <client> # list ALL machines for a client
# rmm-search.sh --list-clients # show distinct client names
#
# Online state is derived from last_seen recency (<5 min); the API is_connected
# flag is currently unreliable (null fleet-wide). Engine: rmm-search.py.
set -u
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
QUERY=""; CLIENT=""; ONLINE=0; JSON=0; LISTC=0; LIMIT=0
while [ $# -gt 0 ]; do
case "$1" in
-c|--client) CLIENT="${2:-}"; shift 2;;
-n|--limit) LIMIT="${2:-0}"; shift 2;;
--online) ONLINE=1; shift;;
--json) JSON=1; shift;;
--list-clients) LISTC=1; shift;;
-h|--help) sed -n '2,20p' "$0"; exit 0;;
*) QUERY="${QUERY:+$QUERY }$1"; shift;;
esac
done
if [ -z "$QUERY" ] && [ -z "$CLIENT" ] && [ "$LISTC" -eq 0 ]; then
echo "[ERROR] give <words>, a --client, or --list-clients" >&2
sed -n '12,17p' "$0" >&2; exit 1
fi
eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh" 2>/dev/null)" >/dev/null
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then echo "[ERROR] RMM auth failed (see rmm-auth.sh)" >&2; exit 1; fi
AGENTS=$(curl -s "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")
if [ -z "$AGENTS" ] || [ "${AGENTS:0:1}" != "[" ]; then echo "[ERROR] could not fetch agents: ${AGENTS:0:160}" >&2; exit 1; fi
# Pipe agents on stdin (payload too large for argv on Windows); flags via env.
printf '%s' "$AGENTS" | QUERY="$QUERY" CLIENT="$CLIENT" ONLINE="$ONLINE" JSON="$JSON" LISTC="$LISTC" LIMIT="$LIMIT" \
python3 "$ROOT/.claude/scripts/rmm-search.py"

View File

@@ -23,6 +23,7 @@ that will fail the next email task; fix it with `assign-exchange-role.sh <domain
| cascadestucson.com | cascadestucson.com | 207fa277-e9d8-4eb7-ada1-1064d2221498 | NO | Old app only; IdentityRiskyUser not consented |
| cclac.net | cclac.net | e8a0fafc-21ee-41e8-a5ba-f3a250a8a30e | NO | |
| Cobalt Fine Arts | cobaltfinearts.com | 03c4d4ec-b6d3-4061-a75c-8a4250ba2b29 | NO | |
| Cryoweave | cryoweave.com | 44705a37-b5d8-4bb1-882d-e18775612ada | YES | All apps consented 2026-06-15 (Sec Inv + Exch Op Exchange Admin, User Mgr User Admin + Auth Admin, Tenant Admin CA Admin); no MDE. Onboarded for outbound-email deliverability investigation. ACG GA `sysadmin@cryoweave.com` created (vault clients/cryoweave/m365-sysadmin + 1Password Clients). |
| CUADRO LLC | cuadro.design | b68c7171-31d6-4b63-8243-7a2cade9caf8 | NO | |
| Curtis Plumbing | cparizona.onmicrosoft.com | d2d7ea54-9146-42d1-b99e-0da098550bde | NO | |
| cwconcretellc.com | NETORGFT11452752.onmicrosoft.com | dfee2224-93cd-4291-9b09-6c6ce9bb8711 | NO | |

View File

@@ -0,0 +1,74 @@
---
name: rmm-search
description: >
Find machines/agents in the GuruRMM fleet cleanly and on the first try. Use
this ANY time you need to locate an RMM agent by name, role, client, site, or
OS before acting on it — instead of pulling /api/agents and grepping (which
bleeds across clients and picks the wrong box). Flexible, forgiving, multi-field
search with a client filter so a query like "hyperv valleywide" returns ONLY
Valley Wide's hyperv host, never Dataforth's. Invoke on: "find the X machine",
"which agent is", "look up <host> in RMM", "<client>'s server/DC/hyperv/file
server", "search RMM for", "what's the agent id for". After finding the agent,
hand its hostname/id to the `rmm` skill to run commands.
---
# rmm-search — clean machine lookup in GuruRMM
The `/rmm` skill resolves agents by grepping `/api/agents` client-side, which is
exactly where lookups go wrong: a bare term like `hyperv` matches every client's
hyperv box, and it's easy to act on the wrong one. This skill does a forgiving,
ranked, **client-aware** search instead. Use it as the front door for *finding*
an agent; use `/rmm` for *acting* on it.
## Usage
```bash
bash .claude/scripts/rmm-search.sh <words...> [-c|--client <name>] [--online] [--json] [-n N]
bash .claude/scripts/rmm-search.sh -c <client> # list ALL machines for a client
bash .claude/scripts/rmm-search.sh --list-clients # distinct client names
```
## How matching works (built for first-try correctness)
- **Normalized:** case, spaces, and hyphens are ignored — `valleywide`,
`valley wide`, and `Valley Wide Plastering` all match the same client.
- **Multi-field:** every query word is matched against `hostname`, `client_name`,
`site_name`, `os_type`, and `id`.
- **AND semantics:** *every* word must hit some field, so adding words **narrows**.
`hyperv valleywide` → only the box that is both a hyperv host AND Valley Wide.
This is why a query can't bleed across clients without `-c`.
- **Ranked:** exact > prefix > substring > subsequence (typo tolerance on
hostnames, e.g. `hyprv``HYPERV`). Hostname matches outrank client/site/OS.
Best result first; a whole-query hostname hit gets a boost.
- **`-c/--client`** is an explicit hard scope. If the client term is ambiguous
(matches >1 client and none exactly), it **lists the candidates and stops**
rather than guessing — narrow the value and retry.
- **`--online`** keeps only agents seen in the last 5 minutes. Online state is
derived from `last_seen`, NOT the API `is_connected` flag (currently null
fleet-wide — don't trust it).
## Output
A ranked table: `HOSTNAME | CLIENT | SITE | OS | STAT | ID` (best match first).
`--json` emits `{hostname,id,client,site,os,online,last_seen,score}` for piping
the `id` into a `/rmm` command. `-n N` caps the rows.
## Examples
```bash
rmm-search.sh hyperv valleywide # VWP-HYPERV1 only (not DF-HYPERV-B)
rmm-search.sh dc -c dataforth # Dataforth's domain controllers
rmm-search.sh vwp fil # VWP-FILES (partial words ok)
rmm-search.sh -c "peaceful spirit" # every Peaceful Spirit machine
rmm-search.sh files valleywide --json | jq -r '.[0].id' # id -> feed to /rmm
```
## Implementation
- Wrapper `.claude/scripts/rmm-search.sh` (arg parsing + auth via `rmm-auth.sh` +
fetch `/api/agents`), engine `.claude/scripts/rmm-search.py` (scoring/filter).
- The agents payload is piped to the engine on **stdin** — it's too large to pass
as a CLI argument on Windows ("Argument list too long").
- Read-only. To run a command on a found agent, pass its hostname/id to `/rmm`.
- Ranking heuristic mirrors the dashboard Omnibox (`scoreMatch`) in spirit but is
deliberately looser (multi-field + subsequence) to favor first-try hits.