harness: fleet-wide functional-error + correction + friction logging
Add .claude/scripts/log-skill-error.sh — the canonical agent error log helper (writes errorlog.md in DATE | MACHINE | skill | [type] error format, soft-fails). Three categories: execution failures (default), user corrections (--correction), and preventable self-inflicted friction (--friction; cite ref= when it repeats a documented gotcha). Goal: stop paying tokens twice for the same avoidable mistake. - CLAUDE.md: make logging mandatory for all skills + corrections + friction. - skill-creator: new skills must wire in the helper (guidance + checklist). - Retrofit every skill script's genuine failure branches to call the helper (b2/bitdefender/mailprotector/packetdial/coord python CLIs; remediation-tool + onboard365 bash; vault, rmm-auth, post-bot-alert, agy, grok, 1password, run-onboarding-diagnostic). Handled conditions + self-tests left alone. - errorlog.md: broaden header to cover skills + harness + corrections; seed this session's corrections (INKY, Mail.Send token-audience, omnibox-strictness) and friction (git-bash /tmp, env-persistence, argv-limit, PowerShell var-case). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,9 @@ production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
|
|||||||
- **SSH:** system OpenSSH (`C:\Windows\System32\OpenSSH\ssh.exe`), never Git-for-Windows SSH.
|
- **SSH:** system OpenSSH (`C:\Windows\System32\OpenSSH\ssh.exe`), never Git-for-Windows SSH.
|
||||||
- **Data integrity:** never placeholder/fake data — check vault, wiki, or ask.
|
- **Data integrity:** never placeholder/fake data — check vault, wiki, or ask.
|
||||||
- **Hard-to-reverse or outward-facing actions:** confirm first (per-action, per-session).
|
- **Hard-to-reverse or outward-facing actions:** confirm first (per-action, per-session).
|
||||||
- **Error logging:** when a task errors during execution (failed command, skill, or tool call), append a brief entry to `errorlog.md` (repo root) — date, machine (from `identity.json`), the command/skill that failed, and the error. One line or two; just enough to spot patterns. These feed skill + harness improvements. Don't log expected/handled conditions, only genuine failures.
|
- **Error logging (mandatory, all skills):** when a task or skill hits a GENUINE functional error during execution (failed command, API/auth failure, unexpected API response, tool call), record it to `errorlog.md` (repo root) via the canonical helper — never hand-format: `bash .claude/scripts/log-skill-error.sh "<skill/command>" "<brief error>" [--context "k=v ..."]`. It stamps UTC date + machine (from `identity.json`) and inserts in the standard `YYYY-MM-DD | MACHINE | skill | error` format (newest on top) for later skill **linting**, and soft-fails so it never breaks the caller. **Every skill MUST call it at its failure branches**; you (main loop) call it after any skill/command genuinely fails. Do NOT log expected/handled conditions (no search matches, no unread messages, a user declining a prompt) — only real failures worth spotting a pattern. Python skills shell out to the same helper.
|
||||||
|
- **Log user corrections too (`--correction`):** when the user CORRECTS an improper assumption or wrong approach you took (e.g. "don't use INKY unless onboarded", "EXO already has Mail.Send", "I don't need an exact match"), log it: `bash .claude/scripts/log-skill-error.sh "<skill/context>" "assumed X; correct is Y" --correction`. These are the highest-value entries — they surface recurring bad assumptions so we can train them out of the system. If the correction is a durable preference, ALSO save it as a `feedback` memory (the two are complementary: memory fixes future behavior, errorlog tracks the pattern for linting).
|
||||||
|
- **Log preventable friction too (`--friction`):** any time you waste tokens on a preventable, repeatable self-inflicted error — harness/env/tool misuse (Git-Bash `/tmp` path mismatch, shell env not persisting between Bash calls, passing huge args on the command line, PowerShell var case-collisions, etc.) — log it: `bash .claude/scripts/log-skill-error.sh "<context e.g. bash/env>" "what wasted tokens + the fix" --friction [--context "ref=<memory-or-rule>"]`. **If it repeats something already in memory or CLAUDE.md, that's the highest-value entry** — it means a rule/memory isn't working; cite the ref. This log is the corpus we lint to build better CLAUDE.md rules and to clean stale/misleading memory. Goal: stop paying twice for the same mistake.
|
||||||
- **Windows:** ensure `bash` resolves to Git-for-Windows MSYS bash, not the WSL stub; write
|
- **Windows:** ensure `bash` resolves to Git-for-Windows MSYS bash, not the WSL stub; write
|
||||||
`.claude/current-mode` with a relative/forward-slash path only (never a backslash Windows
|
`.claude/current-mode` with a relative/forward-slash path only (never a backslash Windows
|
||||||
path). Detail + fixes: EXTENDED.
|
path). Detail + fixes: EXTENDED.
|
||||||
|
|||||||
@@ -78,7 +78,11 @@ if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
|||||||
ENV_FILE="$ROOT/projects/discord-bot/.env"
|
ENV_FILE="$ROOT/projects/discord-bot/.env"
|
||||||
[ -f "$ENV_FILE" ] && TOKEN="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$ENV_FILE" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')"
|
[ -f "$ENV_FILE" ] && TOKEN="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$ENV_FILE" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')"
|
||||||
fi
|
fi
|
||||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "[ERROR] no bot token (vault + .env both empty)" >&2; exit 2; fi
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||||
|
echo "[ERROR] no bot token (vault + .env both empty)" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "no Discord bot token (vault projects/discord-bot/bot-token + .env both empty)" >/dev/null 2>&1
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
|
||||||
|
|
||||||
@@ -87,7 +91,11 @@ if [ "$MODE" = "dm" ]; then
|
|||||||
DM="$(printf '%s' "$(jq -nc --arg r "$TARGET" '{recipient_id:$r}')" | \
|
DM="$(printf '%s' "$(jq -nc --arg r "$TARGET" '{recipient_id:$r}')" | \
|
||||||
curl -s -m 15 "${auth[@]}" -X POST "$API/users/@me/channels" --data-binary @-)"
|
curl -s -m 15 "${auth[@]}" -X POST "$API/users/@me/channels" --data-binary @-)"
|
||||||
CHID="$(printf '%s' "$DM" | jq -r '.id // empty' 2>/dev/null)"
|
CHID="$(printf '%s' "$DM" | jq -r '.id // empty' 2>/dev/null)"
|
||||||
if [ -z "$CHID" ]; then echo "[ERROR] could not open DM channel for $LABEL: $DM" >&2; exit 3; fi
|
if [ -z "$CHID" ]; then
|
||||||
|
echo "[ERROR] could not open DM channel for $LABEL: $DM" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "failed to open DM channel for $LABEL" --context "resp=${DM:0:80}" >/dev/null 2>&1
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
TARGET="$CHID"
|
TARGET="$CHID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -103,4 +111,5 @@ if [ "$HTTP" = "200" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response} — ${BODY}" >&2
|
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response} — ${BODY}" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1
|
||||||
exit 3
|
exit 3
|
||||||
|
|||||||
89
.claude/scripts/log-skill-error.sh
Normal file
89
.claude/scripts/log-skill-error.sh
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# log-skill-error.sh — append an entry to errorlog.md in the canonical format,
|
||||||
|
# for later linting that feeds skill fixes, CLAUDE.md rules, and memory cleanup.
|
||||||
|
#
|
||||||
|
# Despite the name this is the GENERAL agent error/correction/friction log — it
|
||||||
|
# captures three things (see --type below):
|
||||||
|
# 1. skill/command FUNCTIONAL failures (API/auth/unexpected-response/bad-exit)
|
||||||
|
# 2. user CORRECTIONS of an improper assumption I made (--correction)
|
||||||
|
# 3. preventable self-inflicted FRICTION that wasted tokens (--friction) —
|
||||||
|
# harness/env/tool misuse, ESPECIALLY a repeat of an already-documented
|
||||||
|
# gotcha (that means a rule or memory isn't working and needs strengthening)
|
||||||
|
#
|
||||||
|
# Do NOT call it for expected/handled conditions (a search with no matches, a
|
||||||
|
# "no unread messages", a user declining a prompt) — only real, preventable,
|
||||||
|
# pattern-worthy events.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash log-skill-error.sh <skill-or-command> "<brief error>"
|
||||||
|
# echo "<brief error>" | bash log-skill-error.sh <skill-or-command>
|
||||||
|
# bash log-skill-error.sh <skill> "<error>" --context "op=send id=123 http=403"
|
||||||
|
# bash log-skill-error.sh <skill/context> "<what I wrongly assumed + the correction>" --correction
|
||||||
|
#
|
||||||
|
# Categories (all feed the lint that improves skills, CLAUDE.md, and memory):
|
||||||
|
# (default) execution failure — API/auth failure, unexpected response, bad exit.
|
||||||
|
# --correction — the USER corrected an improper assumption/approach I made.
|
||||||
|
# --friction — preventable self-inflicted error that wasted tokens (harness/env/
|
||||||
|
# tool misuse). If it repeats a documented gotcha, note it in
|
||||||
|
# --context (e.g. ref=feedback_tmp_path_windows) — that's the signal
|
||||||
|
# a rule/memory needs strengthening.
|
||||||
|
# (--type <other> also supported; tags the error column as [<type>].)
|
||||||
|
# bash log-skill-error.sh <context> "<what wasted tokens + the fix>" --friction --context "ref=<memory>"
|
||||||
|
#
|
||||||
|
# Writes: YYYY-MM-DD | MACHINE | <skill> | [<type>] <error> [ctx: <context>]
|
||||||
|
# (newest entry inserted at the top, just under the append marker).
|
||||||
|
#
|
||||||
|
# Soft-fail by design: this NEVER breaks the caller. Missing log, missing jq,
|
||||||
|
# empty message -> prints a [WARN] to stderr and exits 0.
|
||||||
|
set -u
|
||||||
|
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
|
||||||
|
SKILL="${1:-unknown}"; shift || true
|
||||||
|
CONTEXT=""
|
||||||
|
ETYPE="" # "" / exec = execution failure; "correction" = user corrected a bad assumption
|
||||||
|
ARGS=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--context) CONTEXT="${2:-}"; shift 2;;
|
||||||
|
--type) ETYPE="${2:-}"; shift 2;;
|
||||||
|
--correction) ETYPE="correction"; shift;;
|
||||||
|
--friction) ETYPE="friction"; shift;;
|
||||||
|
*) ARGS+=("$1"); shift;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
MSG="${ARGS[*]:-}"
|
||||||
|
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
|
||||||
|
if [ -z "$MSG" ]; then echo "[WARN] log-skill-error: empty message, nothing logged" >&2; exit 0; fi
|
||||||
|
|
||||||
|
LOG="$ROOT/errorlog.md"
|
||||||
|
if [ ! -f "$LOG" ]; then echo "[WARN] log-skill-error: $LOG not found" >&2; exit 0; fi
|
||||||
|
|
||||||
|
DATE="$(date -u +%F)"
|
||||||
|
IDF="$ROOT/.claude/identity.json"
|
||||||
|
MACHINE=""
|
||||||
|
if command -v jq >/dev/null 2>&1 && [ -f "$IDF" ]; then
|
||||||
|
MACHINE="$(jq -r '.machine_name // .hostname // empty' "$IDF" 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
[ -z "$MACHINE" ] && MACHINE="$(hostname 2>/dev/null || echo unknown)"
|
||||||
|
|
||||||
|
# normalize whitespace/newlines so each entry is one line
|
||||||
|
MSG="$(printf '%s' "$MSG" | tr '\n' ' ' | sed 's/[[:space:]]\{1,\}/ /g; s/^ //; s/ $//')"
|
||||||
|
[ -n "$CONTEXT" ] && MSG="$MSG [ctx: $CONTEXT]"
|
||||||
|
# Tag non-execution categories at the start of the error column for easy linting
|
||||||
|
# (e.g. grep "\[correction\]" errorlog.md to surface improper-assumption patterns).
|
||||||
|
if [ -n "$ETYPE" ] && [ "$ETYPE" != "exec" ]; then MSG="[$ETYPE] $MSG"; fi
|
||||||
|
ENTRY="$DATE | $MACHINE | $SKILL | $MSG"
|
||||||
|
|
||||||
|
MARK="<!-- Append entries below this line -->"
|
||||||
|
TMP="$LOG.tmp.$$"
|
||||||
|
if awk -v entry="$ENTRY" -v mark="$MARK" '
|
||||||
|
{ print }
|
||||||
|
($0==mark && !done) { print ""; print entry; done=1 }
|
||||||
|
END { if (!done) { print ""; print entry } } # marker missing -> append at end
|
||||||
|
' "$LOG" > "$TMP" 2>/dev/null && mv "$TMP" "$LOG" 2>/dev/null; then
|
||||||
|
echo "[OK] logged skill error to errorlog.md ($SKILL)"
|
||||||
|
else
|
||||||
|
rm -f "$TMP" 2>/dev/null
|
||||||
|
echo "[WARN] log-skill-error: could not write $LOG" >&2
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
@@ -86,4 +86,8 @@ if [ "$HTTP" = "200" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[WARNING] post-bot-alert: Discord returned ${HTTP:-no-response} — ${BODY}" >&2
|
echo "[WARNING] post-bot-alert: Discord returned ${HTTP:-no-response} — ${BODY}" >&2
|
||||||
|
# Log the Discord POST failure (non-200 / unreachable) once. Do NOT route this
|
||||||
|
# through post-bot-alert itself — that would recurse; log-skill-error.sh only
|
||||||
|
# writes to errorlog.md. Soft-fail preserved: this never changes the exit 0.
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "post-bot-alert" "Discord POST failed (non-200/unreachable)" --context "channel=${CHANNEL_NAME} http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1 || true
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -11,19 +11,27 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
IDENTITY_FILE="$REPO_ROOT/.claude/identity.json"
|
IDENTITY_FILE="$REPO_ROOT/.claude/identity.json"
|
||||||
|
|
||||||
|
# Functional-error logger. MUST stay silent on stdout (this script's stdout is
|
||||||
|
# eval'd by the caller) — log-skill-error.sh prints only to stderr, and we
|
||||||
|
# redirect everything to /dev/null to be safe.
|
||||||
|
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "rmm-auth" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
if [ ! -f "$IDENTITY_FILE" ]; then
|
if [ ! -f "$IDENTITY_FILE" ]; then
|
||||||
|
_logerr "identity.json not found; RMM auth cannot resolve vault" --context "path=$IDENTITY_FILE"
|
||||||
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] identity.json not found' >&2"
|
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] identity.json not found' >&2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VAULT_PATH=$(jq -r '.vault_path // empty' "$IDENTITY_FILE")
|
VAULT_PATH=$(jq -r '.vault_path // empty' "$IDENTITY_FILE")
|
||||||
if [ -z "$VAULT_PATH" ]; then
|
if [ -z "$VAULT_PATH" ]; then
|
||||||
|
_logerr "vault_path not in identity.json; RMM auth failed" --context "path=$IDENTITY_FILE"
|
||||||
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] vault_path not in identity.json' >&2"
|
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] vault_path not in identity.json' >&2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VAULT_SH="$VAULT_PATH/scripts/vault.sh"
|
VAULT_SH="$VAULT_PATH/scripts/vault.sh"
|
||||||
if [ ! -f "$VAULT_SH" ]; then
|
if [ ! -f "$VAULT_SH" ]; then
|
||||||
|
_logerr "vault.sh not found at resolved vault_path; RMM auth failed" --context "path=$VAULT_SH"
|
||||||
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] vault.sh not found at $VAULT_SH' >&2"
|
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] vault.sh not found at $VAULT_SH' >&2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -35,6 +43,7 @@ RMM_EMAIL=$(bash "$VAULT_SH" get-field infrastructure/gururmm-server.sops.yaml c
|
|||||||
RMM_PASS=$(bash "$VAULT_SH" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-password 2>/dev/null)
|
RMM_PASS=$(bash "$VAULT_SH" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-password 2>/dev/null)
|
||||||
|
|
||||||
if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ]; then
|
if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ]; then
|
||||||
|
_logerr "vault read of GuruRMM API credentials failed (empty email/password)" --context "entry=infrastructure/gururmm-server.sops.yaml"
|
||||||
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] Failed to get RMM credentials from vault' >&2"
|
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] Failed to get RMM credentials from vault' >&2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -45,6 +54,7 @@ JWT=$(curl -s -X POST "$RMM_URL/api/auth/login" -H "Content-Type: application/js
|
|||||||
TOKEN=$(echo "$JWT" | jq -r '.token // empty')
|
TOKEN=$(echo "$JWT" | jq -r '.token // empty')
|
||||||
|
|
||||||
if [ -z "$TOKEN" ]; then
|
if [ -z "$TOKEN" ]; then
|
||||||
|
_logerr "RMM login failed (no token returned from /api/auth/login)" --context "url=$RMM_URL resp=${JWT:0:80}"
|
||||||
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] RMM login failed: $JWT' >&2"
|
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] RMM login failed: $JWT' >&2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -37,9 +37,17 @@ if [ -z "$QUERY" ] && [ -z "$CLIENT" ] && [ "$LISTC" -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh" 2>/dev/null)" >/dev/null
|
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
|
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then
|
||||||
|
echo "[ERROR] RMM auth failed (see rmm-auth.sh)" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "rmm-search" "RMM auth failed via rmm-auth.sh (no TOKEN/RMM)" >/dev/null 2>&1
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
AGENTS=$(curl -s "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")
|
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
|
if [ -z "$AGENTS" ] || [ "${AGENTS:0:1}" != "[" ]; then
|
||||||
|
echo "[ERROR] could not fetch agents: ${AGENTS:0:160}" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "rmm-search" "GET /api/agents returned non-array/empty" --context "resp=${AGENTS:0:80}" >/dev/null 2>&1
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Pipe agents on stdin (payload too large for argv on Windows); flags via env.
|
# 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" \
|
printf '%s' "$AGENTS" | QUERY="$QUERY" CLIENT="$CLIENT" ONLINE="$ONLINE" JSON="$JSON" LISTC="$LISTC" LIMIT="$LIMIT" \
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ PROBE="$SCRIPT_DIR/onboarding-diagnostic.ps1"
|
|||||||
ALERT="$REPO_ROOT/.claude/scripts/post-bot-alert.sh"
|
ALERT="$REPO_ROOT/.claude/scripts/post-bot-alert.sh"
|
||||||
RMM="http://172.16.3.30:3001"
|
RMM="http://172.16.3.30:3001"
|
||||||
|
|
||||||
|
# Functional-error logger (skill name "rmm-diagnose"). Logs genuine operational
|
||||||
|
# failures (auth, vault, dispatch) — NOT the RED/AMBER/GREEN diagnostic grade,
|
||||||
|
# which is a normal by-design result. Soft-fails; never breaks the run.
|
||||||
|
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "rmm-diagnose" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
if [ ! -f "$PROBE" ]; then
|
if [ ! -f "$PROBE" ]; then
|
||||||
echo "[ERROR] Probe script not found: $PROBE" >&2
|
echo "[ERROR] Probe script not found: $PROBE" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -65,6 +70,7 @@ RMM_PASS="$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml cred
|
|||||||
|
|
||||||
if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ] || [ "$RMM_EMAIL" = "null" ]; then
|
if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ] || [ "$RMM_EMAIL" = "null" ]; then
|
||||||
echo "[ERROR] Could not read GuruRMM credentials from vault (infrastructure/gururmm-server.sops.yaml)" >&2
|
echo "[ERROR] Could not read GuruRMM credentials from vault (infrastructure/gururmm-server.sops.yaml)" >&2
|
||||||
|
_logerr "vault read of GuruRMM credentials failed (empty/null)" --context "entry=infrastructure/gururmm-server.sops.yaml"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -75,6 +81,7 @@ TOKEN="$(curl -s -m 30 -X POST "$RMM/api/auth/login" \
|
|||||||
|
|
||||||
if [ -z "$TOKEN" ]; then
|
if [ -z "$TOKEN" ]; then
|
||||||
echo "[ERROR] RMM login failed (no token returned)" >&2
|
echo "[ERROR] RMM login failed (no token returned)" >&2
|
||||||
|
_logerr "RMM login failed (no token returned from /api/auth/login)" --context "url=$RMM"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "[OK] Authenticated to GuruRMM"
|
echo "[OK] Authenticated to GuruRMM"
|
||||||
@@ -85,6 +92,7 @@ echo "[OK] Authenticated to GuruRMM"
|
|||||||
AGENTS="$(curl -s -m 30 "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")"
|
AGENTS="$(curl -s -m 30 "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")"
|
||||||
if [ -z "$AGENTS" ] || ! echo "$AGENTS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
if [ -z "$AGENTS" ] || ! echo "$AGENTS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
echo "[ERROR] Could not retrieve agent list" >&2
|
echo "[ERROR] Could not retrieve agent list" >&2
|
||||||
|
_logerr "GET /api/agents returned non-array/empty" --context "resp=${AGENTS:0:80}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -263,6 +271,7 @@ PS
|
|||||||
CH_STATUS="$(echo "$CH_RESULT" | jq -r '.status')"
|
CH_STATUS="$(echo "$CH_RESULT" | jq -r '.status')"
|
||||||
if [ "$CH_STATUS" != "completed" ]; then
|
if [ "$CH_STATUS" != "completed" ]; then
|
||||||
echo "[ERROR] Chunk $IDX upload failed: status=$CH_STATUS stderr=$(echo "$CH_RESULT" | jq -r '.stderr' | head -c 200)" >&2
|
echo "[ERROR] Chunk $IDX upload failed: status=$CH_STATUS stderr=$(echo "$CH_RESULT" | jq -r '.stderr' | head -c 200)" >&2
|
||||||
|
_logerr "probe chunk upload failed" --context "host=$AGENT_HOST idx=$IDX/$N_CHUNKS status=$CH_STATUS"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "[OK] Uploaded chunk $IDX/$N_CHUNKS"
|
echo "[OK] Uploaded chunk $IDX/$N_CHUNKS"
|
||||||
@@ -288,7 +297,7 @@ try {
|
|||||||
}
|
}
|
||||||
PS
|
PS
|
||||||
|
|
||||||
RESULT="$(dispatch_one "$RUN_SCRIPT" "$EXEC_TIMEOUT")" || { echo "[ERROR] Probe execution dispatch failed" >&2; exit 1; }
|
RESULT="$(dispatch_one "$RUN_SCRIPT" "$EXEC_TIMEOUT")" || { echo "[ERROR] Probe execution dispatch failed" >&2; _logerr "probe execution dispatch failed" --context "host=$AGENT_HOST agent=$AGENT_ID"; exit 1; }
|
||||||
CMD_ID="$(cat "$WORK_DIR/last_cmd_id" 2>/dev/null || echo unknown)"
|
CMD_ID="$(cat "$WORK_DIR/last_cmd_id" 2>/dev/null || echo unknown)"
|
||||||
|
|
||||||
FINAL_STATUS="$(echo "$RESULT" | jq -r '.status // empty')"
|
FINAL_STATUS="$(echo "$RESULT" | jq -r '.status // empty')"
|
||||||
@@ -317,6 +326,7 @@ if [ -z "$DIAG_JSON" ] || ! echo "$DIAG_JSON" | jq -e '.host' >/dev/null 2>&1; t
|
|||||||
fi
|
fi
|
||||||
echo "--- stdout (first 60 lines) ---" >&2
|
echo "--- stdout (first 60 lines) ---" >&2
|
||||||
printf '%s\n' "$STDOUT" | head -60 >&2
|
printf '%s\n' "$STDOUT" | head -60 >&2
|
||||||
|
_logerr "could not extract valid diagnostic JSON from probe output" --context "host=$AGENT_HOST status=$FINAL_STATUS exit=$EXIT_CODE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||||
|
|
||||||
|
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "vault" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
if [[ ! -f "$IDENTITY_FILE" ]]; then
|
if [[ ! -f "$IDENTITY_FILE" ]]; then
|
||||||
echo "[ERROR] .claude/identity.json not found at $IDENTITY_FILE" >&2
|
echo "[ERROR] .claude/identity.json not found at $IDENTITY_FILE" >&2
|
||||||
echo " Run onboarding to create it, or add vault_path manually." >&2
|
echo " Run onboarding to create it, or add vault_path manually." >&2
|
||||||
|
_logerr "identity.json not found; vault read cannot resolve vault_path" --context "path=$IDENTITY_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@ fi
|
|||||||
if [[ -z "$VAULT_ROOT" ]]; then
|
if [[ -z "$VAULT_ROOT" ]]; then
|
||||||
echo "[ERROR] vault_path not set in $IDENTITY_FILE" >&2
|
echo "[ERROR] vault_path not set in $IDENTITY_FILE" >&2
|
||||||
echo " Add: \"vault_path\": \"/path/to/vault\"" >&2
|
echo " Add: \"vault_path\": \"/path/to/vault\"" >&2
|
||||||
|
_logerr "vault_path not set in identity.json; vault read failed" --context "path=$IDENTITY_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -48,6 +52,7 @@ REAL_VAULT_SH="$VAULT_ROOT/scripts/vault.sh"
|
|||||||
if [[ ! -f "$REAL_VAULT_SH" ]]; then
|
if [[ ! -f "$REAL_VAULT_SH" ]]; then
|
||||||
echo "[ERROR] vault.sh not found at $REAL_VAULT_SH" >&2
|
echo "[ERROR] vault.sh not found at $REAL_VAULT_SH" >&2
|
||||||
echo " Check vault_path in $IDENTITY_FILE" >&2
|
echo " Check vault_path in $IDENTITY_FILE" >&2
|
||||||
|
_logerr "real vault.sh not found at resolved vault_path; vault read failed" --context "path=$REAL_VAULT_SH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Functional-error logger (skill name "1password"); 4 levels up to the ClaudeTools repo.
|
||||||
|
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "1password" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
VAULT=""
|
VAULT=""
|
||||||
ITEM=""
|
ITEM=""
|
||||||
OUTPUT=".env"
|
OUTPUT=".env"
|
||||||
@@ -35,6 +39,7 @@ done
|
|||||||
# Check op is available
|
# Check op is available
|
||||||
if ! command -v op &>/dev/null; then
|
if ! command -v op &>/dev/null; then
|
||||||
echo "❌ 1Password CLI (op) not found. Install: https://developer.1password.com/docs/cli/get-started/"
|
echo "❌ 1Password CLI (op) not found. Install: https://developer.1password.com/docs/cli/get-started/"
|
||||||
|
_logerr "op CLI not found on PATH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Functional-error logger (skill name "1password"); 4 levels up to the ClaudeTools repo.
|
||||||
|
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "1password" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
VAULT="Dev"
|
VAULT="Dev"
|
||||||
ITEM=""
|
ITEM=""
|
||||||
UPDATE=false
|
UPDATE=false
|
||||||
@@ -88,14 +92,16 @@ ALL_FIELDS=("${OP_FIELDS[@]+"${OP_FIELDS[@]}"}" "${SECRET_VALUES[@]+"${SECRET_VA
|
|||||||
echo "Saving to 1Password..."
|
echo "Saving to 1Password..."
|
||||||
|
|
||||||
if $UPDATE; then
|
if $UPDATE; then
|
||||||
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}"
|
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}" \
|
||||||
|
|| { rc=$?; _logerr "op item edit failed (update MCP creds)" --context "item=$ITEM vault=$VAULT rc=$rc"; exit $rc; }
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Updated '$ITEM' in vault '$VAULT'"
|
echo "✅ Updated '$ITEM' in vault '$VAULT'"
|
||||||
else
|
else
|
||||||
# Try create, fall back to update if already exists
|
# Try create, fall back to update if already exists
|
||||||
if op item get "$ITEM" --vault "$VAULT" &>/dev/null 2>&1; then
|
if op item get "$ITEM" --vault "$VAULT" &>/dev/null 2>&1; then
|
||||||
echo " Item already exists — updating instead..."
|
echo " Item already exists — updating instead..."
|
||||||
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}"
|
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}" \
|
||||||
|
|| { rc=$?; _logerr "op item edit failed (update MCP creds)" --context "item=$ITEM vault=$VAULT rc=$rc"; exit $rc; }
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Updated '$ITEM' in vault '$VAULT'"
|
echo "✅ Updated '$ITEM' in vault '$VAULT'"
|
||||||
else
|
else
|
||||||
@@ -103,7 +109,8 @@ else
|
|||||||
--category API_CREDENTIAL \
|
--category API_CREDENTIAL \
|
||||||
--title "$ITEM" \
|
--title "$ITEM" \
|
||||||
--vault "$VAULT" \
|
--vault "$VAULT" \
|
||||||
"${ALL_FIELDS[@]}"
|
"${ALL_FIELDS[@]}" \
|
||||||
|
|| { rc=$?; _logerr "op item create failed (MCP creds)" --context "item=$ITEM vault=$VAULT rc=$rc"; exit $rc; }
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Created '$ITEM' in vault '$VAULT'"
|
echo "✅ Created '$ITEM' in vault '$VAULT'"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Functional-error logger (skill name "1password"); 4 levels up to the ClaudeTools repo.
|
||||||
|
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "1password" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
TITLE=""
|
TITLE=""
|
||||||
FIELD="credential"
|
FIELD="credential"
|
||||||
VALUE=""
|
VALUE=""
|
||||||
@@ -67,7 +71,8 @@ VAULT_FLAG=""
|
|||||||
|
|
||||||
if $UPDATE; then
|
if $UPDATE; then
|
||||||
echo "Updating '${FIELD}' in '${TITLE}'..."
|
echo "Updating '${FIELD}' in '${TITLE}'..."
|
||||||
op item edit "$TITLE" $VAULT_FLAG "${FIELD}[password]=${VALUE}"
|
op item edit "$TITLE" $VAULT_FLAG "${FIELD}[password]=${VALUE}" \
|
||||||
|
|| { rc=$?; _logerr "op item edit failed (update secret)" --context "item=$TITLE field=$FIELD vault=${VAULT:-default} rc=$rc"; exit $rc; }
|
||||||
echo "✅ Updated '${FIELD}' in '${TITLE}'"
|
echo "✅ Updated '${FIELD}' in '${TITLE}'"
|
||||||
else
|
else
|
||||||
echo "Creating '${TITLE}' in 1Password..."
|
echo "Creating '${TITLE}' in 1Password..."
|
||||||
@@ -76,7 +81,8 @@ else
|
|||||||
--title "$TITLE" \
|
--title "$TITLE" \
|
||||||
$VAULT_FLAG \
|
$VAULT_FLAG \
|
||||||
"${FIELD}[password]=${VALUE}" \
|
"${FIELD}[password]=${VALUE}" \
|
||||||
--format=json)
|
--format=json) \
|
||||||
|
|| { rc=$?; _logerr "op item create failed" --context "item=$TITLE category=$CATEGORY vault=${VAULT:-default} rc=$rc"; exit $rc; }
|
||||||
|
|
||||||
ITEM_ID=$(echo "$RESULT" | jq -r '.id')
|
ITEM_ID=$(echo "$RESULT" | jq -r '.id')
|
||||||
VAULT_NAME=$(echo "$RESULT" | jq -r '.vault.name')
|
VAULT_NAME=$(echo "$RESULT" | jq -r '.vault.name')
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ MODE="${1:-}"; shift 2>/dev/null || true
|
|||||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||||
PF="$TMP/prompt.txt"; OUT="$TMP/out.txt"; ERR="$TMP/err.txt"
|
PF="$TMP/prompt.txt"; OUT="$TMP/out.txt"; ERR="$TMP/err.txt"
|
||||||
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
|
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
|
||||||
|
# Functional-error logger (skill name "agy"); soft-fails, never breaks the caller.
|
||||||
|
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "agy" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
# gtimeout on macOS (brew coreutils), timeout elsewhere.
|
# gtimeout on macOS (brew coreutils), timeout elsewhere.
|
||||||
TIMEOUT_CMD="timeout"
|
TIMEOUT_CMD="timeout"
|
||||||
@@ -191,6 +193,7 @@ emit_or_fail() { # print .response, or retry once on a transient empty turn, el
|
|||||||
# Auth failures won't be fixed by a retry — report immediately.
|
# Auth failures won't be fixed by a retry — report immediately.
|
||||||
if auth_failed; then
|
if auth_failed; then
|
||||||
echo "[$SELF] Gemini auth error — run 'gemini' interactively and choose 'Login with Google', then retry." >&2
|
echo "[$SELF] Gemini auth error — run 'gemini' interactively and choose 'Login with Google', then retry." >&2
|
||||||
|
_logerr "gemini auth/login failure" --context "mode=$MODE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# Gemini occasionally returns an empty turn (or absorbs a 429 backoff into the
|
# Gemini occasionally returns an empty turn (or absorbs a 429 backoff into the
|
||||||
@@ -202,11 +205,13 @@ emit_or_fail() { # print .response, or retry once on a transient empty turn, el
|
|||||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
|
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
|
||||||
if auth_failed; then
|
if auth_failed; then
|
||||||
echo "[$SELF] Gemini auth error — run 'gemini' interactively and choose 'Login with Google', then retry." >&2
|
echo "[$SELF] Gemini auth error — run 'gemini' interactively and choose 'Login with Google', then retry." >&2
|
||||||
|
_logerr "gemini auth/login failure (after retry)" --context "mode=$MODE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo "[$SELF] no response from gemini. stderr tail:" >&2
|
echo "[$SELF] no response from gemini. stderr tail:" >&2
|
||||||
tail -3 "$ERR" >&2 2>/dev/null || true
|
tail -3 "$ERR" >&2 2>/dev/null || true
|
||||||
|
_logerr "gemini returned no response (empty after retry)" --context "mode=$MODE err=$(tail -1 "$ERR" 2>/dev/null | tr -d '\n' | cut -c1-80)"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,32 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from b2_client import B2Client, B2Error, RATE_PER_GB_USD, BYTES_PER_GB, BYTES_PER_GIB
|
from b2_client import B2Client, B2Error, RATE_PER_GB_USD, BYTES_PER_GB, BYTES_PER_GIB
|
||||||
|
|
||||||
|
|
||||||
|
def _log_skill_error(skill, msg, context=""):
|
||||||
|
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
|
||||||
|
try:
|
||||||
|
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||||
|
)
|
||||||
|
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
|
||||||
|
if not os.path.exists(h):
|
||||||
|
return
|
||||||
|
a = ["bash", h, skill, msg]
|
||||||
|
if context:
|
||||||
|
a += ["--context", context]
|
||||||
|
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _emit(obj, as_json: bool, table_fn=None) -> None:
|
def _emit(obj, as_json: bool, table_fn=None) -> None:
|
||||||
if as_json or table_fn is None:
|
if as_json or table_fn is None:
|
||||||
print(json.dumps(obj, indent=2, default=str))
|
print(json.dumps(obj, indent=2, default=str))
|
||||||
@@ -757,6 +777,10 @@ def main(argv=None) -> int:
|
|||||||
return rc if isinstance(rc, int) else 0
|
return rc if isinstance(rc, int) else 0
|
||||||
except B2Error as exc:
|
except B2Error as exc:
|
||||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
_log_skill_error("b2", f"{exc}",
|
||||||
|
context=f"cmd={getattr(args, 'command', '?')}"
|
||||||
|
+ (f" http={exc.status}" if exc.status else "")
|
||||||
|
+ (f" code={exc.code}" if exc.code else ""))
|
||||||
return 1
|
return 1
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return 130
|
return 130
|
||||||
|
|||||||
@@ -37,11 +37,31 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from gz_client import GravityZoneClient, GravityZoneError, GZEndpointSummary
|
from gz_client import GravityZoneClient, GravityZoneError, GZEndpointSummary
|
||||||
|
|
||||||
|
|
||||||
|
def _log_skill_error(skill, msg, context=""):
|
||||||
|
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
|
||||||
|
try:
|
||||||
|
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||||
|
)
|
||||||
|
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
|
||||||
|
if not os.path.exists(h):
|
||||||
|
return
|
||||||
|
a = ["bash", h, skill, msg]
|
||||||
|
if context:
|
||||||
|
a += ["--context", context]
|
||||||
|
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _emit(obj, as_json: bool, table_fn=None) -> None:
|
def _emit(obj, as_json: bool, table_fn=None) -> None:
|
||||||
if as_json or table_fn is None:
|
if as_json or table_fn is None:
|
||||||
print(json.dumps(obj, indent=2, default=_json_default))
|
print(json.dumps(obj, indent=2, default=_json_default))
|
||||||
@@ -554,6 +574,8 @@ def main(argv=None) -> int:
|
|||||||
return rc if isinstance(rc, int) else 0
|
return rc if isinstance(rc, int) else 0
|
||||||
except GravityZoneError as exc:
|
except GravityZoneError as exc:
|
||||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
_log_skill_error("bitdefender", f"{exc}",
|
||||||
|
context=f"cmd={getattr(args, 'command', '?')}")
|
||||||
return 1
|
return 1
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return 130
|
return 130
|
||||||
|
|||||||
@@ -21,7 +21,25 @@ Usage:
|
|||||||
coord.py lock release <id>
|
coord.py lock release <id>
|
||||||
coord.py lock list [--project KEY]
|
coord.py lock list [--project KEY]
|
||||||
"""
|
"""
|
||||||
import sys, os, json, argparse, urllib.request, urllib.error, urllib.parse
|
import sys, os, json, argparse, subprocess, urllib.request, urllib.error, urllib.parse
|
||||||
|
|
||||||
|
|
||||||
|
def _log_skill_error(skill, msg, context=""):
|
||||||
|
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
|
||||||
|
try:
|
||||||
|
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||||
|
)
|
||||||
|
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
|
||||||
|
if not os.path.exists(h):
|
||||||
|
return
|
||||||
|
a = ["bash", h, skill, msg]
|
||||||
|
if context:
|
||||||
|
a += ["--context", context]
|
||||||
|
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def find_identity():
|
def find_identity():
|
||||||
@@ -73,6 +91,10 @@ def call(method, path, body=None, query=None):
|
|||||||
def die(st, resp, ok=(200, 201)):
|
def die(st, resp, ok=(200, 201)):
|
||||||
if st not in ok:
|
if st not in ok:
|
||||||
print(f"[coord] ERROR HTTP {st}: {json.dumps(resp)[:500]}", file=sys.stderr)
|
print(f"[coord] ERROR HTTP {st}: {json.dumps(resp)[:500]}", file=sys.stderr)
|
||||||
|
_log_skill_error(
|
||||||
|
"coord", f"coord API call failed (HTTP {st})",
|
||||||
|
context=f"http={st} cmd={' '.join(sys.argv[1:3])} resp={json.dumps(resp)[:80]}",
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ WORK="$TMP/work"; mkdir -p "$WORK"
|
|||||||
PF="$TMP/prompt.txt"; OUT="$TMP/out.json"
|
PF="$TMP/prompt.txt"; OUT="$TMP/out.json"
|
||||||
RUN_CWD="$WORK" # grok's working dir; the 'review' mode overrides to the repo so read_file can reach repo files
|
RUN_CWD="$WORK" # grok's working dir; the 'review' mode overrides to the repo so read_file can reach repo files
|
||||||
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
|
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
|
||||||
|
# Functional-error logger (skill name "grok"); soft-fails, never breaks the caller.
|
||||||
|
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "grok" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
# run grok headless. $1=timeout secs; rest=extra flags. Reads $PF -> $OUT.
|
# run grok headless. $1=timeout secs; rest=extra flags. Reads $PF -> $OUT.
|
||||||
# Never fails the script on grok's exit code (Cancelled is expected; we read artifacts).
|
# Never fails the script on grok's exit code (Cancelled is expected; we read artifacts).
|
||||||
@@ -170,7 +172,7 @@ case "$MODE" in
|
|||||||
run_grok 180 --disable-web-search --max-turns 3
|
run_grok 180 --disable-web-search --max-turns 3
|
||||||
txt="$(jfield text)"
|
txt="$(jfield text)"
|
||||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
||||||
echo "[$SELF] no text (stopReason=$(jfield stopReason)); raw: $OUT" >&2; exit 1; fi
|
echo "[$SELF] no text (stopReason=$(jfield stopReason)); raw: $OUT" >&2; _logerr "grok returned no text" --context "mode=$MODE stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
image)
|
image)
|
||||||
[ -z "${1:-}" ] && { echo "usage: $SELF image \"<prompt>\" [out.png]" >&2; exit 2; }
|
[ -z "${1:-}" ] && { echo "usage: $SELF image \"<prompt>\" [out.png]" >&2; exit 2; }
|
||||||
@@ -180,7 +182,7 @@ case "$MODE" in
|
|||||||
sid="$(jfield sessionId)"; art="$(find_artifact "$sid" images)"
|
sid="$(jfield sessionId)"; art="$(find_artifact "$sid" images)"
|
||||||
if [ -n "$art" ] && [ -f "$art" ]; then cp -f "$art" "$out"
|
if [ -n "$art" ] && [ -f "$art" ]; then cp -f "$art" "$out"
|
||||||
echo "[$SELF] image OK -> $out (session $sid)"
|
echo "[$SELF] image OK -> $out (session $sid)"
|
||||||
else echo "[$SELF] no image artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; exit 1; fi
|
else echo "[$SELF] no image artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; _logerr "grok produced no image artifact" --context "session=$sid stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
video)
|
video)
|
||||||
[ -z "${1:-}" ] || [ -z "${2:-}" ] && { echo "usage: $SELF video \"<prompt>\" <input-image> [out.mp4]" >&2; exit 2; }
|
[ -z "${1:-}" ] || [ -z "${2:-}" ] && { echo "usage: $SELF video \"<prompt>\" <input-image> [out.mp4]" >&2; exit 2; }
|
||||||
@@ -192,7 +194,7 @@ case "$MODE" in
|
|||||||
sid="$(jfield sessionId)"; art="$(find_artifact "$sid" videos)"
|
sid="$(jfield sessionId)"; art="$(find_artifact "$sid" videos)"
|
||||||
if [ -n "$art" ] && [ -f "$art" ]; then cp -f "$art" "$out"
|
if [ -n "$art" ] && [ -f "$art" ]; then cp -f "$art" "$out"
|
||||||
echo "[$SELF] video OK -> $out (session $sid)"
|
echo "[$SELF] video OK -> $out (session $sid)"
|
||||||
else echo "[$SELF] no video artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; exit 1; fi
|
else echo "[$SELF] no video artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; _logerr "grok produced no video artifact" --context "session=$sid stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
xsearch)
|
xsearch)
|
||||||
[ -z "${1:-}" ] && { echo "usage: $SELF xsearch \"<query>\"" >&2; exit 2; }
|
[ -z "${1:-}" ] && { echo "usage: $SELF xsearch \"<query>\"" >&2; exit 2; }
|
||||||
@@ -200,7 +202,7 @@ case "$MODE" in
|
|||||||
run_grok 150 --max-turns 6
|
run_grok 150 --max-turns 6
|
||||||
txt="$(jfield text)"
|
txt="$(jfield text)"
|
||||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
||||||
echo "[$SELF] no result (stopReason=$(jfield stopReason))" >&2; exit 1; fi
|
echo "[$SELF] no result (stopReason=$(jfield stopReason))" >&2; _logerr "grok xsearch returned no result" --context "mode=xsearch stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
review|file)
|
review|file)
|
||||||
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
|
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
|
||||||
@@ -233,7 +235,7 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
||||||
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; exit 1; fi
|
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; _logerr "grok review returned no result" --context "mode=$MODE session=$(jfield sessionId) stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
review-files)
|
review-files)
|
||||||
# review-files [-i "instructions"] <file> [file ...]
|
# review-files [-i "instructions"] <file> [file ...]
|
||||||
@@ -283,7 +285,7 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
||||||
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; exit 1; fi
|
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; _logerr "grok review returned no result" --context "mode=$MODE session=$(jfield sessionId) stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
review-diff)
|
review-diff)
|
||||||
# review-diff [-C <repo-dir>] [-i "instructions"] <gitref> [-- <pathspec...>]
|
# review-diff [-C <repo-dir>] [-i "instructions"] <gitref> [-- <pathspec...>]
|
||||||
@@ -328,7 +330,7 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
|
||||||
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; exit 1; fi
|
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; _logerr "grok review returned no result" --context "mode=$MODE session=$(jfield sessionId) stopReason=$(jfield stopReason)"; exit 1; fi
|
||||||
;;
|
;;
|
||||||
raw)
|
raw)
|
||||||
"$GROK" "$@"
|
"$GROK" "$@"
|
||||||
|
|||||||
@@ -37,11 +37,31 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from mp_client import MailprotectorClient, MailprotectorError, VALID_SCOPES
|
from mp_client import MailprotectorClient, MailprotectorError, VALID_SCOPES
|
||||||
|
|
||||||
|
|
||||||
|
def _log_skill_error(skill, msg, context=""):
|
||||||
|
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
|
||||||
|
try:
|
||||||
|
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||||
|
)
|
||||||
|
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
|
||||||
|
if not os.path.exists(h):
|
||||||
|
return
|
||||||
|
a = ["bash", h, skill, msg]
|
||||||
|
if context:
|
||||||
|
a += ["--context", context]
|
||||||
|
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _emit(obj) -> None:
|
def _emit(obj) -> None:
|
||||||
print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
|
print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
|
||||||
|
|
||||||
@@ -314,6 +334,8 @@ def main(argv=None) -> int:
|
|||||||
p.error(f"unknown command {args.cmd}")
|
p.error(f"unknown command {args.cmd}")
|
||||||
except MailprotectorError as exc:
|
except MailprotectorError as exc:
|
||||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
_log_skill_error("mailprotector", f"{exc}",
|
||||||
|
context=f"cmd={getattr(args, 'cmd', '?')}")
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ set -euo pipefail
|
|||||||
TENANT_ADMIN_APPID="709e6eed-0711-4875-9c44-2d3518c47063"
|
TENANT_ADMIN_APPID="709e6eed-0711-4875-9c44-2d3518c47063"
|
||||||
CONSENT_BASE="https://login.microsoftonline.com"
|
CONSENT_BASE="https://login.microsoftonline.com"
|
||||||
CONSENT_REDIRECT="https://azcomputerguru.com"
|
CONSENT_REDIRECT="https://azcomputerguru.com"
|
||||||
|
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
|
||||||
# ── Locate the reused remediation-tool scripts ────────────────────────────────
|
# ── Locate the reused remediation-tool scripts ────────────────────────────────
|
||||||
# Prefer the applied global copy (stable path on every fleet machine); fall back
|
# Prefer the applied global copy (stable path on every fleet machine); fall back
|
||||||
@@ -43,6 +44,7 @@ RT="$(find_rtool)" || {
|
|||||||
echo "[ERROR] remediation-tool scripts not found." >&2
|
echo "[ERROR] remediation-tool scripts not found." >&2
|
||||||
echo " Expected: \$HOME/.claude/skills/remediation-tool/scripts/onboard-tenant.sh" >&2
|
echo " Expected: \$HOME/.claude/skills/remediation-tool/scripts/onboard-tenant.sh" >&2
|
||||||
echo " Run a repo sync, or check identity.json.claudetools_root." >&2
|
echo " Run a repo sync, or check identity.json.claudetools_root." >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "onboard365" "onboard365: remediation-tool scripts not found (onboard-tenant.sh missing on this machine)" >/dev/null 2>&1 || true
|
||||||
exit 3
|
exit 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,31 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ns_client import NetSapiensClient, PacketDialError
|
from ns_client import NetSapiensClient, PacketDialError
|
||||||
|
|
||||||
|
|
||||||
|
def _log_skill_error(skill, msg, context=""):
|
||||||
|
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
|
||||||
|
try:
|
||||||
|
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||||
|
)
|
||||||
|
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
|
||||||
|
if not os.path.exists(h):
|
||||||
|
return
|
||||||
|
a = ["bash", h, skill, msg]
|
||||||
|
if context:
|
||||||
|
a += ["--context", context]
|
||||||
|
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _emit(obj) -> None:
|
def _emit(obj) -> None:
|
||||||
print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
|
print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
|
||||||
|
|
||||||
@@ -161,6 +181,8 @@ def main(argv=None) -> int:
|
|||||||
p.error(f"unknown command {args.cmd}")
|
p.error(f"unknown command {args.cmd}")
|
||||||
except PacketDialError as exc:
|
except PacketDialError as exc:
|
||||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
_log_skill_error("packetdial", f"{exc}",
|
||||||
|
context=f"cmd={getattr(args, 'cmd', '?')}")
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,10 @@ process_one() {
|
|||||||
case "$rc" in
|
case "$rc" in
|
||||||
201) echo "ASSIGNED (Exchange Admin -> Exchange Operator SP)" ;;
|
201) echo "ASSIGNED (Exchange Admin -> Exchange Operator SP)" ;;
|
||||||
400) if echo "$body" | grep -qiE 'conflicting object|already (exist|present)'; then echo "OK (already assigned)"
|
400) if echo "$body" | grep -qiE 'conflicting object|already (exist|present)'; then echo "OK (already assigned)"
|
||||||
else echo "ERROR (HTTP 400: $(echo "$body" | jqr '.error.message // .' | head -c 120))"; fi ;;
|
else echo "ERROR (HTTP 400: $(echo "$body" | jqr '.error.message // .' | head -c 120))"
|
||||||
*) echo "ERROR (HTTP $rc: $(echo "$body" | jqr '.error.message // .' | head -c 120))" ;;
|
bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: role assignment POST failed" --context "tenant=$tgt http=400 msg=$(echo "$body" | jqr '.error.message // .' | head -c 80)" >/dev/null 2>&1 || true; fi ;;
|
||||||
|
*) echo "ERROR (HTTP $rc: $(echo "$body" | jqr '.error.message // .' | head -c 120))"
|
||||||
|
bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: role assignment POST failed" --context "tenant=$tgt http=$rc msg=$(echo "$body" | jqr '.error.message // .' | head -c 80)" >/dev/null 2>&1 || true ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ echo "=== assign-exchange-role [mode=$MODE] ==="
|
|||||||
echo "Role: Exchange Administrator ($EXCH_ADMIN_TEMPLATE) -> SP: Exchange Operator ($EXCHANGE_OP_APPID)"
|
echo "Role: Exchange Administrator ($EXCH_ADMIN_TEMPLATE) -> SP: Exchange Operator ($EXCHANGE_OP_APPID)"
|
||||||
echo "------------------------------------------------------------------------"
|
echo "------------------------------------------------------------------------"
|
||||||
if [ "$TARGET" = "--all" ]; then
|
if [ "$TARGET" = "--all" ]; then
|
||||||
[ -f "$TENANTS_MD" ] || { echo "[ERROR] tenants.md not found: $TENANTS_MD" >&2; exit 66; }
|
[ -f "$TENANTS_MD" ] || { echo "[ERROR] tenants.md not found: $TENANTS_MD" >&2; bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: --all run but references/tenants.md not found" --context "path=$TENANTS_MD" >/dev/null 2>&1 || true; exit 66; }
|
||||||
# extract tenant GUIDs from the markdown table (column 3)
|
# extract tenant GUIDs from the markdown table (column 3)
|
||||||
grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' "$TENANTS_MD" \
|
grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' "$TENANTS_MD" \
|
||||||
| sort -u | while read -r tid; do process_one "$tid"; done
|
| sort -u | while read -r tid; do process_one "$tid"; done
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ case "$AUTH_OVERRIDE" in
|
|||||||
if [[ -z "$CERT_X5T" || -z "$CERT_KEY_B64" ]]; then
|
if [[ -z "$CERT_X5T" || -z "$CERT_KEY_B64" ]]; then
|
||||||
echo "ERROR: REMEDIATION_AUTH=cert but cert fields missing in vault ($VAULT_PATH)" >&2
|
echo "ERROR: REMEDIATION_AUTH=cert but cert fields missing in vault ($VAULT_PATH)" >&2
|
||||||
echo " Required fields under credentials: cert_thumbprint_b64url, cert_private_key_pem_b64" >&2
|
echo " Required fields under credentials: cert_thumbprint_b64url, cert_private_key_pem_b64" >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: cert auth forced but cert fields missing in vault" --context "tier=$TIER vault=$VAULT_PATH" >/dev/null 2>&1 || true
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
AUTH_METHOD="cert"
|
AUTH_METHOD="cert"
|
||||||
@@ -251,6 +252,7 @@ case "$AUTH_OVERRIDE" in
|
|||||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||||
echo "ERROR: REMEDIATION_AUTH=secret but client_secret missing in vault ($VAULT_PATH)" >&2
|
echo "ERROR: REMEDIATION_AUTH=secret but client_secret missing in vault ($VAULT_PATH)" >&2
|
||||||
echo " Check field: credentials.client_secret (or credentials.credential for older entries)" >&2
|
echo " Check field: credentials.client_secret (or credentials.credential for older entries)" >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: secret auth forced but client_secret missing in vault" --context "tier=$TIER vault=$VAULT_PATH" >/dev/null 2>&1 || true
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
AUTH_METHOD="secret"
|
AUTH_METHOD="secret"
|
||||||
@@ -269,6 +271,7 @@ case "$AUTH_OVERRIDE" in
|
|||||||
echo "ERROR: no usable credential found in $VAULT_PATH" >&2
|
echo "ERROR: no usable credential found in $VAULT_PATH" >&2
|
||||||
echo " Need either credentials.cert_thumbprint_b64url + credentials.cert_private_key_pem_b64," >&2
|
echo " Need either credentials.cert_thumbprint_b64url + credentials.cert_private_key_pem_b64," >&2
|
||||||
echo " or credentials.client_secret (or legacy credentials.credential)." >&2
|
echo " or credentials.client_secret (or legacy credentials.credential)." >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: no usable credential (cert or client_secret) in vault" --context "tier=$TIER vault=$VAULT_PATH" >/dev/null 2>&1 || true
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
AUTH_METHOD="secret"
|
AUTH_METHOD="secret"
|
||||||
@@ -336,6 +339,7 @@ PY
|
|||||||
if [[ $ASSERT_RC -ne 0 || -z "$CLIENT_ASSERTION" ]]; then
|
if [[ $ASSERT_RC -ne 0 || -z "$CLIENT_ASSERTION" ]]; then
|
||||||
echo "ERROR: failed to build client_assertion JWT" >&2
|
echo "ERROR: failed to build client_assertion JWT" >&2
|
||||||
[[ -n "$CLIENT_ASSERTION" ]] && echo "$CLIENT_ASSERTION" >&2
|
[[ -n "$CLIENT_ASSERTION" ]] && echo "$CLIENT_ASSERTION" >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: failed to build client_assertion JWT (cert auth)" --context "tier=$TIER rc=$ASSERT_RC" >/dev/null 2>&1 || true
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -371,11 +375,13 @@ if [[ -z "$TOKEN" ]]; then
|
|||||||
echo " After the admin accepts, run onboard-tenant.sh to assign required directory roles:" >&2
|
echo " After the admin accepts, run onboard-tenant.sh to assign required directory roles:" >&2
|
||||||
SCRIPT_DIR_ERR="$(dirname "${BASH_SOURCE[0]}")"
|
SCRIPT_DIR_ERR="$(dirname "${BASH_SOURCE[0]}")"
|
||||||
echo " bash ${SCRIPT_DIR_ERR}/onboard-tenant.sh ${TARGET}" >&2
|
echo " bash ${SCRIPT_DIR_ERR}/onboard-tenant.sh ${TARGET}" >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: AADSTS7000229 — app not consented in tenant" --context "tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD" >/dev/null 2>&1 || true
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "ERROR: token request failed (tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD)" >&2
|
echo "ERROR: token request failed (tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD)" >&2
|
||||||
echo "$RESP" >&2
|
echo "$RESP" >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: token request failed (no access_token)" --context "tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD err=${ERROR_CODE:-none}" >/dev/null 2>&1 || true
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}"
|
||||||
|
|
||||||
TARGET="${1:?Usage: onboard-tenant.sh <domain-or-tenant-id> [--dry-run]}"
|
TARGET="${1:?Usage: onboard-tenant.sh <domain-or-tenant-id> [--dry-run]}"
|
||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
@@ -182,6 +183,7 @@ create_sp_if_missing() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo " [ERROR] Failed to create SP for $app_name: $(echo "$resp" | jq -r '.error.message // empty')" >&2
|
echo " [ERROR] Failed to create SP for $app_name: $(echo "$resp" | jq -r '.error.message // empty')" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: failed to create service principal" --context "app=$app_name appId=$app_id msg=$(echo "$resp" | jq -r '.error.message // empty' | head -c 80)" >/dev/null 2>&1 || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -239,6 +241,7 @@ grant_app_role() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo " [ERROR] grant_app_role failed for $role_id: $(echo "$resp" | jq -r '.error.message // "unknown"')" >&2
|
echo " [ERROR] grant_app_role failed for $role_id: $(echo "$resp" | jq -r '.error.message // "unknown"')" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: grant_app_role appRoleAssignment failed" --context "role=$role_id msg=$(echo "$resp" | jq -r '.error.message // "unknown"' | head -c 80)" >/dev/null 2>&1 || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -380,6 +383,7 @@ assign_role() {
|
|||||||
fi
|
fi
|
||||||
echo " [ERROR] Failed to assign $role_name" >&2
|
echo " [ERROR] Failed to assign $role_name" >&2
|
||||||
echo " Response: $resp" >&2
|
echo " Response: $resp" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: failed to assign directory role" --context "role=$role_name sp=$sp_oid msg=$(echo "$resp" | jq -r '.error.message // empty' | head -c 80)" >/dev/null 2>&1 || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo " [OK] $role_name assigned (assignment id=$assigned_id)"
|
echo " [OK] $role_name assigned (assignment id=$assigned_id)"
|
||||||
@@ -390,6 +394,7 @@ echo "[INFO] Resolving tenant: $TARGET"
|
|||||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
|
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
|
||||||
if [[ -z "$TENANT_ID" ]]; then
|
if [[ -z "$TENANT_ID" ]]; then
|
||||||
echo "[ERROR] Could not resolve tenant ID for: $TARGET" >&2
|
echo "[ERROR] Could not resolve tenant ID for: $TARGET" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: could not resolve tenant ID" --context "target=$TARGET" >/dev/null 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -412,6 +417,7 @@ if [[ $GET_TOKEN_EXIT -ne 0 ]]; then
|
|||||||
fi
|
fi
|
||||||
echo "[ERROR] Failed to acquire Tenant Admin token (exit $GET_TOKEN_EXIT)" >&2
|
echo "[ERROR] Failed to acquire Tenant Admin token (exit $GET_TOKEN_EXIT)" >&2
|
||||||
echo "$TOKEN_ERR" >&2
|
echo "$TOKEN_ERR" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: failed to acquire Tenant Admin token" --context "tenant=$TENANT_ID exit=$GET_TOKEN_EXIT" >/dev/null 2>&1 || true
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
TENANT_ADMIN_TOKEN="$TENANT_ADMIN_TOKEN_OUT"
|
TENANT_ADMIN_TOKEN="$TENANT_ADMIN_TOKEN_OUT"
|
||||||
@@ -440,6 +446,7 @@ DEFENDER_SP_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$DEFENDER_APP_ID")
|
|||||||
|
|
||||||
if [[ -z "$GRAPH_SP_OID" ]]; then
|
if [[ -z "$GRAPH_SP_OID" ]]; then
|
||||||
echo "[ERROR] Microsoft Graph SP missing — cannot grant app permissions" >&2
|
echo "[ERROR] Microsoft Graph SP missing — cannot grant app permissions" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: Microsoft Graph SP missing in tenant — cannot grant app permissions" --context "tenant=$TENANT_ID" >/dev/null 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ ROLE_MGMT_PERMISSION_ID="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||||
|
# Repo root for the functional-error logger (4 levels up from this scripts dir).
|
||||||
|
__ELOG_ROOT="${CLAUDETOOLS_ROOT_ENV:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}"
|
||||||
|
|
||||||
VAULT_ROOT="${VAULT_PATH:-}"
|
VAULT_ROOT="${VAULT_PATH:-}"
|
||||||
if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then
|
if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then
|
||||||
@@ -39,7 +41,7 @@ CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT
|
|||||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
|
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
|
||||||
fi
|
fi
|
||||||
[[ -z "$CLIENT_SECRET" ]] && { echo "[ERROR] Could not read secret from $MANAGEMENT_VAULT_PATH" >&2; exit 4; }
|
[[ -z "$CLIENT_SECRET" ]] && { echo "[ERROR] Could not read secret from $MANAGEMENT_VAULT_PATH" >&2; bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: could not read Management app secret from vault" --context "vault=$MANAGEMENT_VAULT_PATH" >/dev/null 2>&1 || true; exit 4; }
|
||||||
echo "[OK] Management app secret retrieved"
|
echo "[OK] Management app secret retrieved"
|
||||||
|
|
||||||
# ── Step 2: Get Management app token (home tenant) ───────────────────────────
|
# ── Step 2: Get Management app token (home tenant) ───────────────────────────
|
||||||
@@ -55,6 +57,7 @@ MGMT_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty')
|
|||||||
if [[ -z "$MGMT_TOKEN" ]]; then
|
if [[ -z "$MGMT_TOKEN" ]]; then
|
||||||
echo "[ERROR] Failed to acquire Management app token" >&2
|
echo "[ERROR] Failed to acquire Management app token" >&2
|
||||||
echo "$TOKEN_RESP" >&2
|
echo "$TOKEN_RESP" >&2
|
||||||
|
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: failed to acquire Management app token (home tenant)" --context "err=$(echo "$TOKEN_RESP" | jq -r '.error // empty' 2>/dev/null)" >/dev/null 2>&1 || true
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
echo "[OK] Management app token acquired"
|
echo "[OK] Management app token acquired"
|
||||||
@@ -73,6 +76,7 @@ APP_DISPLAY=$(echo "$APP_RESP" | jq -r '.value[0].displayName // empty')
|
|||||||
if [[ -z "$APP_OBJ_ID" ]]; then
|
if [[ -z "$APP_OBJ_ID" ]]; then
|
||||||
echo "[ERROR] Tenant Admin application not found (appId=$TENANT_ADMIN_APP_ID)" >&2
|
echo "[ERROR] Tenant Admin application not found (appId=$TENANT_ADMIN_APP_ID)" >&2
|
||||||
echo "Response: $APP_RESP" >&2
|
echo "Response: $APP_RESP" >&2
|
||||||
|
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: Tenant Admin application registration not found" --context "appId=$TENANT_ADMIN_APP_ID" >/dev/null 2>&1 || true
|
||||||
exit 6
|
exit 6
|
||||||
fi
|
fi
|
||||||
echo "[OK] Found app: $APP_DISPLAY (objectId=$APP_OBJ_ID)"
|
echo "[OK] Found app: $APP_DISPLAY (objectId=$APP_OBJ_ID)"
|
||||||
@@ -108,6 +112,7 @@ else
|
|||||||
echo "[OK] App manifest patched (HTTP 204)"
|
echo "[OK] App manifest patched (HTTP 204)"
|
||||||
else
|
else
|
||||||
echo "[ERROR] PATCH returned HTTP $PATCH_RESP" >&2
|
echo "[ERROR] PATCH returned HTTP $PATCH_RESP" >&2
|
||||||
|
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: app manifest PATCH failed" --context "appObjId=$APP_OBJ_ID http=$PATCH_RESP" >/dev/null 2>&1 || true
|
||||||
exit 7
|
exit 7
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -121,7 +126,7 @@ TA_SP_RESP=$(curl -s --max-time 15 \
|
|||||||
--data-urlencode "\$select=id,displayName" \
|
--data-urlencode "\$select=id,displayName" \
|
||||||
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
||||||
TA_SP_OID=$(echo "$TA_SP_RESP" | jq -r '.value[0].id // empty')
|
TA_SP_OID=$(echo "$TA_SP_RESP" | jq -r '.value[0].id // empty')
|
||||||
[[ -z "$TA_SP_OID" ]] && { echo "[ERROR] Tenant Admin SP not found in home tenant" >&2; exit 8; }
|
[[ -z "$TA_SP_OID" ]] && { echo "[ERROR] Tenant Admin SP not found in home tenant" >&2; bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: Tenant Admin SP not found in home tenant" --context "appId=$TENANT_ADMIN_APP_ID" >/dev/null 2>&1 || true; exit 8; }
|
||||||
echo "[OK] Tenant Admin SP: $TA_SP_OID"
|
echo "[OK] Tenant Admin SP: $TA_SP_OID"
|
||||||
|
|
||||||
echo "[INFO] Locating Microsoft Graph SP in home tenant..."
|
echo "[INFO] Locating Microsoft Graph SP in home tenant..."
|
||||||
@@ -132,7 +137,7 @@ GRAPH_SP_RESP=$(curl -s --max-time 15 \
|
|||||||
--data-urlencode "\$select=id" \
|
--data-urlencode "\$select=id" \
|
||||||
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
||||||
GRAPH_SP_OID=$(echo "$GRAPH_SP_RESP" | jq -r '.value[0].id // empty')
|
GRAPH_SP_OID=$(echo "$GRAPH_SP_RESP" | jq -r '.value[0].id // empty')
|
||||||
[[ -z "$GRAPH_SP_OID" ]] && { echo "[ERROR] Microsoft Graph SP not found in home tenant" >&2; exit 8; }
|
[[ -z "$GRAPH_SP_OID" ]] && { echo "[ERROR] Microsoft Graph SP not found in home tenant" >&2; bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: Microsoft Graph SP not found in home tenant" >/dev/null 2>&1 || true; exit 8; }
|
||||||
echo "[OK] Microsoft Graph SP: $GRAPH_SP_OID"
|
echo "[OK] Microsoft Graph SP: $GRAPH_SP_OID"
|
||||||
|
|
||||||
# ── Step 6: Check if appRoleAssignment already granted ────────────────────────
|
# ── Step 6: Check if appRoleAssignment already granted ────────────────────────
|
||||||
@@ -165,6 +170,7 @@ else
|
|||||||
if [[ -z "$GRANT_ID" ]]; then
|
if [[ -z "$GRANT_ID" ]]; then
|
||||||
echo "[ERROR] Failed to grant appRoleAssignment" >&2
|
echo "[ERROR] Failed to grant appRoleAssignment" >&2
|
||||||
echo "$GRANT_RESP" >&2
|
echo "$GRANT_RESP" >&2
|
||||||
|
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: failed to grant RoleManagement.ReadWrite.Directory appRoleAssignment" --context "msg=$(echo "$GRANT_RESP" | jq -r '.error.message // empty' 2>/dev/null | head -c 80)" >/dev/null 2>&1 || true
|
||||||
exit 9
|
exit 9
|
||||||
fi
|
fi
|
||||||
echo "[OK] appRoleAssignment granted (id=$GRANT_ID)"
|
echo "[OK] appRoleAssignment granted (id=$GRANT_ID)"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}"
|
||||||
|
|
||||||
TENANT_INPUT="${1:?usage: reset-password.sh <tenant|domain> <upn> <new-password> [--force-change]}"
|
TENANT_INPUT="${1:?usage: reset-password.sh <tenant|domain> <upn> <new-password> [--force-change]}"
|
||||||
UPN="${2:?usage: reset-password.sh <tenant|domain> <upn> <new-password> [--force-change]}"
|
UPN="${2:?usage: reset-password.sh <tenant|domain> <upn> <new-password> [--force-change]}"
|
||||||
@@ -43,7 +44,7 @@ G="https://graph.microsoft.com/v1.0"
|
|||||||
# --- resolve target user object id ---
|
# --- resolve target user object id ---
|
||||||
UID_=$(curl -s "${GH[@]}" "$G/users/${UPN}?\$select=id" | tr -d '\000-\037' \
|
UID_=$(curl -s "${GH[@]}" "$G/users/${UPN}?\$select=id" | tr -d '\000-\037' \
|
||||||
| python -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
| python -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
[[ -z "$UID_" ]] && { echo "[ERROR] user not found: $UPN" >&2; exit 1; }
|
[[ -z "$UID_" ]] && { echo "[ERROR] user not found: $UPN" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: target user not found / Graph returned no id" --context "tenant=$TENANT_ID upn=$UPN" >/dev/null 2>&1 || true; exit 1; }
|
||||||
echo "[info] tenant=$TENANT_ID target=$UPN id=$UID_ force_change=$FORCE_CHANGE"
|
echo "[info] tenant=$TENANT_ID target=$UPN id=$UID_ force_change=$FORCE_CHANGE"
|
||||||
|
|
||||||
# --- build payload (single-quoted heredoc would block $NEWPW; use python to emit JSON safely) ---
|
# --- build payload (single-quoted heredoc would block $NEWPW; use python to emit JSON safely) ---
|
||||||
@@ -61,6 +62,7 @@ fi
|
|||||||
if [[ "$CODE" != "403" ]]; then
|
if [[ "$CODE" != "403" ]]; then
|
||||||
echo "[ERROR] unexpected HTTP $CODE on password PATCH" >&2
|
echo "[ERROR] unexpected HTTP $CODE on password PATCH" >&2
|
||||||
curl -s -X PATCH "${GH[@]}" "$G/users/$UID_" --data-binary "$PAYLOAD" | tr -d '\000-\037' >&2
|
curl -s -X PATCH "${GH[@]}" "$G/users/$UID_" --data-binary "$PAYLOAD" | tr -d '\000-\037' >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: unexpected HTTP on password PATCH" --context "tenant=$TENANT_ID upn=$UPN http=$CODE" >/dev/null 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ echo "[info] 403 on direct reset (target likely holds an admin role) -> JIT elev
|
|||||||
# --- resolve tenant-admin SP object id ---
|
# --- resolve tenant-admin SP object id ---
|
||||||
SPID=$(curl -s "${GH[@]}" "$G/servicePrincipals(appId='$TENANT_ADMIN_APPID')?\$select=id" | tr -d '\000-\037' \
|
SPID=$(curl -s "${GH[@]}" "$G/servicePrincipals(appId='$TENANT_ADMIN_APPID')?\$select=id" | tr -d '\000-\037' \
|
||||||
| python -c "import sys,json;print(json.load(sys.stdin).get('id',''))")
|
| python -c "import sys,json;print(json.load(sys.stdin).get('id',''))")
|
||||||
[[ -z "$SPID" ]] && { echo "[ERROR] could not resolve Tenant Admin service principal" >&2; exit 1; }
|
[[ -z "$SPID" ]] && { echo "[ERROR] could not resolve Tenant Admin service principal" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: could not resolve Tenant Admin SP for JIT elevation" --context "tenant=$TENANT_ID" >/dev/null 2>&1 || true; exit 1; }
|
||||||
|
|
||||||
# --- does the SP already hold Privileged Authentication Administrator? ---
|
# --- does the SP already hold Privileged Authentication Administrator? ---
|
||||||
EXISTING=$(curl -s "${GH[@]}" "$G/roleManagement/directory/roleAssignments?\$filter=principalId+eq+'$SPID'+and+roleDefinitionId+eq+'$PAA_ROLE_ID'" \
|
EXISTING=$(curl -s "${GH[@]}" "$G/roleManagement/directory/roleAssignments?\$filter=principalId+eq+'$SPID'+and+roleDefinitionId+eq+'$PAA_ROLE_ID'" \
|
||||||
@@ -82,7 +84,7 @@ else
|
|||||||
ASSIGN_BODY=$(SPID="$SPID" RID="$PAA_ROLE_ID" python -c "import os,json;print(json.dumps({'principalId':os.environ['SPID'],'roleDefinitionId':os.environ['RID'],'directoryScopeId':'/'}))")
|
ASSIGN_BODY=$(SPID="$SPID" RID="$PAA_ROLE_ID" python -c "import os,json;print(json.dumps({'principalId':os.environ['SPID'],'roleDefinitionId':os.environ['RID'],'directoryScopeId':'/'}))")
|
||||||
CREATED_ASSIGNMENT=$(curl -s -X POST "${GH[@]}" "$G/roleManagement/directory/roleAssignments" --data-binary "$ASSIGN_BODY" \
|
CREATED_ASSIGNMENT=$(curl -s -X POST "${GH[@]}" "$G/roleManagement/directory/roleAssignments" --data-binary "$ASSIGN_BODY" \
|
||||||
| tr -d '\000-\037' | python -c "import sys,json;d=json.load(sys.stdin);print(d.get('id',''))" 2>/dev/null || true)
|
| tr -d '\000-\037' | python -c "import sys,json;d=json.load(sys.stdin);print(d.get('id',''))" 2>/dev/null || true)
|
||||||
[[ -z "$CREATED_ASSIGNMENT" ]] && { echo "[ERROR] failed to assign Privileged Authentication Administrator to SP" >&2; exit 1; }
|
[[ -z "$CREATED_ASSIGNMENT" ]] && { echo "[ERROR] failed to assign Privileged Authentication Administrator to SP" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: failed to assign Privileged Authentication Administrator to SP (JIT elevation)" --context "tenant=$TENANT_ID sp=$SPID" >/dev/null 2>&1 || true; exit 1; }
|
||||||
echo "[info] assigned Privileged Authentication Administrator to SP (assignment $CREATED_ASSIGNMENT)"
|
echo "[info] assigned Privileged Authentication Administrator to SP (assignment $CREATED_ASSIGNMENT)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ fi
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
if [[ -n "$CREATED_ASSIGNMENT" ]]; then
|
if [[ -n "$CREATED_ASSIGNMENT" ]]; then
|
||||||
DC=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "${GH[@]}" "$G/roleManagement/directory/roleAssignments/$CREATED_ASSIGNMENT")
|
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; fi
|
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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
@@ -108,4 +110,5 @@ done
|
|||||||
|
|
||||||
echo "[ERROR] password reset still failing after elevation (last HTTP $CODE)" >&2
|
echo "[ERROR] password reset still failing after elevation (last HTTP $CODE)" >&2
|
||||||
curl -s -X PATCH "${GH[@]}" "$G/users/$UID_" --data-binary "$PAYLOAD" | tr -d '\000-\037' >&2
|
curl -s -X PATCH "${GH[@]}" "$G/users/$UID_" --data-binary "$PAYLOAD" | tr -d '\000-\037' >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: reset still failing after JIT elevation + retries" --context "tenant=$TENANT_ID upn=$UPN http=$CODE" >/dev/null 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# Output (stdout): tenant GUID. Exit 0 on success, 1 on failure.
|
# Output (stdout): tenant GUID. Exit 0 on success, 1 on failure.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
|
||||||
INPUT="${1:?usage: resolve-tenant.sh <domain|upn|tenant-id>}"
|
INPUT="${1:?usage: resolve-tenant.sh <domain|upn|tenant-id>}"
|
||||||
|
|
||||||
# If it looks like a GUID already, pass through.
|
# If it looks like a GUID already, pass through.
|
||||||
@@ -31,6 +33,7 @@ TENANT_ID=$(echo "$RESP" | jq -r '.issuer // empty' | sed -E 's|^https://login\.
|
|||||||
if [[ -z "$TENANT_ID" ]] || [[ ! "$TENANT_ID" =~ ^[0-9a-fA-F]{8}- ]]; then
|
if [[ -z "$TENANT_ID" ]] || [[ ! "$TENANT_ID" =~ ^[0-9a-fA-F]{8}- ]]; then
|
||||||
echo "ERROR: could not resolve tenant for domain: $DOMAIN" >&2
|
echo "ERROR: could not resolve tenant for domain: $DOMAIN" >&2
|
||||||
echo "Response: $RESP" >&2
|
echo "Response: $RESP" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "resolve-tenant: OpenID discovery did not return a tenant GUID" --context "domain=$DOMAIN" >/dev/null 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
|
||||||
TENANT_INPUT="${1:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
TENANT_INPUT="${1:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
||||||
UPN="${2:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
UPN="${2:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
||||||
@@ -28,6 +29,7 @@ UID_=$(jq -r '.id // empty' "$OUT/00_user.json")
|
|||||||
if [[ -z "$UID_" ]]; then
|
if [[ -z "$UID_" ]]; then
|
||||||
echo "ERROR: user not found or Graph returned error" >&2
|
echo "ERROR: user not found or Graph returned error" >&2
|
||||||
cat "$OUT/00_user.json" >&2
|
cat "$OUT/00_user.json" >&2
|
||||||
|
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "user-breach-check: user not found or Graph returned error resolving user object" --context "tenant=$TENANT_ID upn=$UPN err=$(jq -r '.error.code // empty' "$OUT/00_user.json" 2>/dev/null)" >/dev/null 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "[info] object id: $UID_"
|
echo "[info] object id: $UID_"
|
||||||
|
|||||||
@@ -98,6 +98,23 @@ After creating the files:
|
|||||||
- Commands: Tell them to use `/{name}` or `/{name} arguments`
|
- Commands: Tell them to use `/{name}` or `/{name} arguments`
|
||||||
3. Remind them to update CLAUDE.md's Commands & Skills table if they want it documented there
|
3. Remind them to update CLAUDE.md's Commands & Skills table if they want it documented there
|
||||||
|
|
||||||
|
## Mandatory: functional error logging
|
||||||
|
|
||||||
|
**Every skill MUST report genuine functional errors to `errorlog.md`** via the canonical
|
||||||
|
helper, so failures can be linted and fed back into skill improvements (CLAUDE.md core rule).
|
||||||
|
Bake this into the skill from the start:
|
||||||
|
|
||||||
|
- In each skill **script's failure branches** (API/auth failure, unexpected response,
|
||||||
|
validation error, unexpected non-zero exit), call:
|
||||||
|
```bash
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "<skill-name>" "<brief error>" --context "op=... http=..."
|
||||||
|
```
|
||||||
|
It stamps date+machine, inserts in the standard `YYYY-MM-DD | MACHINE | skill | error`
|
||||||
|
format, and soft-fails (never breaks the caller). Python skills shell out to the same helper.
|
||||||
|
- In the **SKILL.md**, add a line under workflow/guidelines: "On a functional error, log it via
|
||||||
|
`log-skill-error.sh` before surfacing it."
|
||||||
|
- Do NOT log expected/handled conditions (no results, no unread, user-declined) — only real failures.
|
||||||
|
|
||||||
## Quality Checklist
|
## Quality Checklist
|
||||||
|
|
||||||
Before finalizing, verify:
|
Before finalizing, verify:
|
||||||
@@ -107,6 +124,7 @@ Before finalizing, verify:
|
|||||||
- [ ] File is in the correct location (`.claude/skills/` or `.claude/commands/`)
|
- [ ] File is in the correct location (`.claude/skills/` or `.claude/commands/`)
|
||||||
- [ ] Name uses kebab-case and is concise
|
- [ ] Name uses kebab-case and is concise
|
||||||
- [ ] For skills with auto-triggers: triggers are specific enough to avoid false positives
|
- [ ] For skills with auto-triggers: triggers are specific enough to avoid false positives
|
||||||
|
- [ ] **Functional errors are logged** via `log-skill-error.sh` in the script's failure branches
|
||||||
|
|
||||||
## Tips for Good Skills/Commands
|
## Tips for Good Skills/Commands
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,12 @@
|
|||||||
# Anything you put OUTSIDE those keys is committed in PLAINTEXT — never do that.
|
# Anything you put OUTSIDE those keys is committed in PLAINTEXT — never do that.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Functional-error logger ───────────────────────────────────────────────────
|
||||||
|
# errorlog.md + log-skill-error.sh live in the ClaudeTools repo (4 levels up from
|
||||||
|
# .claude/skills/vault/scripts/), NOT the SOPS vault repo (VAULT_DIR below).
|
||||||
|
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
|
||||||
|
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "vault" "$@" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
# ── Resolve vault root (the #1 thing sessions get wrong) ──────────────────────
|
# ── Resolve vault root (the #1 thing sessions get wrong) ──────────────────────
|
||||||
# Order: $VAULT_PATH override → the repo we're standing in (correct identity) →
|
# Order: $VAULT_PATH override → the repo we're standing in (correct identity) →
|
||||||
# $HOME identity vault_path → $HOME identity claudetools_root → that repo's identity.
|
# $HOME identity vault_path → $HOME identity claudetools_root → that repo's identity.
|
||||||
@@ -84,7 +90,7 @@ cmd_verify() {
|
|||||||
local f; f=$(abspath "${1:?usage: verify <path>}")
|
local f; f=$(abspath "${1:?usage: verify <path>}")
|
||||||
[[ -f "$f" ]] || { echo "[ERROR] not found: $f" >&2; exit 1; }
|
[[ -f "$f" ]] || { echo "[ERROR] not found: $f" >&2; exit 1; }
|
||||||
if ! _is_encrypted "$f"; then echo "[FAIL] $1 — NO encrypted values found (plaintext?)"; exit 1; fi
|
if ! _is_encrypted "$f"; then echo "[FAIL] $1 — NO encrypted values found (plaintext?)"; exit 1; fi
|
||||||
if ! ( cd "$VAULT_DIR" && sops -d "$f" >/dev/null 2>&1 ); then echo "[FAIL] $1 — encrypted but does not decrypt (key mismatch?)"; exit 1; fi
|
if ! ( cd "$VAULT_DIR" && sops -d "$f" >/dev/null 2>&1 ); then echo "[FAIL] $1 — encrypted but does not decrypt (key mismatch?)"; _logerr "sops decrypt failed (key mismatch?)" --context "op=verify path=$1"; exit 1; fi
|
||||||
echo "[OK] $1 — encrypted and decrypts cleanly"
|
echo "[OK] $1 — encrypted and decrypts cleanly"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +135,7 @@ doc["notes"]=""
|
|||||||
with open(f,"w",encoding="utf-8",newline="\n") as fh:
|
with open(f,"w",encoding="utf-8",newline="\n") as fh:
|
||||||
yaml.safe_dump(doc,fh,default_flow_style=False,sort_keys=False,allow_unicode=True)
|
yaml.safe_dump(doc,fh,default_flow_style=False,sort_keys=False,allow_unicode=True)
|
||||||
PY
|
PY
|
||||||
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] sops encrypt failed; removing plaintext" >&2; rm -f "$f"; exit 1; }
|
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] sops encrypt failed; removing plaintext" >&2; _logerr "sops encrypt failed (new entry)" --context "op=new path=$path"; rm -f "$f"; exit 1; }
|
||||||
cmd_verify "$path"
|
cmd_verify "$path"
|
||||||
echo "[INFO] Created ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh (Phase 6 commits+pushes the vault)"
|
echo "[INFO] Created ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh (Phase 6 commits+pushes the vault)"
|
||||||
}
|
}
|
||||||
@@ -142,7 +148,7 @@ cmd_set() {
|
|||||||
local f; f=$(abspath "$path")
|
local f; f=$(abspath "$path")
|
||||||
[[ -f "$f" ]] || { echo "[ERROR] not found: ${f#$VAULT_DIR/} (use 'new' to create)" >&2; exit 1; }
|
[[ -f "$f" ]] || { echo "[ERROR] not found: ${f#$VAULT_DIR/} (use 'new' to create)" >&2; exit 1; }
|
||||||
local tmp; tmp=$(mktemp)
|
local tmp; tmp=$(mktemp)
|
||||||
( cd "$VAULT_DIR" && sops -d "$f" ) > "$tmp" 2>/dev/null || { echo "[ERROR] decrypt failed" >&2; rm -f "$tmp"; exit 1; }
|
( cd "$VAULT_DIR" && sops -d "$f" ) > "$tmp" 2>/dev/null || { echo "[ERROR] decrypt failed" >&2; _logerr "sops decrypt failed (set)" --context "op=set path=$path"; rm -f "$tmp"; exit 1; }
|
||||||
SETS="$(printf '%s\n' "${sets[@]}")" "$PY" - "$tmp" <<'PY'
|
SETS="$(printf '%s\n' "${sets[@]}")" "$PY" - "$tmp" <<'PY'
|
||||||
import os,sys,yaml
|
import os,sys,yaml
|
||||||
f=sys.argv[1]
|
f=sys.argv[1]
|
||||||
@@ -154,7 +160,7 @@ for kv in os.environ["SETS"].splitlines():
|
|||||||
yaml.safe_dump(doc,open(f,"w",encoding="utf-8",newline="\n"),default_flow_style=False,sort_keys=False,allow_unicode=True)
|
yaml.safe_dump(doc,open(f,"w",encoding="utf-8",newline="\n"),default_flow_style=False,sort_keys=False,allow_unicode=True)
|
||||||
PY
|
PY
|
||||||
cp "$tmp" "$f"; rm -f "$tmp"
|
cp "$tmp" "$f"; rm -f "$tmp"
|
||||||
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] re-encrypt failed" >&2; exit 1; }
|
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] re-encrypt failed" >&2; _logerr "sops re-encrypt failed (set)" --context "op=set path=$path"; exit 1; }
|
||||||
cmd_verify "$path"
|
cmd_verify "$path"
|
||||||
echo "[INFO] Updated ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh"
|
echo "[INFO] Updated ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh"
|
||||||
}
|
}
|
||||||
|
|||||||
29
errorlog.md
29
errorlog.md
@@ -1,14 +1,37 @@
|
|||||||
# Error Log
|
# Error Log
|
||||||
|
|
||||||
Brief records of task-execution errors across the fleet, used to improve skills and the
|
Brief records of preventable, pattern-worthy events across the fleet — used to improve
|
||||||
command harness. Append newest entries at the top. Keep each entry to 1-2 lines.
|
skills, write better CLAUDE.md rules, and clean stale/misleading memory. The aim: never
|
||||||
|
pay tokens twice for the same avoidable mistake. Append newest at the top; keep entries to
|
||||||
|
1-2 lines. **Always write via the helper, never by hand:**
|
||||||
|
`bash .claude/scripts/log-skill-error.sh "<skill/context>" "<brief>" [--correction|--friction] [--context "k=v"]`
|
||||||
|
|
||||||
Format: `YYYY-MM-DD | MACHINE | command/skill | error (brief)`
|
Format: `YYYY-MM-DD | MACHINE | command/skill/context | [type] error (brief) [ctx: ...]`
|
||||||
|
|
||||||
|
Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
|
||||||
|
`[correction]` = user corrected an improper assumption I made ·
|
||||||
|
`[friction]` = preventable self-inflicted token-waste (harness/env/tool misuse; cite a
|
||||||
|
`ref=` in ctx when it repeats a documented gotcha — that flags a rule/memory to strengthen).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Append entries below this line -->
|
<!-- Append entries below this line -->
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | powershell/var-case | [friction] PowerShell vars are case-INSENSITIVE: $gUid silently overwrote $guid (GPO id), Set-ADObject hit a bad DN and left GPT.ini/AD versionNumber inconsistent until fixed. Never rely on case to distinguish PS variables
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | python/argv-limit | [friction] passed full /api/agents JSON (248 agents) as a python CLI arg -> 'Argument list too long' on Windows. Pipe large payloads via stdin, not argv
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | bash/env-persist | [friction] re-derived RMM token every call after $TOKEN/$RMM vanished between Bash tool calls - shell env does NOT persist across calls; must re-eval auth (or chain) in the same command
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | bash/tmp-path | [friction] wrote curl -o /tmp/x.json then jq read it back and failed (No such file) - Git-Bash vs Write/tool /tmp resolve differently. Pipe directly or use repo-relative paths. REPEAT of documented gotcha [ctx: ref=feedback_tmp_path_windows]
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | DMARC / DNS | [correction] assumed ACG's own INKY rua convention (reports-sg.inkydmarc.com) applied to a client domain; only use the INKY rua if THAT client is onboarded to INKY - otherwise plain p=none or a real mailbox
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | remediation-tool (sendMail) | [correction] assumed none of the consented apps could send mail and started granting Graph Mail.Send; the Exchange Operator app ALREADY had Graph Mail.Send - I was decoding the EXO-audience token, not a Graph-audience token. Mint a Graph token for the app before concluding a permission is missing
|
||||||
|
|
||||||
|
2026-06-15 | GURU-5070 | rmm-search | [correction] assumed the CLI search must replicate the UI Omnibox scoreMatch exactly; user wants a FLEXIBLE forgiving multi-field search optimized for first-try correctness, not UI parity
|
||||||
|
|
||||||
|
|
||||||
2026-06-15 | GURU-BEAST-ROG | /syncro (comment edit) | Syncro API does not expose a comment-edit or comment-delete endpoint — once posted, comments can only be modified via the GUI. Bot posted an internal resolution note with an unwanted "Performed by: ClaudeTools Discord Bot" line and could not remove it programmatically. Remediation needed: either suppress bot-attribution lines from internal notes by default, or add a GUI-edit step to the workflow when the note needs correction.
|
2026-06-15 | GURU-BEAST-ROG | /syncro (comment edit) | Syncro API does not expose a comment-edit or comment-delete endpoint — once posted, comments can only be modified via the GUI. Bot posted an internal resolution note with an unwanted "Performed by: ClaudeTools Discord Bot" line and could not remove it programmatically. Remediation needed: either suppress bot-attribution lines from internal notes by default, or add a GUI-edit step to the workflow when the note needs correction.
|
||||||
|
|
||||||
2026-06-14 | GURU-5070 | mailbox skill (Graph token) | FABB app `fabb3421` (Claude-MSP-Access / "Cloud MSP Access") token request returned AADSTS700016 — app/SP no longer present in azcomputerguru.com tenant (deleted; gotchas.md already marked it deprecated). Blocks /mailbox + the M365 contacts task. Verified the remediation suite (live, ACG tenant) carries NO Mail.Send/Mail.ReadWrite/Contacts scopes (investigator has Mail.Read only) — so a straight repoint can't restore mailbox-send/contacts. Pending Mike decision: stand up a single-tenant ACG-internal mailbox app vs. add scopes to a suite tier. [2026-06-15] Docs hardened — gotchas.md now marks fabb3421 DELETED with the Mail/Contacts-scope blast radius + flags the 3 legacy "old app only" tenants (Valleywide/Dataforth/Cascades) as now having NO working remediation app (migration URGENT); mailbox.md carries a BLOCKED/AADSTS700016 banner. DECISION 2026-06-15 (Mike): Mail.Send goes into the suite (Exchange Operator tier) since its real use is IR victim-notification during mailbox takeovers; add Mail.Send to the exchange-op manifest + consent, repoint mailbox.md to exchange-op. Implementation not yet executed (production app change, needs go).
|
2026-06-14 | GURU-5070 | mailbox skill (Graph token) | FABB app `fabb3421` (Claude-MSP-Access / "Cloud MSP Access") token request returned AADSTS700016 — app/SP no longer present in azcomputerguru.com tenant (deleted; gotchas.md already marked it deprecated). Blocks /mailbox + the M365 contacts task. Verified the remediation suite (live, ACG tenant) carries NO Mail.Send/Mail.ReadWrite/Contacts scopes (investigator has Mail.Read only) — so a straight repoint can't restore mailbox-send/contacts. Pending Mike decision: stand up a single-tenant ACG-internal mailbox app vs. add scopes to a suite tier. [2026-06-15] Docs hardened — gotchas.md now marks fabb3421 DELETED with the Mail/Contacts-scope blast radius + flags the 3 legacy "old app only" tenants (Valleywide/Dataforth/Cascades) as now having NO working remediation app (migration URGENT); mailbox.md carries a BLOCKED/AADSTS700016 banner. DECISION 2026-06-15 (Mike): Mail.Send goes into the suite (Exchange Operator tier) since its real use is IR victim-notification during mailbox takeovers; add Mail.Send to the exchange-op manifest + consent, repoint mailbox.md to exchange-op. Implementation not yet executed (production app change, needs go).
|
||||||
|
|||||||
Reference in New Issue
Block a user