fix(sessions): keep named-pipe socket paths inside the macOS sun_path limit (NEXT-01)

The pipe name mxaccess-gateway-{pid}-session-{32hex} plus .NET's
CoreFxPipe_ prefix overflowed the 104-byte Unix-domain-socket path limit
under the default per-user macOS TMPDIR (~49 chars), so every test that
opened a real pipe threw ArgumentOutOfRangeException at pipe creation
unless TMPDIR=/tmp was exported. Rename to mxgw-{pid}-{sessionUid} (the
session guid hex without the session- prefix; worst-case 43 chars) and
shorten the three test-fixture names the same way. Uniqueness is
unchanged: gateway pid + full session guid. The worker receives the pipe
name via its launch command line, so mixed Server/Worker deploy SHAs are
unaffected. Docs updated in the same change (gateway.md,
GatewayProcessDesign, GatewayConfiguration, Sessions, CLAUDE.md); new
regression test pins the format and the length budget.

Verified: SessionManagerTests 39/39; SessionWorkerClientFactory,
GatewayEndToEndFakeWorkerSmoke, WorkerClient, and ReconnectReplay suites
33/33 under the default macOS TMPDIR — this also retires the
previously-misdiagnosed 'macOS pipe-timeout test failures': they were
this path-length throw, not a timeout-message defect.
This commit is contained in:
Joseph Doherty
2026-08-10 05:54:11 -04:00
parent 0152180929
commit 8769ee9765
14 changed files with 769 additions and 15 deletions
@@ -0,0 +1,261 @@
# Follow-Ups: windev Redeploy, LDAP Test Fixtures, Runner Hygiene — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development
> (opus implementers; controller verifies ops evidence; final review pass).
**Goal:** Close the five items surfaced by the 2026-08-07 live-actions cycle: repair windev's
crash-looping gateway and finish the deferred SEC-36 dashboard verification (NEXT-07), fix the
DashboardLdapLiveTests fixture drift (NEXT-06), resolve the unexpected macOS instance runner,
harden runner-1's plaintext registration token, and verify the cargo Bearer fix.
**Architecture:** Three independent live streams (windev serial: 1→2→3; repo test fix: 4;
Gitea/runner hygiene: 5, 6) run concurrently; task 7 closes out docs/trackers. No contract,
gateway-logic, or client changes — one test-file edit (Task 4) plus live ops plus docs.
**Tech Stack:** SSH + PowerShell `-EncodedCommand` (windev 10.100.0.48), SSH + docker compose
(10.100.0.35), Gitea admin API (`gitea.dohertylan.com`, token via `~/.zshenv` `GITEA_TOKEN`),
NSSM, xUnit live-LDAP suite, GLAuth at `10.100.0.35:3893`.
---
## Preflight facts (verified before planning)
- Unpushed local mxaccessgw commits: `0566716`, `9760497`, `5b153da`, `41e8648` (all docs-only).
**`origin/main` = `a346d51`** — contains all current code, so windev can build from
`origin/main` without any push.
- windev (`10.100.0.48`): NSSM service `MxAccessGw`; deployed Server build of 2026-06-25
(Auth 0.1.2.0, supports auth-DB schema 2) crash-loops on
`C:\ProgramData\MxGateway\gateway-auth.db` migrated to schema 3 on 2026-07-15
(`AuthStoreMigrationException`, ~3.8k10k Hosting-failed events/day). The NEW LDAP secret is
already staged as the 10th `AppEnvironmentExtra` entry (SEC-36 Task 3) — preserve it.
- `DashboardLdapLiveTests.cs` (`src/ZB.MOM.WW.MxGateway.IntegrationTests/`): uses
`admin`/`admin123` (3 tests) and `readonly`/`readonly123`. Directory reality
(`scadaproj/infra/glauth/config.toml`): `admin` exists, password is the standard dev test
password (`password`, hash `5e884898…42d8` — same as `multi-role`), and IS in GwAdmin
(othergroups `[5610, 5701]`); `readonly` does not exist; `gw-viewer` (primarygroup 5611 =
GwReader, NOT GwAdmin) is the natural not-an-admin fixture. Test binds
`MxGateway:Ldap` from `appsettings.json` (**`Server: localhost`**) + env overrides — so the
live run needs `MxGateway__Ldap__Server=10.100.0.35` as well as
`MxGateway__Ldap__ServiceAccountPassword` (from Mac user-secrets, never printed).
- Gitea instance runners (`GET /api/v1/admin/actions/runners`): id 1 `gitea-runner` (cap 4),
id 4 `macos-local-Josephs-MacBook-Pro` (**unexpected, online, labels overlap
ubuntu-latest**), id 5 `gitea-runner-2` (cap 2).
- `10.100.0.35:/opt/gitea/docker-compose.yml` (+ `docker-compose.yml.bak-tst30`): runner-1's
registration token inline in plaintext env, file world-readable. runner-2 uses
`GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /run/secrets/runner_token` ← 0600
`/opt/gitea/runner_token`. runner-1 data volume `/opt/gitea/runner:/data` (its `.runner`
credential persists — the registration env is only needed for first registration).
- Cargo Bearer fix already applied (`~/.zshenv`, backup `~/.zshenv.bak-cli39`) and documented
(`docs/ClientPackaging.md`, commit `5b153da`). Task 7 verifies; no further action expected.
## Secret hygiene (binding, all tasks)
- Never print the LDAP service-account password, `GITEA_TOKEN`, cargo token, runner
registration tokens, or API keys — not in commands, logs, commits, or reports. Read the LDAP
password from `dotnet user-secrets list` into an env var without echoing
(e.g. `export MxGateway__Ldap__ServiceAccountPassword="$(dotnet user-secrets list --project src/ZB.MOM.WW.MxGateway.Server | awk -F' = ' '/ServiceAccountPassword/ {print $2}')"`).
- Documented dev **test users** (`multi-role`/`password`, `admin`/`password`,
`gw-viewer`/`password`) are NOT secrets — glauth.md publishes them; fine in code/commits.
- SSH→windev PowerShell: always `powershell -NoProfile -EncodedCommand <base64-UTF16LE>`;
never put secrets inside EncodedCommand blobs or argv.
---
### Task 1: NEXT-07 — Recon windev deployment layout + schema support
**Classification:** standard — read-only recon, but its output gates a service redeploy
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 4, Task 5, Task 6
**Files:** none edited. SSH recon on `10.100.0.48` + repo/scadaproj reads on the Mac.
Determine everything Task 2 needs, and confirm the fresh-deploy path is safe:
1. `nssm get MxAccessGw Application`, `AppDirectory`, `AppParameters`,
`AppEnvironmentExtra` (count entries; do NOT print values of secret-bearing entries —
names only).
2. Inventory the deployed dir: path, `ZB.MOM.WW.MxGateway.Server.exe` timestamp, whether
`appsettings.json`/`appsettings.Production.json` in the deploy dir differ from repo
`origin/main` (diff; windev-specific config must survive the redeploy).
3. Confirm build feasibility on windev: `dotnet --list-sdks` (need 10.x), locate an existing
mxaccessgw checkout/worktree (CI uses `scripts/ci/windev-worker-ci.ps1` — find its
worktree path) or pick a fresh clone location. Confirm `git fetch` reaches `origin/main`
= `a346d514dd24e775640e5667aa7cd8e561fec68a`.
4. Confirm current code supports auth-DB schema 3: find the auth-store supported-schema
constant (ZB.MOM.WW.Auth packages — check the package version the Server at `origin/main`
references, and/or the migration code in the shared scadaproj libs) and state the
evidence. **If current code does NOT support schema 3, STOP — report, do not deploy.**
5. Gateway endpoints for verification: bound URLs/ports (from deployed config/env), dashboard
scheme (http vs https → cookie will be `MxGatewayDashboard` vs `__Host-…`).
6. Check what migrated the DB to schema 3 on 2026-07-15 (event log / file timestamps) — only
to confirm schema 3 is the shared-lib current version, not an anomaly.
**Step: report** all findings as structured text (no secrets); no changes, no commits.
### Task 2: NEXT-07 — Build current Server on windev and redeploy the service
**Classification:** high-risk — replaces a running (crash-looping) service's binaries
**Estimated implement time:** ~10 min
**Parallelizable with:** none (needs Task 1)
**Files:** none in repo. windev filesystem + NSSM only.
Using Task 1's facts:
1. On windev, fetch/checkout `origin/main` (`a346d51…`) in the build worktree/clone.
2. `dotnet publish src/ZB.MOM.WW.MxGateway.Server -c Release` (match deployed layout/RID from
Task 1; framework-dependent vs self-contained must match what NSSM `Application` points at).
3. Stop the service (`nssm stop MxAccessGw`), confirm process exited.
4. Backup: deployed dir → sibling `*.bak-next07` copy; copy
`C:\ProgramData\MxGateway\gateway-auth.db` (+ `-wal`/`-shm` if present) to
`gateway-auth.db.bak-next07`. **Never delete the live DB.**
5. Deploy publish output over the deploy dir, then restore any windev-specific config files
identified in Task 1 (do not clobber live overrides; NSSM env entries are untouched by
file copies but verify count unchanged after start).
6. `nssm start MxAccessGw`; verify: service state RUNNING and stable ≥60 s (no restart
cycle), Application event log shows clean host start and **zero new
`AuthStoreMigrationException` / `Hosting failed to start`** after the start timestamp,
bound port answers (e.g. dashboard root or health endpoint returns HTTP).
7. Rollback if unhealthy: stop, restore `*.bak-next07` dir, start, report.
**Step: report** deployed SHA, verification evidence, backup paths. No repo commits.
### Task 3: SEC-36 deferred verification + NEXT-07/runbook closeout
**Classification:** standard
**Estimated implement time:** ~6 min
**Parallelizable with:** none (needs Task 2)
**Files:**
- Modify: `docs/runbooks/SEC-36-ldap-credential-rotation.md` (Correction 3 — mark the
deferred dashboard check done, dated)
- Modify: `archreview/2026-07-12/remediation/90-candidate-findings-next-cycle.md` (NEXT-07 →
resolved 2026-08-07, evidence one-liner)
1. Complete SEC-36 step 4 against the repaired windev gateway: log in to the dashboard as
`multi-role`/`password` end-to-end. Preferred: `curl` flow — GET `/login` (capture
antiforgery token + cookie), POST credentials, expect success redirect + auth cookie
(name per Task 1 scheme). If the login page resists scripting (Blazor circuit), report
exactly why and fall back to asserting a fresh `DashboardLdapLiveTests` green run
(Task 4) plus windev log evidence of successful LDAP bind on a manual attempt.
2. Update the two docs; commit locally (`docs(sec-36,next-07): …`), do NOT push.
### Task 4: NEXT-06 — Fix DashboardLdapLiveTests fixtures to match the shared directory
**Classification:** small — one test file, but must go green against live GLAuth
**Estimated implement time:** ~6 min
**Parallelizable with:** Task 1, Task 5, Task 6
**Files:**
- Modify: `src/ZB.MOM.WW.MxGateway.IntegrationTests/DashboardLdapLiveTests.cs`
- Modify: `archreview/2026-07-12/remediation/90-candidate-findings-next-cycle.md` (NEXT-06 →
resolved)
- Possibly modify: `docs/GatewayTesting.md` (live-LDAP opt-in row: document the
`MxGateway__Ldap__Server` override needed when GLAuth is not localhost)
1. Read `DashboardAuthenticator` first: confirm a user who binds successfully but maps to no
role yields `Succeeded == false` (drives the gw-viewer fixture).
2. Fix fixtures: `admin`/`admin123``admin`/`password` (positive + wrong-password +
unreachable tests); `readonly`/`readonly123``gw-viewer`/`password` (exercises
user-binds-but-lacks-GwAdmin; keep the no-password-leak assertion, updating the asserted
literal). Update XML doc comments to match. Keep MXAccess-repo style rules
(TreatWarningsAsErrors).
3. Build: `dotnet build src/ZB.MOM.WW.MxGateway.IntegrationTests` (macOS OK — net10.0).
4. Live run (env only, never echo the password):
`MXGATEWAY_RUN_LIVE_LDAP_TESTS=1 MxGateway__Ldap__Server=10.100.0.35 MxGateway__Ldap__ServiceAccountPassword=<from user-secrets> dotnet test … --filter FullyQualifiedName~DashboardLdapLiveTests`
→ expect **5/5 passed** (this is also positive live proof of the SEC-36 service-account
bind).
5. Update tracker row (+ GatewayTesting.md if the Server-override note is missing); commit
locally (`test(ldap): …`), do NOT push.
### Task 5: Resolve the unexpected macOS instance runner (id 4)
**Classification:** standard — evidence-gated removal of a live runner registration
**Estimated implement time:** ~6 min
**Parallelizable with:** Task 1, Task 4, Task 6
**Files:**
- Modify: `docs/runbooks/TST-30-second-ci-runner.md` (the Correction paragraph mentions
"id 4 — an unrelated local macOS runner" — update to final state)
1. Evidence, local: `pgrep -fl act_runner`, `launchctl list | grep -i act`,
`brew services list | grep -i act`, look for `~/.runner`/act_runner config dirs. Evidence,
Gitea (token from `~/.zshenv`, never printed): runner detail for id 4 (labels, last
online), and whether any recent runs' jobs report `runner_id == 4`
(`GET /repos/{owner}/{repo}/actions/runs?…``…/runs/{id}/jobs` for both `mxaccessgw`
and `lmxopcua` recent runs).
2. Decision rule: the runner advertises ubuntu labels from a macOS host, so it can steal
Linux container jobs → **remove it** unless evidence shows it deliberately serves jobs
the docker runners cannot (none expected). Removal = stop the local act_runner process
AND disable its autostart (launchd/brew), then `DELETE /api/v1/admin/actions/runners/4`.
Keep the local config file (renamed `*.disabled-2026-08-07`) so re-registering with
mac-specific labels stays easy; note the re-registration recipe in the runbook edit.
3. Verify: admin runner list shows only ids 1 and 5, both online; no act_runner process
locally; a `pgrep` after 60 s still empty (nothing respawned).
4. Update the TST-30 runbook correction paragraph; commit locally, do NOT push.
### Task 6: Harden runner-1's registration token on 10.100.0.35
**Classification:** standard — touches the live CI stack's compose file
**Estimated implement time:** ~7 min
**Parallelizable with:** Task 1, Task 4, Task 5
**Files:** none in repo (host `/opt/gitea/` only; runbook note lands in Task 7 if needed).
1. Preconditions on the host: confirm runner-1's `/data/.runner` exists in its volume
(registration credential persists → the registration env var is no longer needed);
confirm both runners idle (no `act_runner`-spawned job containers, no in-progress runs
via API) before recreating.
2. Edit `/opt/gitea/docker-compose.yml` (backup first → `docker-compose.yml.bak-tst30b`):
replace runner-1's inline `GITEA_RUNNER_REGISTRATION_TOKEN: <plaintext>` with the same
`_FILE`/secrets pattern runner-2 uses (`/opt/gitea/runner_token`, 0600). Do NOT touch the
`gitea` service definition.
3. `docker compose up -d --no-deps` the runner-1 service only; verify it comes back online
in the admin runner list and its `.runner` identity is unchanged (still id 1).
4. Tighten perms: `chmod 600 /opt/gitea/docker-compose.yml docker-compose.yml.bak-tst30 docker-compose.yml.bak-tst30b`
(verify compose stack still operable by the deploy user).
5. Rotate the leaked registration token if the deployment allows:
`docker exec … gitea actions generate-runner-token` (or admin API) — if Gitea offers no
invalidation of the old value, say so explicitly in the report (residual risk: LAN actor
could register a rogue runner until rotation) rather than claiming it rotated.
6. Verify CI still works: trigger nothing; just confirm both runners online and the token
file perms; a real push lands naturally later. Report evidence.
### Task 7: Closeout — cargo Bearer verification, docs/tracker sync, commits
**Classification:** small
**Estimated implement time:** ~5 min
**Parallelizable with:** none (needs Tasks 3, 4, 5, 6)
**Files:**
- Modify: `archreview/2026-07-12/remediation/90-candidate-findings-next-cycle.md` (final
state of NEXT-06/NEXT-07 rows if Tasks 3/4 left anything)
- Possibly modify: `docs/GatewayTesting.md` / TST-30 runbook (runner topology now ids 1+5
only; token-hardening note)
1. Cargo Bearer verification (no printing): assert `~/.zshenv` line matches
`CARGO_REGISTRIES_DOHERTJ2_GITEA_TOKEN="Bearer …"` via `grep -c`, confirm
`docs/ClientPackaging.md` note present (commit `5b153da`); check no other credential
location (CI secrets, windev profiles) publishes to cargo — expected none.
2. Sweep: every doc touched this cycle consistent (runbooks, trackers, GatewayTesting.md);
`git grep` for stale phrases ("crash-loop… pending", "id 4", "admin123") and fix.
3. Commit remaining doc changes locally; do NOT push. List the full unpushed stack in the
report.
---
## Out of scope
- Pushing any mxaccessgw commits (user decides; stack listed at closeout).
- The five next-cycle candidate findings other than NEXT-06/NEXT-07.
- Auth-DB restore path for windev (fresh deploy chosen — preserves schema-3 data).
- `ci.yml` changes (labels, concurrency groups).
## Dependency graph
```
{1} → 2 → 3 ┐
{4} ├→ 7
{5} │
{6} ─────────┘
```