diff --git a/.claude/bootstrap/backup-to-bundle.ps1 b/.claude/bootstrap/backup-to-bundle.ps1 index 92475a4..c00cb64 100644 --- a/.claude/bootstrap/backup-to-bundle.ps1 +++ b/.claude/bootstrap/backup-to-bundle.ps1 @@ -30,6 +30,11 @@ param( $ErrorActionPreference = 'Stop' $u = $env:USERPROFILE +# Decode native (git) stdout as UTF-8 so captured patch text is not mangled, and give +# us a UTF-8 (no BOM) encoding for writing patches `git apply` can actually parse. +try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch {} +$Utf8NoBom = New-Object System.Text.UTF8Encoding($false) + function Save($src,$dst){ if (Test-Path -LiteralPath $src) { $p = Split-Path $dst -Parent; if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null } @@ -81,6 +86,37 @@ if (Test-Path "$ClaudeToolsRoot\.claude\state") { Copy-Item "$ClaudeToolsRoot\.c Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\*.ps1" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\RESTORE.md" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue +# --- at-risk local WIP: stashes + untracked diffs that are on NO remote --- +# Written as UTF-8 (no BOM, LF) so restore-at-risk-work.ps1 / `git apply` can parse them. +# (Earlier ad-hoc captures used PowerShell `>` redirection = UTF-16, which git apply +# rejects with "No valid patches in input" - hence the explicit byte-level write here.) +$awRoot = "$root\at-risk-work" +function Save-RepoStashes($repo,$label){ + if (-not (Test-Path "$repo\.git")) { return } + $marks = @(& git -C $repo stash list --format='%gd' 2>$null) + if (-not $marks) { return } + $dir = "$awRoot\$label"; New-Item -ItemType Directory -Force -Path $dir | Out-Null + $base = (& git -C $repo rev-parse HEAD 2>$null) + [System.IO.File]::WriteAllText("$dir\BASE-COMMIT.txt", "$base`n", $Utf8NoBom) + for ($i=0; $i -lt $marks.Count; $i++) { + $files = @(& git -C $repo stash show --name-only "stash@{$i}" 2>$null) + $slug = if ($files.Count) { ([IO.Path]::GetFileNameWithoutExtension($files[0])) -replace '[^\w\-]','_' } else { "stash$i" } + $lines = @(& git -C $repo --no-pager stash show -p "stash@{$i}" 2>$null) + [System.IO.File]::WriteAllText("$dir\stash$i-$slug.patch", (($lines -join "`n") + "`n"), $Utf8NoBom) + Write-Host "[OK] at-risk stash: $label stash@{$i} -> stash$i-$slug.patch" + } +} +Save-RepoStashes "$ClaudeToolsRoot\projects\msp-tools\guru-rmm" 'guru-rmm' +Save-RepoStashes "$ClaudeToolsRoot\projects\msp-tools\guru-connect" 'guru-connect' +# untracked working diffs (e.g. tmp-*.diff) that aren't committed anywhere +$gcRepo = "$ClaudeToolsRoot\projects\msp-tools\guru-connect" +if (Test-Path $gcRepo) { + Get-ChildItem $gcRepo -Filter 'tmp-*.diff' -File -ErrorAction SilentlyContinue | ForEach-Object { + $dir = "$awRoot\guru-connect"; New-Item -ItemType Directory -Force -Path $dir | Out-Null + Copy-Item $_.FullName "$dir\$($_.Name)" -Force; Write-Host "[OK] at-risk untracked diff: guru-connect\$($_.Name)" + } +} + # --- manifests --- $m = "$root\manifests" $tools = 'node','npm','claude','gemini','grok','ollama','py','git','gh','jq','sops','age','cargo','rustc','code','op' diff --git a/.claude/bootstrap/restore-at-risk-work.ps1 b/.claude/bootstrap/restore-at-risk-work.ps1 index 582a15b..a966c1c 100644 --- a/.claude/bootstrap/restore-at-risk-work.ps1 +++ b/.claude/bootstrap/restore-at-risk-work.ps1 @@ -15,6 +15,18 @@ Non-destructive and re-runnable. If a patch won't apply cleanly (submodule moved on), it is reported and the .patch file is left in place for manual `git apply --3way`. + ROBUSTNESS NOTES (why this is not just `git apply `): + * Patch files may have been written by PowerShell redirection (UTF-16 LE/BE w/ BOM). + `git apply` only understands UTF-8/ASCII and otherwise reports + "No valid patches in input". Get-Utf8PatchPath normalizes any encoding to a + UTF-8 (no BOM) temp copy before applying. + * git writes progress/errors to stderr; capturing that with `2>&1` while + $ErrorActionPreference='Stop' turns it into a *terminating* error (PS 5.1 + NativeCommandError) that aborts the whole bootstrap. Invoke-Git captures + output without that trap and returns the real exit code. + * If the submodule still has stashes, the WIP almost certainly survived the reset. + Re-applying would create DUPLICATE stashes, so we skip and report instead. + .PARAMETER BundlePath Recovery bundle root (auto-detect F:\ then E:\). .PARAMETER ClaudeToolsRoot Default D:\claudetools. #> @@ -22,6 +34,35 @@ param([string]$BundlePath,[string]$ClaudeToolsRoot='D:\claudetools') $ErrorActionPreference='Stop' +# Read a patch regardless of encoding (UTF-16 LE/BE +/- BOM, UTF-8 +/- BOM) and return +# the path to a normalized UTF-8 (no BOM) temp copy that `git apply` can parse. +function Get-Utf8PatchPath($path){ + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { $text = [System.Text.Encoding]::Unicode.GetString($bytes,2,$bytes.Length-2) } + elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { $text = [System.Text.Encoding]::BigEndianUnicode.GetString($bytes,2,$bytes.Length-2) } + elseif ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $text = [System.Text.Encoding]::UTF8.GetString($bytes,3,$bytes.Length-3) } + else { + # No BOM: detect UTF-16 LE without BOM by counting interleaved NUL bytes in the head. + $nul = 0; $n = [Math]::Min(64,$bytes.Length) + for ($i=0; $i -lt $n; $i++) { if ($bytes[$i] -eq 0) { $nul++ } } + if ($nul -gt 8) { $text = [System.Text.Encoding]::Unicode.GetString($bytes) } + else { $text = [System.Text.Encoding]::UTF8.GetString($bytes) } + } + $text = $text -replace "`r`n","`n" # normalize to LF so git apply is happy + $tmp = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllText($tmp, $text, (New-Object System.Text.UTF8Encoding($false))) + return $tmp +} + +# Run git without letting native stderr (under $ErrorActionPreference='Stop') become a +# terminating error. Returns [pscustomobject]@{ Code; Output }. +function Invoke-Git([string[]]$GitArgs){ + $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' + try { $out = (& git @GitArgs 2>&1 | Out-String); $code = $LASTEXITCODE } + finally { $ErrorActionPreference = $old } + [pscustomobject]@{ Code = $code; Output = ($out).Trim() } +} + if (-not $BundlePath) { foreach ($d in 'F:','E:','D:') { if (Test-Path "$d\claudetools-recovery\at-risk-work") { $BundlePath="$d\claudetools-recovery"; break } } } $aw = "$BundlePath\at-risk-work" if (-not $BundlePath -or -not (Test-Path $aw)) { Write-Host "[INFO] no at-risk-work folder found in bundle - nothing to restore"; return } @@ -32,20 +73,29 @@ function Have-Git($repo){ Test-Path "$repo\.git" } # ---- guru-rmm stashes ---- $rmm = "$ClaudeToolsRoot\projects\msp-tools\guru-rmm" if ((Test-Path "$aw\guru-rmm") -and (Have-Git $rmm)) { - if (git -C $rmm status --porcelain) { + $existing = (Invoke-Git @('-C',$rmm,'stash','list')).Output + if ($existing) { + Write-Host "[SKIP] guru-rmm already has stashes (local WIP survived the reset) - not re-applying to avoid duplicates:" -ForegroundColor Yellow + Write-Host $existing + Write-Host " Bundle patches remain in $aw\guru-rmm; apply by hand if you really need them." -ForegroundColor Yellow + } + elseif ((Invoke-Git @('-C',$rmm,'status','--porcelain')).Output) { Write-Host "[WARN] guru-rmm working tree is dirty; skipping auto-restore to avoid mixing changes. Apply patches in $aw\guru-rmm manually." -ForegroundColor Yellow } else { # highest N first so stash0 lands at stash@{0} $patches = Get-ChildItem "$aw\guru-rmm" -Filter '*.patch' | Sort-Object Name -Descending foreach ($p in $patches) { - $check = git -C $rmm apply --check --3way $p.FullName 2>&1 - if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] won't apply cleanly, left for manual restore: $($p.Name) ($check)" -ForegroundColor Yellow; continue } - git -C $rmm apply --3way $p.FullName 2>$null - git -C $rmm stash push -u -m "restored WIP: $($p.BaseName)" 2>$null | Out-Null - Write-Host "[OK] re-stashed guru-rmm: $($p.BaseName)" -ForegroundColor Green + $u8 = Get-Utf8PatchPath $p.FullName + try { + $chk = Invoke-Git @('-C',$rmm,'apply','--check','--3way',$u8) + if ($chk.Code -ne 0) { Write-Host "[WARN] won't apply cleanly, left for manual restore: $($p.Name) ($($chk.Output))" -ForegroundColor Yellow; continue } + Invoke-Git @('-C',$rmm,'apply','--3way',$u8) | Out-Null + Invoke-Git @('-C',$rmm,'stash','push','-u','-m',"restored WIP: $($p.BaseName)") | Out-Null + Write-Host "[OK] re-stashed guru-rmm: $($p.BaseName)" -ForegroundColor Green + } finally { Remove-Item $u8 -Force -ErrorAction SilentlyContinue } } Write-Host "[INFO] guru-rmm stashes now:" -ForegroundColor Cyan - git -C $rmm stash list + Write-Host (Invoke-Git @('-C',$rmm,'stash','list')).Output } } @@ -53,7 +103,11 @@ if ((Test-Path "$aw\guru-rmm") -and (Have-Git $rmm)) { $gc = "$ClaudeToolsRoot\projects\msp-tools\guru-connect" $diff = "$aw\guru-connect\tmp-spec018.diff" if ((Test-Path $diff) -and (Test-Path $gc)) { - Copy-Item $diff "$gc\tmp-spec018.diff" -Force - Write-Host "[OK] guru-connect\tmp-spec018.diff restored (untracked working file - 'git apply --3way tmp-spec018.diff' to apply it)" -ForegroundColor Green + if (Test-Path "$gc\tmp-spec018.diff") { + Write-Host "[SKIP] guru-connect\tmp-spec018.diff already present in repo (survived the reset) - not overwriting." -ForegroundColor Yellow + } else { + Copy-Item $diff "$gc\tmp-spec018.diff" -Force + Write-Host "[OK] guru-connect\tmp-spec018.diff restored (untracked working file - 'git apply --3way tmp-spec018.diff' to apply it)" -ForegroundColor Green + } } Write-Host "[DONE] at-risk WIP restore" -ForegroundColor Cyan diff --git a/session-logs/2026-06-06-mike-mcp-gitauth-reinstall-tailscale.md b/session-logs/2026-06-06-mike-mcp-gitauth-reinstall-tailscale.md new file mode 100644 index 0000000..1c4d784 --- /dev/null +++ b/session-logs/2026-06-06-mike-mcp-gitauth-reinstall-tailscale.md @@ -0,0 +1,111 @@ +## User +- **User:** Mike Swanson (mike) +- **Machine:** GURU-5070 +- **Role:** admin + +## Session Summary + +Started from a `/doctor` report flagging the `ticktick` MCP server as failed ("Connection closed"). Diagnosed it as a missing-dependency crash, not an OAuth issue: the local stdio server `mcp-servers/ticktick/ticktick_mcp.py` is launched with bare `python` (Python 3.12) and that interpreter was missing `httpx` and (behind a try/except) `mcp`. Installed both into 3.12, created `mcp-servers/ticktick/requirements.txt`, and verified the server starts clean. The server now shows "Pending approval" (normal trust prompt) instead of crashing. + +Mike then stated a hard requirement: git must never sit at an interactive credential prompt — his objection to Git for Windows is the Git Credential Manager popups that hang automation, not the tooling itself. Discovered a live symptom: a backgrounded Gitea Agent push was hung on a GCM prompt (commit unpushed). Stopped it, pushed via the vault Gitea API token, then built a durable, fleet-wide non-interactive auth solution: `setup-git-auth.sh` (primes the `store` credential helper from the vault token, scoped per-repo by origin host, only seizing the helper from GCM `manager`), a backgrounded `SessionStart` hook, and `GIT_TERMINAL_PROMPT=0` / `GCM_INTERACTIVE=Never` in `settings.json` env. Corrected an earlier wrong memory (had attributed the dislike to the bash/path-mangling layer). All subsequent pushes this session used native `git.exe` via PowerShell with zero prompts. + +Mike disclosed the machine was recently reinstalled and asked to install anything missing. Found the reinstall installed Python deps into only one of the machine's two interpreters (`py`/3.14 for vault+scripts vs `python`/3.12 for MCP servers), which is exactly why ticktick (3.12) and `vault get-field` (3.14, missing PyYAML) were both broken. Installed PyYAML into `py`, `websocket-client` into `py` (cdp.py), and persisted `grok` to the User PATH. Ollama `list` came back empty, but the 5 expected models (47.8 GB) were intact on `D:\OllamaModels` — the tray app just needed a few seconds to hydrate; restarting it loaded all 5. No 48 GB re-download. Then amended the recovery script `windows-bootstrap.ps1` (Phase 7 both interpreters + pyyaml/websocket-client; Phase 3 persist grok PATH; Phase 6 prime git auth; Phase 8 full model set + hydration guard) and documented the gotchas in `machines/guru-5070.md`. + +Finally, Mike asked for help managing Tailscale for a tech-inept two-machine client. Recommended one tailnet per client (never merge into ACG's own), MSP holds Admin, devices enrolled as tagged nodes via pre-auth keys pushed from GuruRMM. Authored `wiki/patterns/tailscale-client-management.md` + `tailscale-client-enroll.ps1` (idempotent unattended Windows MSI install + tagged auth-key join). Scanned GuruRMM for the client (Robert Wolkin): found client `Wolkin, Robert`/site `Main` with 3 online Win11 Home agents. Mike clarified scope: connect RSW-Laptop -> front for file + printer sharing; DESKTOP-V1JT1SE is Bob's personal machine (out of scope). Created the `robert-wolkin` client stub, then added a reusable "files + printer over Tailscale (Windows)" section (SMB over the tailnet, the 445-firewall-on-Tailscale-interface gotcha, local-account auth on Home, MagicDNS FQDN, Point-and-Print via RMM, Taildrive alternative). + +## Key Decisions + +- **ticktick fix is dependency install, not OAuth.** "Connection closed" on a local stdio MCP server = process crash before handshake. `/mcp` authenticate would not have helped. +- **Non-interactive git auth via repo-local `store` helper, not global change.** Kept global GCM untouched for other repos; scoped the credential to each repo's actual origin host. `GIT_TERMINAL_PROMPT=0` enforced via committed settings.json so even an unprimed machine fails fast instead of hanging. +- **Conservative helper override.** `setup-git-auth.sh` only switches the helper to `store` when it is empty or GCM `manager`, so a Mac osxkeychain setup is left alone — safe to run fleet-wide via SessionStart hook. +- **Two-interpreter Python installs in bootstrap.** Root cause of the reinstall breakage; de-dupe by `sys.executable` so a single install isn't run twice. +- **Did not re-download Ollama models.** Confirmed manifests + 47.8 GB on `D:\OllamaModels`; an empty `ollama list` right after login is a hydration-timing artifact, not "models gone." +- **One tailnet per client.** Isolation, billing, offboarding, IdP blast radius. Never merge a client into ACG's tailnet or share one tailnet across clients. +- **SMB over Tailscale for Wolkin (not Taildrive).** A shared printer forces SMB regardless, so use SMB for both files and printer; no subnet router because files/printer live on a node. +- **Session log to root** (`session-logs/`) — multi-topic (machine maintenance + git infra + a client), no single article implied; wiki articles were hand-authored inline during the session. + +## Problems Encountered + +- **ticktick MCP "Connection closed":** missing `httpx` then `mcp` in Python 3.12. Installed both; server starts clean. +- **Gitea Agent background push hung:** GCM credential prompt invisible in background. Stopped the agent (TaskStop), pushed with the vault API token, then made auth non-interactive permanently. +- **`git credential approve` rejected input:** PowerShell piping mangled the multiline credential ("missing protocol field"). Wrote directly to `~/.git-credentials` idempotently instead. +- **PowerShell commit failed on embedded double quotes:** `"see each other"` in a commit message split into stray pathspecs (PS 5.1 native-arg quoting bug). Used `git commit -F ` / avoided embedded quotes. +- **`vault get-field` returned empty:** `yaml-query.py` (run via `py`/3.14) missing PyYAML. Installed pyyaml; added a `get`+grep fallback to setup-git-auth.sh. +- **Ollama `list` empty despite 47.8 GB on disk:** tray app server hydration delay (also the app vs CLI env). Restarted; all 5 models loaded. Documented as a bootstrap Phase 8 guard. +- **GuruRMM showed 3 agents, Mike expected 2:** clarified DESKTOP-V1JT1SE is Bob's personal machine, out of Tailscale scope. + +## Configuration Changes + +Created: +- `mcp-servers/ticktick/requirements.txt` — `httpx>=0.28`, `mcp>=1.27` +- `.claude/scripts/setup-git-auth.sh` — non-interactive git auth primer +- `.claude/memory/feedback_git_noninteractive_auth.md` — feedback memory (replaced the deleted `feedback_avoid_git_for_windows.md`) +- `wiki/patterns/tailscale-client-management.md` — MSP Tailscale pattern +- `wiki/patterns/tailscale-client-enroll.ps1` — GuruRMM enrollment script +- `wiki/clients/robert-wolkin.md` — client stub + +Modified: +- `.claude/agents/gitea.md` — "Non-interactive auth" guidance block +- `.claude/settings.json` — `env` (GIT_TERMINAL_PROMPT=0, GCM_INTERACTIVE=Never) + SessionStart hook for setup-git-auth.sh +- `.claude/bootstrap/windows-bootstrap.ps1` — Phases 3, 6, 7, 8 amended +- `.claude/machines/guru-5070.md` — Known issues (interpreter split, ollama hydration, grok PATH, git auth) +- `.claude/memory/MEMORY.md` — index pointer +- `wiki/index.md` — Patterns + Clients rows + +Deleted: +- `.claude/memory/feedback_avoid_git_for_windows.md` (superseded, never committed) + +Machine state (not in repo): +- Python 3.12 (`python`): installed `httpx`, `mcp` +- Python 3.14 (`py`): installed `pyyaml`, `websocket-client` +- User PATH: appended `~\.grok\bin` (also `~\.local\bin`, `%APPDATA%\npm`) +- `~/.git-credentials`: two store entries (internal + public Gitea hosts); stale `%3a3000` entry removed +- Ollama: restarted; 5 models loaded from `D:\OllamaModels` + +## Credentials & Secrets + +- **Gitea shared push account:** `azcomputerguru`, API token used for non-interactive push. Vault: `services/gitea.sops.yaml` field `credentials.api.api-token` (also `credentials.password`). Token cached locally in `C:\Users\guru\.git-credentials` (plaintext, local-only, scoped to `172.16.3.20:3000` + `git.azcomputerguru.com`). +- **GuruRMM API admin:** vault `infrastructure/gururmm-server.sops.yaml` fields `credentials.gururmm-api.admin-email` / `admin-password`. +- No new secrets created. Tailscale auth key for Wolkin not yet generated (pending Mike standing up the tailnet). + +## Infrastructure & Servers + +- **Gitea (internal):** `http://172.16.3.20:3000` (origin for claudetools push this session). Public: `https://git.azcomputerguru.com` (vault repo origin). +- **GuruRMM API:** `http://172.16.3.30:3001` (JWT auth). +- **MariaDB:** `172.16.3.30:3306` (firewall opened to 172.16.0.0/22 per incoming coord message; not exercised this session). +- **GURU-5070 interpreters:** `py` -> Python 3.14 (`C:\Users\guru\AppData\Local\Programs\Python\...`); `python` -> Python 3.12. +- **Ollama:** `OLLAMA_MODELS=D:\OllamaModels`, `OLLAMA_HOST=0.0.0.0:11434`. Models: nomic-embed-text, qwen3:8b, qwen3:14b, codestral:22b, qwen3.6:latest. + +### Robert Wolkin (GuruRMM) +- Client: `Wolkin, Robert` | Site: `Main` | site_id `2bb05f85-9fc8-4a7e-a5e5-ffe0c46431ac` +- Agents (Win 11 Home 25H2 build 26200, agent v0.6.57, all online 2026-06-06): + - DESKTOP-V1JT1SE — `30f6af79-ab19-4ed3-9ebc-71b2bffc2d27` — Bob's personal machine (OUT of Tailscale scope) + - RSW-Laptop — `043fd673-35a2-4d3d-8f91-ed73ce70cc1e` — Tailscale node (connects to front) + - front — `877d311a-4b24-462c-97b1-d2a0f7730a71` — Tailscale node (file + printer host) + +## Commands & Outputs + +- ticktick crash: `python ticktick_mcp.py` -> `ModuleNotFoundError: No module named 'httpx'`, then `[ERROR] MCP package not installed`. +- Installs: `python -m pip install httpx`; `python -m pip install mcp`; `py -m pip install pyyaml`; `py -m pip install websocket-client`. +- Non-interactive push pattern: `$env:GIT_TERMINAL_PROMPT='0'; git -C D:\ClaudeTools push origin main` (succeeds silently; PS 5.1 wraps git stderr as NativeCommandError on success — trust `$LASTEXITCODE`). +- Prime creds: append `http://azcomputerguru:@172.16.3.20:3000` to `~/.git-credentials` + repo-local `credential.helper=store`. +- Ollama fix: stop `ollama`/`ollama app`, start `ollama app.exe`, wait ~10s -> `ollama list` shows 5 models. +- GuruRMM lookup: `POST /api/auth/login` then `GET /api/agents`, filter `.client_name=="Wolkin, Robert"`. +- Firewall rule (planned for `front`): `New-NetFirewallRule -DisplayName "Tailscale SMB (files+print)" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 445 -RemoteAddress 100.64.0.0/10`. + +## Pending / Incomplete Tasks + +- **ticktick MCP:** restart session or `/mcp` reconnect to approve (currently "Pending approval"). Crash resolved. +- **grok:** on persistent User PATH but not callable in THIS already-running session; fine in new shells. +- **Tailscale (Wolkin):** Mike to stand up the tailnet on Robert's account, assign himself Admin, enable MagicDNS, set `tag:wolkin` ACL, generate reusable+pre-approved tagged auth key. Then: vault the key at `clients/robert-wolkin/tailscale-authkey.sops.yaml`, enroll RSW-Laptop + front via the enroll script, push post-connect SMB config. +- **OPEN ITEM (blocks printer step):** confirm whether the printer is USB-attached to `front` (Windows print share over SMB) or a separate network printer (install by IP on laptop, or subnet router on front). +- **Offered, not yet done:** stage the 4 RMM jobs (firewall + share/account on front; drive map + printer on laptop); wire enroll script into a GuruRMM script library vs ad-hoc. +- **Pre-existing WIP** (not from this session): `.claude/bootstrap/backup-to-bundle.ps1`, `restore-at-risk-work.ps1` (modified), `projects/msp-tools/guru-connect` (untracked) — swept by sync.sh `add -A`. + +## Reference Information + +- Commits (origin/main, pushed): `f3a175e` ticktick requirements; `9ff5a9f0` gitea agent auth docs; `162145b5` git-auth automation; `fd30af6a` bootstrap amendments + machine doc; `8d7e3805` tailscale pattern + script; `5c7e196b` wolkin stub; `32e71a13` wolkin RMM detail + scope; `f7540550` SMB files+printer pattern. +- Vault paths: `services/gitea.sops.yaml` (`credentials.api.api-token`), `infrastructure/gururmm-server.sops.yaml` (`credentials.gururmm-api.*`). +- Tailscale docs: subnet routers `tailscale.com/docs/features/subnet-routers`; kernel-vs-netstack `/docs/reference/kernel-vs-userspace-routers`; Windows MSI `/docs/install/windows/msi`; run unattended `/docs/how-to/run-unattended`; auth keys `/kb/1085/auth-keys`; firewall ports `/kb/1082/firewall-ports`; Taildrive `/docs/features/taildrive`. +- Tailscale MSI silent props: `TS_UNATTENDEDMODE=always`, `TS_NOLAUNCH`, `TS_LOGINURL`, `INSTALLDIR` (no auth-key property — use `tailscale up --authkey`). +- Wiki: `wiki/patterns/tailscale-client-management.md`, `wiki/patterns/tailscale-client-enroll.ps1`, `wiki/clients/robert-wolkin.md`.