diff --git a/.claude/commands/syncro.md b/.claude/commands/syncro.md index dd62f58f..93672074 100644 --- a/.claude/commands/syncro.md +++ b/.claude/commands/syncro.md @@ -629,7 +629,7 @@ COMMENT_RESP=$(curl -s -X POST "${BASE}/tickets/${ID}/comment?api_key=${API_KEY} } JSON ) -COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id') +COMMENT_ID=$(echo "$COMMENT_RESP" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') ``` - `hidden: true` = internal only (customer can't see) @@ -1275,7 +1275,7 @@ COMMENT_RESP=$(curl -s -X POST "${BASE}/tickets/${ID}/comment?api_key=${API_KEY} {"subject": "Resolution", "body": "", "hidden": false, "do_not_email": false} JSON ) -COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id') +COMMENT_ID=$(echo "$COMMENT_RESP" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') # STOP if null: GET ticket, check .ticket.comments[] by subject # 2. Add line item — response is FLAT: {"id": N, ...} diff --git a/.claude/skills/coord/scripts/coord.py b/.claude/skills/coord/scripts/coord.py index d60a29ff..2736fc0d 100644 --- a/.claude/skills/coord/scripts/coord.py +++ b/.claude/skills/coord/scripts/coord.py @@ -236,7 +236,7 @@ def c_todo_list(a): extra += f" @{t['assigned_to_user']}" if t.get("project_key"): extra += f" ({t['project_key']})" - print(f" [{t.get('status'):<7}] {str(t.get('id',''))[:8]} {str(t.get('text',''))[:88]}{extra}") + print(f" [{t.get('status'):<7}] {t.get('id','')} {str(t.get('text',''))[:88]}{extra}") def c_todo_done(a): diff --git a/.claude/skills/remediation-tool/scripts/get-token.sh b/.claude/skills/remediation-tool/scripts/get-token.sh index dde676a0..a7a74b0f 100755 --- a/.claude/skills/remediation-tool/scripts/get-token.sh +++ b/.claude/skills/remediation-tool/scripts/get-token.sh @@ -134,13 +134,22 @@ if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -55 2>/dev/null) ]]; fi # Locate vault repo via .claude/identity.json (per-machine, gitignored). -# Falls back to VAULT_PATH env var if set. +# Resolution order for identity.json: +# 1. Repo-local: /.claude/identity.json +# 2. Home dir: $HOME/.claude/identity.json +# Falls back to VAULT_ROOT_ENV / VAULT_DIR env vars or D:/vault default if vault_path missing. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json" +IDENTITY_FILE="" +for _candidate in "$CLAUDETOOLS_ROOT/.claude/identity.json" "$HOME/.claude/identity.json"; do + if [[ -f "$_candidate" ]]; then + IDENTITY_FILE="$_candidate" + break + fi +done VAULT_ROOT="${VAULT_ROOT_ENV:-}" -if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then +if [[ -z "$VAULT_ROOT" && -n "$IDENTITY_FILE" ]]; then if command -v jq >/dev/null 2>&1; then VAULT_ROOT=$(jq -r '.vault_path // empty' "$IDENTITY_FILE" 2>/dev/null) fi @@ -153,7 +162,15 @@ if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then done fi fi -[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: vault_path not set in $IDENTITY_FILE and VAULT_ROOT_ENV env var not set" >&2; exit 3; } +# Fallback: VAULT_DIR env var, then sensible default (D:/vault). +# Note: VAULT_PATH is already used in this script for the sops file path within the vault. +if [[ -z "$VAULT_ROOT" ]]; then + VAULT_ROOT="${VAULT_DIR:-}" +fi +if [[ -z "$VAULT_ROOT" ]]; then + VAULT_ROOT="D:/vault" + echo "[WARNING] vault_path not in identity.json and no VAULT_ROOT_ENV/VAULT_DIR env var; defaulting to $VAULT_ROOT" >&2 +fi [[ ! -d "$VAULT_ROOT" ]] && { echo "ERROR: vault not found at $VAULT_ROOT (check vault_path in $IDENTITY_FILE)" >&2; exit 3; } SOPS_FILE="$VAULT_ROOT/$VAULT_PATH" diff --git a/.claude/skills/remediation-tool/scripts/reset-password.sh b/.claude/skills/remediation-tool/scripts/reset-password.sh index ea5a2d2e..e0a7c835 100644 --- a/.claude/skills/remediation-tool/scripts/reset-password.sh +++ b/.claude/skills/remediation-tool/scripts/reset-password.sh @@ -92,7 +92,34 @@ fi cleanup() { if [[ -n "$CREATED_ASSIGNMENT" ]]; then DC=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "${GH[@]}" "$G/roleManagement/directory/roleAssignments/$CREATED_ASSIGNMENT") - if [[ "$DC" == "204" ]]; then echo "[info] removed JIT role assignment (de-elevated)"; else echo "[WARNING] failed to remove JIT role assignment $CREATED_ASSIGNMENT (HTTP $DC) - REMOVE MANUALLY" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: failed to remove JIT Privileged Auth Admin role - standing privilege left behind, REMOVE MANUALLY" --context "tenant=$TENANT_ID assignment=$CREATED_ASSIGNMENT http=$DC" >/dev/null 2>&1 || true; fi + if [[ "$DC" == "204" ]]; then + echo "[info] removed JIT role assignment (de-elevated)" + else + # Graph blocks an SP from removing its own active role assignment (self-removal). + # The Tenant Admin SP is the only principal with RoleManagement.ReadWrite.Directory, + # so no other SP in our app suite can remove it either. A Global Admin must do it. + echo "" >&2 + echo "[WARNING] ================================================================" >&2 + echo "[WARNING] STANDING PRIVILEGE LEFT BEHIND — MANUAL REMOVAL REQUIRED" >&2 + echo "[WARNING] ================================================================" >&2 + echo "[WARNING] The Tenant Admin service principal now holds Privileged" >&2 + echo "[WARNING] Authentication Administrator in tenant $TENANT_ID." >&2 + echo "[WARNING] Graph returned HTTP $DC on self-removal (an SP cannot remove" >&2 + echo "[WARNING] its own active directory-role assignment)." >&2 + echo "[WARNING]" >&2 + echo "[WARNING] To remove via Graph (requires a Global Admin token or a" >&2 + echo "[WARNING] different principal with RoleManagement.ReadWrite.Directory):" >&2 + echo "[WARNING]" >&2 + echo "[WARNING] DELETE $G/roleManagement/directory/roleAssignments/$CREATED_ASSIGNMENT" >&2 + echo "[WARNING]" >&2 + echo "[WARNING] To remove via Azure Portal:" >&2 + echo "[WARNING] 1. Sign in as Global Admin at https://entra.microsoft.com" >&2 + echo "[WARNING] 2. Roles and administrators > Privileged Authentication Administrator" >&2 + echo "[WARNING] 3. Find 'ComputerGuru - Tenant Admin' (SP id: $SPID)" >&2 + echo "[WARNING] 4. Remove the assignment" >&2 + echo "[WARNING] ================================================================" >&2 + bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: self-removal blocked — standing PAA role left on Tenant Admin SP (HTTP $DC). A Global Admin must DELETE roleAssignments/$CREATED_ASSIGNMENT" --context "tenant=$TENANT_ID assignment=$CREATED_ASSIGNMENT sp=$SPID http=$DC" >/dev/null 2>&1 || true + fi fi } trap cleanup EXIT diff --git a/.claude/skills/unifi-wifi/scripts/gw-sitemanager.sh b/.claude/skills/unifi-wifi/scripts/gw-sitemanager.sh index 93554fe4..63c4d6e7 100644 --- a/.claude/skills/unifi-wifi/scripts/gw-sitemanager.sh +++ b/.claude/skills/unifi-wifi/scripts/gw-sitemanager.sh @@ -260,7 +260,7 @@ for x in sorted(d,key=lambda a:(a.get('type',''),a.get('name') or '')): " || exit 1 ;; raw) [ -z "$RAWPATH" ] && { echo "usage: $SELF net raw " >&2; exit 2; } - api "$CB$RAWPATH" | "$PY" -c "import json,sys; print(json.dumps(json.load(sys.stdin),indent=1)[:5000])" || exit 1 ;; + api "$CB$RAWPATH" | "$PY" -c "import json,sys; print(json.dumps(json.load(sys.stdin),indent=1))" || exit 1 ;; *) echo "[$SELF] net subcommand must be radios|clients|devices|raw" >&2; exit 2 ;; esac ;; diff --git a/projects/discord-bot b/projects/discord-bot index 56938bbf..bd6009c4 160000 --- a/projects/discord-bot +++ b/projects/discord-bot @@ -1 +1 @@ -Subproject commit 56938bbf5ad18a8afe9712891d5eabbc57c5fb10 +Subproject commit bd6009c478371cca77df24751a0cb4bd7ce3bf51 diff --git a/projects/msp-tools/guru-rmm b/projects/msp-tools/guru-rmm index c924081c..38971315 160000 --- a/projects/msp-tools/guru-rmm +++ b/projects/msp-tools/guru-rmm @@ -1 +1 @@ -Subproject commit c924081c833f06e7f501c92adc79451ab1542010 +Subproject commit 38971315f1c56ccc31eaead4ffb7304288d4dde2