sync: auto-sync from HOWARD-HOME at 2026-06-25 21:48:38

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 21:48:38
This commit is contained in:
2026-06-25 21:49:05 -07:00
parent 9a243a9b96
commit 04b0d12150
3 changed files with 407 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
---
name: drive-map
description: Reliably create/repoint Windows network drive maps and share shortcuts on a remote endpoint via GuruRMM. Bakes in the things that make this fight every time — runs in the user session (not SYSTEM, so maps actually appear), stores the per-host credential with cmdkey (the workgroup-PC-to-domain-share case), makes maps persistent, repoints/removes stale NAS shortcuts, and verifies access. Built for the Cascades NAS -> CS-SERVER migration but generic.
---
# drive-map — remote drive maps & share shortcuts that actually stick
`net use` from RMM "doesn't work" for predictable reasons, and we kept re-solving
them by hand. This skill encodes the fixes so a repoint is one command.
## The four things that make mapped drives fight you (and the fix this skill bakes in)
1. **SYSTEM context is invisible to the user.** RMM runs as SYSTEM by default. A
drive mapped/shortcut written as SYSTEM lands in SYSTEM's profile, NOT the
logged-on user's — so the user sees nothing and you think it "failed."
**Fix:** every operation here runs `context: user_session`. It uses the user's
token, `[Environment]::GetFolderPath('Desktop')`, and the user's credential
vault. **Requires an active (logged-on) desktop session** — locked is usually
OK, logged-off is not. If no session, the skill reports it instead of silently
no-opping.
2. **Workgroup PC -> domain share = Access Denied** unless a credential is stored.
A machine that is not domain-joined (e.g. `DESKTOP-LPOPV30`, WORKGROUP, local
login) has no Kerberos/NTLM identity the server trusts. It must present a
**stored** credential for that exact server host.
**Fix:** `cred`/`migrate` run `cmdkey /add:<HOST> /user:<DOMAIN\user> /pass:…`
in the user session so the credential lands in the user's Credential Manager,
keyed to the server host used in the UNC. (This is precisely how Karen reaches
the NAS today: `cmdkey` target `CASCADESDS`, user `karen rossini`.)
3. **Maps vanish on logoff.** `net use` without persistence is gone next login.
**Fix:** persistent by default (`/persistent:yes`); cmdkey credential is
persistent too, so the map reconnects without a prompt.
4. **Stale NAS shortcuts/maps linger.** The old `\\cascadesds\…` shortcut and the
live connection confuse users mid-migration.
**Fix:** `--remove-old <UNC-prefix>` repoints or deletes desktop shortcuts that
target the old prefix, drops the old drive letter, and removes the old cmdkey.
## Credential handling (read this)
- The password is read from the **SOPS vault** (`--cred-vault` + `--cred-field`),
never passed as plaintext on the command line (CLAUDE.md rule).
- **Caveat — it transits RMM.** cmdkey needs the plaintext on the endpoint, so the
password appears in the dispatched command text, which RMM stores in command
history (admin-only, internal). For a sensitive account, purge that history
entry afterward or rotate. The skill never prints the password to stdout/errorlog.
- For a domain account whose password we don't have, the correct move on a DC we
control is to set it deliberately and vault it first — do that, then run `cred`.
## Usage
```
bash .claude/skills/drive-map/scripts/drive-map.sh <verb> --host <name> [opts]
```
| Verb | Does |
|------|------|
| `verify` | Test-Path the target UNC/letter from the user session; report reachable or not. Read-only. |
| `cred` | Store a per-host credential (`cmdkey /add`) so the user can reach a server. |
| `map` | Map a drive letter to `\\HOST\Share` (persistent), optionally storing the cred first. |
| `shortcut` | Drop a desktop `.lnk` to a UNC target (optionally pin to Quick Access). |
| `unmap` | Remove a drive letter and/or desktop shortcuts pointing at `--remove-old`, and the old cmdkey. |
| `migrate` | The all-in-one repoint: remove/repoint old shortcut, store new cred, map and/or shortcut the new target, verify. |
### Options
```
--host NAME RMM hostname (required; resolved to agent id, must be Windows + online)
--server '\\HOST\Share' target share for a drive map
--target '\\HOST\Share\Sub' UNC for a shortcut / verify
--letter X drive letter for map/unmap (no colon)
--name NAME shortcut filename (default: leaf of --target)
--cred-user 'DOMAIN\user' identity to store (e.g. CASCADES\karen.rossini)
--cred-vault PATH sops path holding the password (e.g. clients/cascades-tucson/...sops.yaml)
--cred-field FIELD field within the vault entry (default: credentials.password)
--remove-old '\\oldhost\share' prefix of stale shortcuts/connections to strip (migrate/unmap)
--quick-access also pin --target to Quick Access (best-effort; Shell verb)
--no-persistent non-persistent map (default is persistent)
--profile-hint NAME substring to disambiguate the user when several are logged on
--dry-run print the generated PowerShell, do not dispatch
--timeout N dispatch timeout seconds (default 90)
```
### Examples
```bash
# Karen: workgroup PC, repoint NAS ALDocs shortcut to CS-SERVER, store her domain cred
bash .claude/skills/drive-map/scripts/drive-map.sh migrate \
--host DESKTOP-LPOPV30 \
--target '\\CS-SERVER\Server\ALDocs' --name ALDocs --quick-access \
--remove-old '\\cascadesds\Server' \
--cred-user 'CASCADES\karen.rossini' \
--cred-vault clients/cascades-tucson/karen-rossini.sops.yaml
# Just check a user can reach the new share
bash .claude/skills/drive-map/scripts/drive-map.sh verify --host DESKTOP-LPOPV30 \
--target '\\CS-SERVER\Server\ALDocs'
# Map a letter for a domain-joined user (no cred needed)
bash .claude/skills/drive-map/scripts/drive-map.sh map --host SOME-PC \
--server '\\CS-SERVER\SalesDept' --letter S
```
## Hard rules
- **Always `verify` first and last.** Confirm the target is reachable for that user
before declaring success — a green `net use` line is not proof of access.
- **One user at a time, with a session.** If no interactive user is logged on, stop
and say so; do not "succeed" against SYSTEM's profile.
- **Additive to permissions.** This skill never touches share/NTFS ACLs. If the user
lacks rights on the target, fix that on the server side (group membership), not here.
- **Confirm before mutating a live user's desktop** during business hours unless told
to proceed — it is outward-facing (the user sees their desktop change).
- On any genuine failure the script calls `log-skill-error.sh` (per CLAUDE.md).
## Implementation
- `scripts/drive-map.sh` — bash orchestrator: RMM auth (`rmm-auth.sh`), agent
resolution, vault read for the password, generates the endpoint PowerShell,
dispatches it `context: user_session`, polls, reports. All endpoint logic runs
in the user session by design (see fix #1).
- No endpoint install; the PowerShell is generated per call and dispatched via RMM.

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# drive-map.sh — create/repoint Windows network drive maps + share shortcuts on a
# remote endpoint via GuruRMM, run in the user's session so they actually appear.
# See SKILL.md for the why. Verbs: verify | cred | map | shortcut | unmap | migrate.
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "drive-map" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
die(){ echo "[ERROR] $1" >&2; exit "${2:-2}"; }
VERB="${1:-}"; shift || true
[ -n "$VERB" ] || die "usage: drive-map.sh <verify|cred|map|shortcut|unmap|migrate> --host <name> [opts]"
HOST=""; SERVER=""; TARGET=""; LETTER=""; NAME=""; CRED_USER=""; CRED_VAULT=""
CRED_FIELD="credentials.password"; REMOVE_OLD=""; QUICK=0; PERSIST="yes"
PROFILE_HINT=""; DRYRUN=0; TIMEOUT=90
while [ $# -gt 0 ]; do
case "$1" in
--host) HOST="${2:?}"; shift 2;;
--server) SERVER="${2:?}"; shift 2;;
--target) TARGET="${2:?}"; shift 2;;
--letter) LETTER="${2:?}"; LETTER="${LETTER%%:}"; shift 2;;
--name) NAME="${2:?}"; shift 2;;
--cred-user) CRED_USER="${2:?}"; shift 2;;
--cred-vault) CRED_VAULT="${2:?}"; shift 2;;
--cred-field) CRED_FIELD="${2:?}"; shift 2;;
--remove-old) REMOVE_OLD="${2:?}"; shift 2;;
--quick-access) QUICK=1; shift;;
--no-persistent) PERSIST="no"; shift;;
--profile-hint) PROFILE_HINT="${2:?}"; shift 2;;
--dry-run) DRYRUN=1; shift;;
--timeout) TIMEOUT="${2:?}"; shift 2;;
*) die "unknown option: $1";;
esac
done
[ -n "$HOST" ] || die "--host is required"
# Normalize a UNC: strip any/all leading backslashes, force exactly two. Shell-arg
# layers (Git-bash on Windows) collapse a leading "\\" to "\", which breaks the host
# parse; this makes the tool robust to 0/1/2 leading backslashes on input.
norm_unc(){ [ -n "$1" ] || return 0; printf '\\\\%s' "$(printf '%s' "$1" | sed -E 's#^\\+##')"; }
# host portion of a UNC: \\HOST\share\... -> HOST
unc_host(){ printf '%s' "$1" | sed -E 's#^\\+##; s#\\.*##'; }
# leaf of a UNC path: \\h\share\a\b -> b
unc_leaf(){ printf '%s' "$1" | sed 's#\\*$##; s#.*\\##'; }
SERVER="$(norm_unc "$SERVER")"; TARGET="$(norm_unc "$TARGET")"; REMOVE_OLD="$(norm_unc "$REMOVE_OLD")"
# ---- resolve the credential password (vault only; never plaintext on CLI) ----
CRED_PASS=""
if [ -n "$CRED_USER" ]; then
[ -n "$CRED_VAULT" ] || die "--cred-user given but no --cred-vault to read the password from"
CRED_PASS="$(bash "$VAULT" get-field "$CRED_VAULT" "$CRED_FIELD" 2>/dev/null)"
[ -n "$CRED_PASS" ] || { logerr "vault read empty for $CRED_VAULT:$CRED_FIELD" "host=$HOST"; die "could not read password at vault:$CRED_VAULT field:$CRED_FIELD"; }
fi
# ---- generate the endpoint PowerShell for the verb (all user_session) ----
# PS variable values are injected via single-quoted PS literals; we escape any
# embedded single quote by doubling it (PS rule). Password is masked in all output.
psq(){ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"; }
build_ps(){
local ps=""
# append one PS line with a REAL newline. Never use printf %b here: UNC paths
# contain backslashes (\c, \S) that %b would eat as escapes and truncate output.
add(){ ps+="$1"$'\n'; }
add "\$ErrorActionPreference='Continue'"
add "\$who = (Get-CimInstance Win32_ComputerSystem).UserName; if(-not \$who){ Write-Host '[WARN] no interactive user resolved; user-session ops may no-op' }"
add "Write-Host (\"[INFO] running as: \$env:USERNAME active: \$who\")"
# remove-old: strip stale shortcuts/letter/cred pointing at the old prefix
if [ -n "$REMOVE_OLD" ] && { [ "$VERB" = "migrate" ] || [ "$VERB" = "unmap" ]; }; then
local oldhost; oldhost="$(unc_host "$REMOVE_OLD")"
add "\$old=$(psq "$REMOVE_OLD")"
add "\$desk=[Environment]::GetFolderPath('Desktop')"
add "\$wsh=New-Object -ComObject WScript.Shell"
add "Get-ChildItem \$desk -Filter *.lnk -EA SilentlyContinue | ForEach-Object { \$t=\$wsh.CreateShortcut(\$_.FullName).TargetPath; if(\$t -like (\$old+'*')){ Remove-Item \$_.FullName -Force -EA SilentlyContinue; Write-Host (\"[OK] removed stale shortcut: \"+\$_.Name+\" -> \"+\$t) } }"
add "cmdkey /delete:$oldhost *>\$null; Write-Host '[OK] cleared old cmdkey: $oldhost'"
[ -n "$LETTER" ] && add "net use ${LETTER}: /delete /y *>\$null"
fi
# cred: store per-host credential
if [ -n "$CRED_USER" ] && { [ "$VERB" = "cred" ] || [ "$VERB" = "map" ] || [ "$VERB" = "migrate" ]; }; then
local credhost=""
[ -n "$SERVER" ] && credhost="$(unc_host "$SERVER")"
[ -z "$credhost" ] && [ -n "$TARGET" ] && credhost="$(unc_host "$TARGET")"
[ -n "$credhost" ] || die "cred needs --server or --target to derive the server host"
add "cmdkey /add:$credhost /user:$(psq "$CRED_USER") /pass:$(psq "$CRED_PASS") | Out-Null"
add "Write-Host '[OK] stored credential for $credhost as $CRED_USER'"
fi
# map: persistent drive letter
if [ "$VERB" = "map" ]; then
[ -n "$SERVER" ] || die "map needs --server"
[ -n "$LETTER" ] || die "map needs --letter"
add "net use ${LETTER}: /delete /y *>\$null"
add "\$r = net use ${LETTER}: $(psq "$SERVER") /persistent:$PERSIST 2>&1; Write-Host (\"\$r\")"
add "if(Test-Path ${LETTER}:\\){ Write-Host '[OK] mapped ${LETTER}: -> $SERVER' } else { Write-Host '[FAIL] map ${LETTER}: failed'; exit 1 }"
fi
# migrate may also map a letter if both server+letter given
if [ "$VERB" = "migrate" ] && [ -n "$SERVER" ] && [ -n "$LETTER" ]; then
add "net use ${LETTER}: /delete /y *>\$null"
add "\$r = net use ${LETTER}: $(psq "$SERVER") /persistent:$PERSIST 2>&1; Write-Host (\"\$r\")"
add "if(Test-Path ${LETTER}:\\){ Write-Host '[OK] mapped ${LETTER}: -> $SERVER' } else { Write-Host '[WARN] map ${LETTER}: did not verify' }"
fi
# shortcut: desktop .lnk (+ optional quick access)
if [ -n "$TARGET" ] && { [ "$VERB" = "shortcut" ] || [ "$VERB" = "migrate" ]; }; then
local lname; lname="${NAME:-$(unc_leaf "$TARGET")}"
add "\$desk2=[Environment]::GetFolderPath('Desktop')"
add "\$wsh2=New-Object -ComObject WScript.Shell"
add "\$lp=Join-Path \$desk2 ($(psq "$lname")+'.lnk')"
add "\$lnk=\$wsh2.CreateShortcut(\$lp); \$lnk.TargetPath=$(psq "$TARGET"); \$lnk.Save()"
add "Write-Host ('[OK] shortcut: '+\$lp+' -> $TARGET')"
if [ "$QUICK" = "1" ]; then
add "try { \$sa=New-Object -ComObject Shell.Application; \$sa.Namespace($(psq "$TARGET")).Self.InvokeVerb('pintohome'); Write-Host '[OK] pinned to Quick Access' } catch { Write-Host '[WARN] Quick Access pin failed (non-fatal)' }"
fi
fi
# verify: target reachable from this user
if [ -n "$TARGET" ] && { [ "$VERB" = "verify" ] || [ "$VERB" = "migrate" ]; }; then
add "if(Test-Path -LiteralPath $(psq "$TARGET")){ Write-Host '[OK] reachable: $TARGET' } else { Write-Host '[FAIL] NOT reachable: $TARGET'; exit 1 }"
elif [ "$VERB" = "verify" ] && [ -n "$LETTER" ]; then
add "if(Test-Path ${LETTER}:\\){ Write-Host '[OK] reachable: ${LETTER}:' } else { Write-Host '[FAIL] NOT reachable: ${LETTER}:'; exit 1 }"
fi
printf '%s' "$ps"
}
PS_SCRIPT="$(build_ps)"
[ -n "$PS_SCRIPT" ] || die "verb '$VERB' produced no actions — check options"
if [ "$DRYRUN" = "1" ]; then
echo "[DRY-RUN] would dispatch to $HOST (user_session), timeout ${TIMEOUT}s:"
echo "------------------------------------------------------------"
# mask the password in the preview
if [ -n "$CRED_PASS" ]; then printf '%s\n' "$PS_SCRIPT" | sed "s/$(printf '%s' "$CRED_PASS" | sed 's/[.[\*^$()+?{|]/\\&/g')/********/g"; else printf '%s\n' "$PS_SCRIPT"; fi
echo "------------------------------------------------------------"
exit 0
fi
# ---- RMM auth + agent resolution ----
eval "$(bash "$REPO/.claude/scripts/rmm-auth.sh")" >/dev/null 2>&1 || { logerr "rmm auth failed"; die "RMM auth failed"; }
AGENTS="$(curl -s "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")"
AGENT="$(echo "$AGENTS" | jq --arg h "$HOST" '[.[] | select(.hostname|ascii_downcase|contains($h|ascii_downcase))] | .[0]')"
AID="$(echo "$AGENT" | jq -r '.id // empty')"
AHOST="$(echo "$AGENT" | jq -r '.hostname // empty')"
AOS="$(echo "$AGENT" | jq -r '.os_type // empty')"
ACONN="$(echo "$AGENT" | jq -r '.is_connected // false')"
[ -n "$AID" ] || { logerr "agent not found: $HOST"; die "no RMM agent matching '$HOST'"; }
[ "$AOS" = "windows" ] || die "agent $AHOST is os_type=$AOS — drive-map is Windows-only"
echo "[INFO] target: $AHOST (id=$AID, connected=$ACONN) verb=$VERB"
[ "$ACONN" = "true" ] || echo "[WARN] agent offline — command will queue until it reconnects"
PAYLOAD="$(jq -n --arg ct powershell --arg cmd "$PS_SCRIPT" --argjson to "$TIMEOUT" \
'{command_type:$ct, command:$cmd, timeout_seconds:$to, context:"user_session"}')"
CMD_ID="$(curl -s -X POST "$RMM/api/agents/$AID/command" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$PAYLOAD" | jq -r '.command_id // empty')"
[ -n "$CMD_ID" ] || { logerr "dispatch failed" "host=$AHOST verb=$VERB"; die "dispatch failed"; }
echo "[INFO] dispatched cmd=$CMD_ID (user_session)"
S=""; R=""
for i in $(seq 1 30); do
R="$(curl -s "$RMM/api/commands/$CMD_ID" -H "Authorization: Bearer $TOKEN")"
S="$(echo "$R" | jq -r '.status')"
{ [ "$S" = "completed" ] || [ "$S" = "failed" ] || [ "$S" = "cancelled" ] || [ "$S" = "interrupted" ]; } && break
sleep 4
done
echo "[INFO] status=$S exit=$(echo "$R" | jq -r '.exit_code // "—"')"
echo "--- stdout ---"; echo "$R" | jq -r '.stdout // ""'
STDERR="$(echo "$R" | jq -r '.stderr // ""')"
[ -n "$STDERR" ] && { echo "--- stderr ---"; echo "$STDERR"; }
# alert per RMM convention (write op only)
if [ "$VERB" != "verify" ]; then
bash "$REPO/.claude/scripts/post-bot-alert.sh" "[RMM] drive-map $VERB on $AHOST -> ${TARGET:-$SERVER} (status=$S)" >/dev/null 2>&1 || true
fi
case "$S" in
completed) exit 0;;
failed) logerr "drive-map $VERB failed on $AHOST" "cmd=$CMD_ID"; exit 1;;
*) logerr "drive-map $VERB ended status=$S on $AHOST" "cmd=$CMD_ID"; exit 1;;
esac

View File

@@ -0,0 +1,106 @@
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Resumed the paused Cascades of Tucson Windows Home -> Pro upgrade workstream (prereq for
domain-joining the remaining staff PCs; Windows Home cannot domain-join). Started by loading
context from the wiki, `REMAINING-WORK-PLAN.md`, and the `feedback_windows_pro_upgrade_billing`
memory, then ran a live RMM status check on the 5 target Home machines. Of the five
(LAPTOP-8P7HDSEI, MDIRECTOR-PC, MEMRECEPT-PC, NurseAssist, SALES4-PC), three were online,
NurseAssist was offline (and flagged as a possible duplicate of Assistnurse-pc), and SALES4-PC
was offline and bypassed per the decision to repurpose Tamra's departing machine.
Per Howard's direction, ran the edition upgrade using the generic public Pro key first to verify
the process before consuming a MAK count. Confirmed all three online boxes were `EditionID=Core`
(Home) with no logged-on users, then dispatched `changepk.exe /productkey VK7JG-NPHTM-C97JM-9MPGT-3V66T`
via RMM. As SYSTEM, changepk flips the edition in-place without auto-rebooting, which left the
registry EditionID and the licensing service out of sync. A single reboot per machine reconciled
them: all three came back reading Professional. MDIRECTOR-PC self-activated as genuine Pro via a
built-in digital entitlement (no MAK, no charge). The other two needed activation.
Applied the ACG MAK to MEMRECEPT-PC and LAPTOP-8P7HDSEI (`slmgr /ipk` + `/ato`). Discovered the
ACG MAK is actually a Windows Pro *for Workstations* MAK — `/ipk` retargets the edition to
ProfessionalWorkstation (a higher SKU, fine for domain join). LAPTOP-8P7HDSEI activated; MEMRECEPT-PC
hit a transient `0x8004FE92` on first `/ato` and activated on retry. Both Licensed via VOLUME_MAK.
Created Syncro ticket #32466 under Cascades, added a customer-visible work note, created a reusable
taxable "Windows Pro Upgrade" product ($99), and invoiced: 2x $99 keys (MEMRECEPT-PC, LAPTOP-8P7HDSEI,
machine named per line) plus 1.0h remote labor. Cascades has a 47.75h prepaid block, so the labor
auto-deducted ($0 on the invoice, block -> 46.75); invoice total $215.23 with AZ tax on the keys.
MDIRECTOR-PC was not billed (free digital activation).
## Key Decisions
- Generic Pro key first, MAK second — verify the edition flip works before consuming/billing a MAK
count (Howard's explicit instruction).
- Did the upgrades remotely via RMM rather than waiting for the onsite batch, since no users were
logged in (~8:45 PM Tucson) so the reboots were non-disruptive.
- Rebooted to reconcile the half-applied edition state (changepk as SYSTEM does not auto-reboot;
registry vs licensing diverge until a reboot).
- MDIRECTOR-PC self-activated free via digital entitlement -> NOT billed (billing rule keys $99 to
MAK usage, which was not consumed for that machine).
- Created a dedicated taxable "Windows Pro Upgrade" Syncro product (id 23571919, $99) for clean
QuickBooks mapping and reuse on future Home->Pro billing; taxable to match ACG's other license
products (Howard confirmed).
- 1.0h remote labor drawn from the Cascades prepaid block (standard behavior for a block customer).
## Problems Encountered
- First changepk dispatch failed before running: doubled single-quotes around a PowerShell registry
path inside a single-quoted bash `$SCRIPT` collapsed the string, leaving the "Windows NT" path
unquoted; `$ErrorActionPreference="Stop"` aborted on line 1, so changepk never executed (machines
untouched). Fixed by using double-quotes for paths inside the single-quoted bash script. Logged to
errorlog as --friction (ref feedback_windows_quote_stripping).
- MEMRECEPT-PC `/ato` transient `0x8004FE92` on first attempt -> succeeded on retry (ancient Pentium
box on a 100 Mbps NIC; likely a momentary reach to the activation server).
- LAPTOP-8P7HDSEI briefly reported `EditionID=Enterprise` mid-transition; self-resolved to Professional
after the reboot.
## Configuration Changes
- Modified: `clients/cascades-tucson/docs/REMAINING-WORK-PLAN.md` — recorded the 3 boxes upgraded to
Pro (process, results, billing) and that NurseAssist/SALES4-PC remain pending.
- Modified: `.claude/memory/feedback_windows_pro_upgrade_billing.md` — MAK is Pro-for-Workstations;
the working remote flow; per-machine activation status; invoiced status (#32466).
- Appended: `errorlog.md` — bash/PowerShell quoting friction entry.
- Created (in Syncro, not repo): Windows Pro Upgrade product, ticket #32466, invoice #1650806091.
## Credentials & Secrets
- ACG Windows Pro (for Workstations) MAK: vault `infrastructure/windows-pro-mak.sops.yaml`, field
`credentials.product_key`. Used `/ipk` + `/ato` on MEMRECEPT-PC and LAPTOP-8P7HDSEI (2 counts
consumed). No new secrets created. Generic public Pro key (not secret): VK7JG-NPHTM-C97JM-9MPGT-3V66T.
## Infrastructure & Servers
- GuruRMM: http://172.16.3.30:3001. Cascades agent IDs — MDIRECTOR-PC 6b7990aa-edad-41c7-8f2d-5efdcaa41046,
MEMRECEPT-PC e93ac0b6-c593-4fa2-b1f7-56ebf3816efb, LAPTOP-8P7HDSEI d8e9502f-7061-4574-8cc3-a84f60bb3471,
NurseAssist fc88f14b-06eb-47ac-b9e6-971c44d700ba (offline), SALES4-PC 975f70d8-cd6d-45d7-9da1-6ce2f1ae59ab (offline).
- Syncro: Cascades of Tucson customer_id 20149445; prepay block 47.75 -> 46.75 hrs.
## Commands & Outputs
- Edition flip (per machine, as SYSTEM): `changepk.exe /productkey VK7JG-NPHTM-C97JM-9MPGT-3V66T`
-> EditionID Core->Professional, exitcode 0, no auto-reboot. Reboot once to sync registry+licensing.
- Activation: `cscript //nologo slmgr.vbs /ipk <MAK>` then `/ato`; retry `/ato` if `0x8004FE92`.
Verify with `/dli` (License Status: Licensed, VOLUME_MAK).
- Trust `EditionID` over `ProductName` (ProductName reads "Windows 10 Home" on Win11 boxes - stale).
## Pending / Incomplete Tasks
- Domain-join the 3 now-Pro boxes (MDIRECTOR-PC, MEMRECEPT-PC, LAPTOP-8P7HDSEI) into cascades.local
-> dept OUs -> drives. Deferred to tomorrow (Howard).
- NurseAssist: offline; verify it is a real distinct machine vs a duplicate of Assistnurse-pc before
upgrading/joining.
- SALES4-PC: bypassed (Tamra departing); decide repurpose vs upgrade.
- Broader Cascades migration workstreams continue per REMAINING-WORK-PLAN.md.
## Reference Information
- Syncro ticket #32466 (id 113090740): https://computerguru.syncromsp.com/tickets/113090740
- Invoice #1650806091 — total $215.23 (2x $99 keys + AZ tax; 1.0h labor applied to block).
- Syncro product "Windows Pro Upgrade" id 23571919 ($99, taxable).
- Generic Pro key: VK7JG-NPHTM-C97JM-9MPGT-3V66T (edition flip only, does not activate).