Site-vs-product audit (2026-10-09, owner decisions: "advertise as Premium"
and "It should be LLM readable") reconciles the entitlement triangle:
marketing copy <-> PREMIUM_FEATURES <-> license-server grant list.
(Submitter naming: this is judge round 5 for artifact dashcaddy-dc165-code;
"rev5" is the source revision label — one revision per graded round.)
r4 judge blockers addressed:
- B0 (hygiene glob + deterministic patch discovery): verifier now scans
"$d"/*PATCH.diff and *.md/*.sh GLOBS instead of enumerated name lists —
the judge worktree's 4_PATCH.diff (any index) is found by shape; PATCH
hash discovery likewise globs OUT_DIR/SELF_DIR with NO hardcoded
/root/dc165-judge fallback (B2). Fresh pass + fail-closed transcripts
regenerated from the exact submitted bundle (attached).
- B1 (derivation honesty): openapi.yaml + capabilities.js header now split
the SIX MEASURED fields (version, templates count/categories, tools,
transport triple, premium list, language count — each read at request
time from the runtime module) from the STATIC metadata fields (product,
description, restMount, endpoint — registration facts visible in app.js,
labeled as such). The no-drift claim attaches only to the measured set.
- B2: external hardcoded fallbacks removed (above).
- P0: revision labels synchronized (this paragraph).
- P1: fleet wiring grep asserts the EXACT literal requirePremium('fleet')
mount, not any requirePremium occurrence.
Carried from rev2 (r1 blockers — unchanged content): real-composition-root
fleet gate e2e (4 tests: no-activation 403 envelope contract, premium-without-
fleet 403 per-feature, with-fleet gate cleared, prefix inheritance across
hosts/status/deploy); MCP_RUNTIME single-source in routes/mcp.js (transport
literal occurs exactly once in the repo, read by both /info and
capabilities; unit test asserts manifest === MCP_RUNTIME); OpenAPI 200-only
public contract; APPEND-ONLY offline feature migration (legacy entries
preserved; load() complexity extracted into _migrateOfflineFeatureSnapshot,
lint-clean); eslint rc hard-gated on a FRESH per-run capture incl.
middleware.js.
Product behavior: 'fleet' + 'workflows' + 'auto-backup' in PREMIUM_FEATURES
(descriptions source-quoted: bundled-workflows ids; /backups/schedule
schema fields); requirePremium('fleet') mounted at the /fleet ROUTER prefix
BEFORE fleetRoutes; GET /api/v1/capabilities public machine-readable ground
truth (PUBLIC_ROUTES exact GET + drift-test directMount + openapi doc);
offline-only forward-migration (online activations untouched — server
authoritative across the network boundary).
Diff: 9 files vs 4143bff (exact stats in EVIDENCE header).
Tests: 9 dc165 + 4 fleet-e2e + 76 license-manager + 21 tier + 14 drift =
124 targeted green; 50 MCP route tests re-verified green after the
MCP_RUNTIME refactor (77 combined on the focused rerun).
Feature: /etc/dashcaddy/operator.env (root-owned KEY=VALUE file) is
forwarded into the DashCaddy container as docker -e flags by start.sh.
Canonical flag: ALLOW_LIFETIME_LICENSE=true on the CREATOR host only.
This is a client-side rail, NOT the security boundary: lifetime codes
must HMAC-verify against the creator-only signing secret, which is never
shipped to customers (installs carry only .license-secret-hash; the
license server never issues lifetime codes). Flipping the flag on a
customer host grants nothing. Missing file = normal case (zero flags).
Design (per judge blockers):
1. scripts/operator-flags.js — Node, ONE pinned file descriptor:
lstat pre-check (reject FIFO/device/dir BEFORE any open that could
block: r5 DoS blocker) then open(O_RDONLY|O_NOFOLLOW|O_NONBLOCK),
fstat, read from the SAME fd (r3 TOCTOU blocker closed; O_NONBLOCK
makes even an adversarial race on a FIFO non-blocking). Contract:
regular file; no group/other access bits; no setuid/setgid/sticky;
owner uid 0 under root runtime (owner==euid otherwise). Symlink
refused at open even when the target would pass all checks.
Violation => stderr FATAL + exit 3. Content is pure data: never
sourced/evaled/executed; $(...) values stay inert text (tested).
2. start.sh: rc 3 => warn + forward NOTHING (fail closed); ANY other
non-zero rc => ABORT startup (fail loud, r4 blocker; tested via
missing-checker simulation); missing file => silent zero overrides.
Single helper invocation; keys-only audit line at boot (no values).
3. Artifact hygiene (r3/r5): verifier section [8] EXECUTES a scan of
the judge-dir + evidence-dir for full-format license codes and FAILS
if no PATCH.diff is present at all (no vacuous pass); indexed
0_/1_/2_/3_ names recognized. (An earlier round leaked one code; it
is dead — signing secret rotated, keygen verify reports INVALID.)
Root-cause hardening (2026-10-08 incident: deploy checkouts overwriting
git-TRACKED runtime state revoked the owner's premium and hid service
cards):
- untrack dashcaddy-api/data/services.json AND add an explicit
.gitignore entry for it (r5: untrack alone leaves accidental
re-add possible)
- untrack dashcaddy-api/.license-secret + .license-counter, extend
.gitignore. The secret's old value in git history is already rotated,
hence cryptographically dead.
Tests: 17 executed operator-flags tests — incl. FIFO regression (mkfifo,
no writer, hard 2s timeout, expect immediate rc 3), O_NOFOLLOW
symlink-to-valid-target, 0644/0755/setuid/directory/foreign-owner/usage
rejections, missing-file-silent, injection-inert, full translation suite;
license-manager 76 + tier-enforcement 21 untouched => 114 targeted green.
Judge r1 (C) blockers B1-B3 + polish both applied — see body of rev1 commit for feature scope.
urn:ump:l2tg66wmchkmd6fde4k3ojwc56zpxqh657spe37bu6dpvduswalq
The floating assistant was broken end-to-end; Sami said "Do it that works".
Three user-visible bugs fixed + the promised LLM delegation implemented:
FRONTEND (status/js/ai-chat.js — rewritten):
- Response parsing: old code read data.data; the API returns the FLAT ok()
envelope — EVERY reply rendered "Sorry, something went wrong". Now reads
the real shape (contract asserted by route test).
- Suggestion chips: onclick="...send('"Deploy Plex"')" — nested quotes,
invalid JS, dead buttons (shipped like this). Now data-driven buttons with
addEventListener.
- Deploy button: called window.openAppSelector which NEVER existed anywhere
in the tree (grep-verified). Now opens the REAL deploy modal via new
window.openDeployConfigFor(templateId) exported by app-selector.js —
resolves against the live API template cache, reuses showDeployConfig
(same path a manual catalog click takes). Widgets route to the grid.
- Conversation history (last 6 turns) now rides along per request, engine
badge in header, ⚙ settings sub-panel (baseUrl/model/API key + Test +
Remove), all server text via textContent (no HTML injection), CSP-safe
(no inline handlers).
BACKEND (routes/ai-intent.js):
- TWO engines, one contract. LLM classifier (OpenAI-compatible chat
completions) with system prompt embedding the LIVE 78-app catalog + the
six wizard categories; regex patterns kept as the zero-config default and
the automatic failover (HTTP error, timeout via AbortController,
unparseable output -> pattern answers; response carries engine +
llmError so the UI can say which served — no silent degradation).
- Closed-vocabulary schema validation: intent MUST be in the known set else
'unknown'; appId MUST exist in APP_TEMPLATES else nulled (hallucinated
ids cannot leak into deploy plans); categories filtered to the wizard's
real keys; reply stripped of control chars + bounded to 600; history
role-filtered (only user/assistant pass — client-injected system turns
dropped) + capped; input capped to 1000 chars.
- enrich() rebuilds the response contract per intent; pattern-canned
response is dropped when the LLM rerouted the intent (no stale shape);
deployPlan body.port now fills from the template's defaultPort (was
hardcoded null); deployPlan shape asserted against wizard usage.
- Key storage: PUT /ai/settings stores {baseUrl,model,apiKey} in the
ENCRYPTED credential store (key 'ai:llm') via ctx.credentialManager —
NOT config.json (GET /config echoes the raw file to the browser; that
route's shape was verified before this decision). Blank apiKey on PUT
keeps the stored one. GET /ai/settings reports configured/source/model/
hasApiKey — the key value NEVER crosses the wire (asserted). Env
fallback AI_LLM_BASE_URL/AI_LLM_API_KEY/AI_LLM_MODEL + legacy
LLM_PROXY_URL for headless installs. POST /ai/settings/test = one-shot
real classify probe returning ok/sample or ok:false/reason (never
throws). DELETE clears.
- GET /ai/capabilities now reports engine + llm status (pattern was honest
marketing before; now it can be true).
MOUNT (src/app.js): aiIntentRoutes receives siteConfig, log, fetchImpl
(ctx.fetchT — test-injectable) and credentialManager.
Docs: OpenAPI +4 AI path entries (intent, capabilities, settings, settings/test); CHANGELOG entry; dist bundle rebuilt
(openDeployConfigFor present in features.js; SW cache c62511c40d).
Tests: 20 NEW in __tests__/routes/ai-intent-engine.test.js (engine
selection w/ and w/o store, off-catalog phrasing via mock LLM,
hallucinated-id nulling, illegal intent coercion, HTTP-500 failover,
AbortError timeout failover, markdown-fence repair, reply sanitization,
history cap+role filter, env config path, intent-disagreement rebuild w/
real jellyfin port 8096, flat envelope contract, 400s, 1000-char cap,
settings GET/PUT/DELETE/test incl. key-never-echoed assertions,
capabilities engine reporting) + the 18 original pattern tests unchanged +
composition-root e2e 8 pass. Full suite: 3643 passed; only the pre-existing
pdf-parse billing family red (6 failed in documented band [5,7], patch
touches no billing file).
Judge r2 (grade C) — all 3 blockers + both polish items:
1. REQUEST-vs-NOTIFICATION by PRESENCE, never truthiness: validateJsonRpc
uses hasOwnProperty('id'); id 0 and id '' are VALID request ids echoed
back BY VALUE (strict-equality tested), id:null treated as a request
under the documented error-id convention. mcp-core default branch fixed
the same way (`if (id)` → hasOwnProperty — id 0 / '' unknown-method now
answer -32601 with the id echoed instead of being swallowed as falsy).
Routes' malformed-notification silence path also switched to
presence-based detection (id:null + bad params → 400 envelope, not 204).
2. params:null REJECTED (-32600): contract is object-or-omitted ONLY
(strict JSON-RPC; the old code's m.params !== null guard contradicted
the documented contract). Boundary tests pin the policy.
3. Unknown-method boundary tests: id 0 → -32601 id:0, id '' → -32601 id:'',
id null → -32601 id:null — asserted at BOTH layer levels (route).
Polish:
- 16-case TABLE-DRIVEN boundary matrix covering every allowed/rejected
id/params combination incl. 202 vs 204 notification split.
- Verifier now probes writable /tmp up front (exit 73 + clear message if
unavailable; documented requirement) instead of dying at mktemp.
- Verifier target count 57 → 73 (DC-162 suites) with the boundary table
named in the assertion label.
Full DC-162 suite: 73 passed (route 50 + core 23). No transport/credential
semantics changed from rev2.
urn:ump:or2qqhcytp6lfzlyi7xru7hdqy3uar2hbjllsrrbxrbray3c5kpa
All three r3 blocking issues fixed + polish applied:
- Radicale provision route now FAILS CLOSED (409) unless the target container
is present, inspectable, and its image matches /radicale/i. Missing
containerId, failed inspect, or non-Radicale image → nothing is written to
the shared config and no source/credential is registered. 4 new tests cover
the three refusal paths + the verified-image happy path.
- ICS feed fetch now streams with a hard 5 MiB body cap (same bound as the
CalDAV path) via readBodyCapped(); oversized feeds reject mid-stream and
degrade to per-source error. Test covers a >5 MiB feed end-to-end.
- Verifier mode separation: `--verify` runs the same checks fully read-only
(all output under mktemp -d in /tmp, transcript printed to stdout, scratch
removed on exit — proven rc=0 with zero repo/tmp-side writes); default
transcript mode is explicitly declared a TRANSCRIPT GENERATOR in its header
and writes only under the script's own directory.
- Full-suite count assertions replaced with the documented invariant
(failed ∈ [5,7], failing set == known-flaky billing suite, extras pass
isolated rerun) — judge r3 polish.
- Stale credential-key comment in radicale-provisioner.js header corrected.
Calendar suites: 43 passed (38 + 5 new). Perf note: readBodyCapped streams;
no full-buffer read of unbounded bodies on the ICS path anymore.
All three r2 blocking issues fixed + polish applied:
- useExisting (adopted) Radicale containers no longer get a source registered
with credentials the operator never provisioned. Registration happens ONLY
when pre-start provisioning succeeded; adopted deployments get a
calendarHint in the deploy response pointing at the manual-wiring modal.
- Weekly BYDAY expansion: candidates earlier than DTSTART are skipped (they
do not consume COUNT); window inclusion now uses interval-overlap
semantics (occurrence span vs window span) for both recurring and
non-recurring events. Three new tests cover midweek DTSTART, a straddling
multi-day event, and a boundary-exactly-ending event.
- Verifier is now read-only on the graded tree: no stash/checkout/rewrite.
Base proof runs in a temporary git worktree of 448f535 under /tmp (removed
on exit); the flaky pdf-parse suite is proven pre-existing via "red at
base" + "patch touches no billing file" (empty diff), and a race-flaky
suite (DC-116, witnessed once under parallel load) is given one isolated
rerun which must pass.
Polish: PUT /sources normalizes the user field (and clamps name) like POST.
Calendar suites: 38 passed (35 + 3 new recurrence tests). The 3558/3564
full-suite numbers vary by one flaky pdf-parse member between runs; the
evidence now asserts the invariant (suite red at base, no billing file in
the patch, flaky suites green on isolated rerun) instead of an exact count.
All five r1 blocking issues fixed + polish items applied:
- Unified credential-key convention: registerAutoWiredSource now stores at
calendar:source:<source.id> — the same key the events fetcher resolves —
so auto-wired sources authenticate on first fetch (was calendar:radicale:
<subdomain>, fetched key never written = empty-password CalDAV auth).
- calendarStateManager is now passed INTO assembleContext(...) (the r1 patch
only extended the function signature, so ctx.calendarStateManager was
undefined and the deploy hook silently skipped auto-wiring).
- Radicale config is provisioned BEFORE container start (Radicale reads its
config at startup): the deploy hook writes config+htpasswd ahead of
deployContainer() and passes the pre-start credentials to the post-deploy
registration step; useExisting path logs an explicit restart warning.
- PUT /sources/:id now validates URL updates through the same shared
validateSourceUrl() used by POST (protocol + remote-plaintext rules).
- New end-to-end contract test (calendar-autowire.e2e.test.js): spins a real
HTTP CalDAV server, registers an auto-wired source, then fetches events
through the API — the server verifies Basic auth against the provisioned
password, so a credential-key or wiring regression fails the suite.
Polish: provisioning is idempotent (deterministic source id, update instead
of duplicate), unbiased rejection-sampling for password/salt generation,
writeRadicaleConfig docstring no longer claims idempotence.
Calendar suites: 35 passed (32 prior + 3 new). Lint: 0 errors; complexity
warnings in deploy.js pre-exist at base (61 vs 68 now — the hook did not
worsen the trend line materially; calendar files themselves lint clean).
- __tests__/e2e-node/composition-root.e2e.test.js: `boot()` takes {timeoutMs, code}
and the timeout rejection carries the child it killed; a NEW test drives the real
timeout against a child that never reports PORT/READY and asserts the OS-observed
SIGKILL on that child (marker e2e-timeout-kill) — the r4 claim is now exercised.
- routes/deploys.js: bridge() coerces any non-object JSON body (null/[]/string) into
the same failure shape as a non-JSON body, so no `body.ok` deref can 500 or report a
false success; the subdomain label regex rejects leading AND trailing hyphens.
- __tests__/routes/deploys.catalog.r4.test.js: trailing-dash rejection case + three
malformed-successful-body cases (clean 502, never 500).
readonly BACKUP_DIR (constants) collided with the BACKUP_DIR= tunable, so every
curl|bash run died with 'BACKUP_DIR: readonly variable'. Removed the readonly
constant and resolve the default (/etc/dashcaddy/backups) at end of parse_args;
--backup-dir flag still overrides. Live file + /opt copy patched identically.
BUILD_GUIDE.md wrongly said output goes to dist/ (actual: build-output),
mac builds produce .dmg (actual target: zip), and cited a nonexistent
win-unpacked portable path. Corrected to the verified config truth and
added the wine64+wine32:i386 cross-build requirement discovered today:
without wine32 the NSIS setup exe ships as a 211KB payload-less stub
while electron-builder exits 0. Documented the authoritative 7z payload
check + post-build secrets scan. Round1 judge B+5 polish, folded:
version placeholders, heuristic-vs-authoritative wording, distro/sudo
notes, mac zip wording. Judge: qwen3.8-max stand-in lane.
Also removes package.json.bak and index.js.bak (stale junk).
Batch 2 (installer). Root causes and fixes:
1. config-manager tests asserted stale layout (<path>/config/config.json);
canonical implementation writes flat <path>/config.json matching the
Docker volume-mount contract + REQUIRED_DIRS. Tests aligned to the
production contract (not the other way) + create testDir in beforeEach.
2. saveConfig ENOENT when the install dir didn't exist: now mkdirs the
parent before writing (implementation fix; wizard passes user-typed paths).
3. REAL BUG: saveDNSCredentials/loadDNSCredentials silently dropped the tld
field on round-trip (property test caught it). Now persisted plaintext
(non-secret, like server/username) and restored on load; type-normalized
to string-or-null (judge polish #4).
4. installDocker/installCaddy unit tests hit the real network via
DownloadManager (only child_process mocked) -> 5s timeouts. Now
jest.mock('./download-manager') with fail-fast stubs.
5. dependency-checker.property.test.js ran real exec/downloads (caddy is
installed on this box). Now hermetic: child_process + download-manager
mocked at file scope.
6. installDocker fallback message parroted raw downloader error; now steers
user to the manual instructions returned alongside (satisfies the test
contract AND improves UX).
Also: mkdtemp test dirs (judge polish #1), nested-mkdir regression test
(polish #7). Installer suite: 119/119 green, 7/7 suites (was 17 failed /
4 suites red). Judge: Qwen lane grade A, 0 blocking, 8 polish (1,3,4,7
applied here), verdict /tmp/judge-batch2-verdict.json.
- routes/ca.js: declare format before pfx/pem/crt dispatch (was ReferenceError
on every request that passed validation); add CA_CERT_FORMATS single source
of truth + hardened format extraction (string-coerce, whitelist)
- routes/caddycode.js: fix upstream-validation regexes (control-char classes)
- routes/logs.js: SSE/validation lint fixes
- routes/openclaw.js: remove useless regex escape in ALLOWED_PATH_RE
- fleet-validation.js + http-caddy-admin-origin test: eslint-disable for
intentional control-regex security sentinels
- ca-dc076.routes.test.js: regression test for declared format + behavioral
coverage of format validation (now pre-PKI)
Judge: Qwen lane (qwen3.8-max) grade B, 0 blocking, verdict
/tmp/judge-batch1-verdict.json. API suite 2859/2859 green.
Alpine/musl getaddrinfo (used by Node dns.lookup/tls.connect) queries all
resolv.conf nameservers in parallel and takes the first reply. 8.8.8.8
NXDOMAINs the internal .sami TLD and won that race 18/400 measured inside
the live container — the source of ssl-monitor 'Failed to check cert'
ENOTFOUND warn noise (and the original reason for the git.sami hosts pin).
DNS1 Technitium secondary (100.71.97.12) serves *.sami AND recurses
externally, verified from inside the container, so both race winners are
correct. Shell-only change; no JS/test context touched.
[glm-grade=B] (Codex cold-read, 0 blocking; polish items folded: live-state
claims now carry measured provenance, musl-vs-c-ares attribution verified
by discriminating test A/B/C)
The deploy host showed phantom dist drift on every git pull: committed
dist bundles could not be reproduced on DNS2. Root cause (verified with
esbuild 0.25.12 probes): the production transform uses sourcemap:'both',
whose inline map base64-embeds RAW source bytes as sourcesContent — a CRLF
working copy (Windows dev, core.autocrlf=true) vs an LF checkout produces
different dist bytes and a different sw.js cache tag.
- build.js: normalizeSource (\r\n -> \n) on every source read (bundle
inputs + sw.js read); exported + require.main guard so tests can import
it without triggering a build
- .gitattributes: * text=auto eol=lf (git-layer kill of the CRLF vector)
+ binary exclusions; 9 CRLF-in-index asset files renormalized
- tests/build-determinism.test.js: 3-case pin (byte-identity after
normalization, divergence pre-normalization, CR-strip contract) importing
the ACTUAL normalizeSource from build.js
- package.json: declare jsdom devDependency — 2 committed test files
require('jsdom') but it was never declared, so fresh-checkout
npm test failed (it only passed where a stray ancestor node_modules
happened to contain it)
- dist/features.js + sw.js: canonical deterministic rebuild
(cache tag 3354f5fd96 -> d39ab69dd4)
Verified: status 54/54, API 2858/2858 (128 suites); CRLF-sim tree and LF
tree of same HEAD produce byte-identical dist artifacts (sha256-equal).
Judge: glm-5.3 cold round 1 = A, round 2 (post-fold) = A clean, 0 blocking.
The host-side uptime watchdog and on-host cron jobs curl Caddy with a stock
curl/8.5.0 UA from loopback and the host tailscale IP — ~300 GET /api/health
401 warn-events/day burying real perimeter signal (census: 339+20 of 5000
access.log lines). Dropping on UA alone would blind the store to external
curl scanners, so generic tool UAs (curl/) are now dropped ONLY when the
source remote_ip is one of this host's own addresses (DASHCADDY_SELF_IPS,
default loopback; start.sh derives 127.0.0.1 + tailscale ip -4, empty-safe).
remote_ip (TCP peer) is used, never the spoofable client_ip. DashCaddy-*
probe UAs stay unconditionally dropped. 9 regression pins cover the full
conjunction matrix incl. external-IP+curl KEPT and spoofed-XFF KEPT.
2858/2858 green (128 suites).
Judge: glm-4.6@zai-coding-paas cold-read round 1 = A clean (0 blocking),
both polish notes folded. URN urn:ump:s2sgitfepze65crtp57dpdw4gk4w7upsoi4tqcvfwahcsicyepba
- query().total: full-scan true count (was capped at offset+limit by an
early break — dashboard 24h stat read ≤1000 vs real ~46k)
- trim trigger/curer mismatch: byte trigger + line curer never converged
when avg line > ~524B (live avg 355B); now keeps maxDisk lines OR
≤80% byte budget, whichever retains fewer (TRIM_TARGET_FACTOR);
budget env-overridable via SECURITY_EVENT_TRIM_BYTES / opts.trimSizeLimit
- trim/append race: trim renamed over the file mid-append losing events
to the unlinked inode; now single-flight, queue-empty gated, holds the
write lock, error paths always release + lazily re-kick (no hot loop)
Judge: glm-4.6@zai-coding-paas adversarial cold-read, grade B clean
(0 blocking, 4 polish — 2 folded, 1 already-satisfied, 1 deferred as
judge-endorsed safer). 127 suites / 2849 tests green.
The caddy tail worker (DC-112) was 100% dead in prod: no /var/log/caddy
mount, no CADDY_ACCESS_LOG env, no global access log in the Caddyfile.
Store census 45,912 events, 100% source_type 'api', ZERO 'caddy'.
- createTail: firstStartMaxBytes (5 MiB) bounds first-ever-start replay
against a long-lived access.log; multi-chunk partial-line discard on
the jump; normal restarts resume at exact persisted offset (judge r1
fix-first fold)
- worker: self-noise filter drops our own probe UAs
(DashCaddy-Probe/1.0, DashCaddy-HealthCheck/1.0) from the derived
store — raw log keeps everything; ~50-100 events/min of probe noise
would otherwise bury perimeter signal in the 100k-cap store
- worker: metadata.host reads request.host (real caddy JSON nests it;
verified against live /var/log/caddy/seeds.log — the DC-112 read was
always null on live lines); top-level fallback kept
- worker: onAppear recovery log (DC-112 judge polish fold)
- start.sh: -v /var/log/caddy:/var/log/caddy:ro + CADDY_ACCESS_LOG env
- README: dead caddy-api/ dir refs -> dashcaddy-api/ (queue item e)
- tests: +12 DC-113 pins (hermetic, real worker + store); DC-112
fixtures corrected to the real nested request.host shape
126 suites / 2841 tests green. Judge: GLM-5.3 round-1 B, round-2 A
(SHIP). Verdict urn:ump:mccln523fptotuvrmddlqpf4zpkrxf273tg4kyytuyy3776rj3pq
Queue item (g) — same defect class as DC-111 defect 1, different writer:
the caddy access-log tail named every event http.<status>, so forward_auth
gate hits were unanswerable in the unified security event store.
- resolveCaddyAction() mirrors audit-logger ACTION_MAP vocabulary for BOTH
URI shapes (legacy /api/auth/gate/<id> from Caddyfile line 87, canonical
/api/v1/... from dashboard JS): auth.credential-injection,
auth.app-token-issue, auth.sso-exchange (exact-path match, boundary-tested)
- severity escalation now covers the legacy /api/auth/ prefix too
- metadata fidelity: caddy logs headers as ARRAYS — old single-value read
always produced user_agent:null; added metadata.host (vhost) and
duration_seconds (caddy logs seconds; duration_ms kept, no consumers)
- dead-path visibility: once-per-process warn when the access log is
missing. Live discovery: prod store has 45,912 events, 100%
source_type 'api', ZERO 'caddy' — the container has no /var/log/caddy
mount, so the worker silently no-ops. Infra wiring queued separately.
- new __tests__/caddy-worker-naming-dc112.test.js: 22 pins (real tail +
real store, hermetic sinks, both URI shapes, boundary rows, offset
persistence, once-warn). Full suite 125/2831 green.
Judge: GLM-5.3 cold read round-1 A (deleg_65498358, 3 polish items folded)
+ round-2 A (deleg_eed9dcbf, judge re-ran suite itself).
URN urn:ump:mjbwfcw6z7budx45s2rutovw5fatpp4jyzzspo7tq6nzajayulmq
1. audit-logger middleware computed action/resource from req.path INSIDE
the res.json override — after the /api/v1 router rebased req.url to the
router-relative path. resolveAction fell through ACTION_MAP for every
HTTP request, producing 45,899 'unknown.get' entries since 2026-07-14.
Fix: snapshot req.path/req.method at app-level (post-shim, pre-router).
2. DC-044 shim double-prefixed ALREADY-canonical /api/v1/auth/gate|x
into /api/v1/v1/... → 401 for canonical-URI clients. Fix: rewrite only
legacy /api/auth/* shapes; canonical pass through untouched.
3. event-store VALID_OUTCOMES lacked 'failure' → every failed API action's
security event was rejected+dropped from security-events.jsonl. Fix: add
'failure' to the vocabulary set.
8 regression pins over a faithful shim→audit→router mount mirror.
Suite: 124 suites / 2809 tests green.
Judge: GLM-5.3 cold read round-1 A/ship, URN urn:ump:ywv6rpmx55tlxbgc7ciyxqfcmx2v6q756cjbmif2d66k4bwwn6ya
audit-logger.js (StateManager write path into audit-log.json) starred only
6 sensitive keys at the middleware layer; email-bearing resource paths
(/invites/<email>/accept), DC-048 details.userEmail attribution, and emails
in non-sensitive body keys landed RAW — while the parallel unified-logger
path has masked at every sink since DC-095. DC-110 closes the parity gap
using the SAME canonical primitives (sa****@example.com): log() masks
resource (maskEmailsInString) and deep-masks details (maskEmails) at the
single write-point, covering middleware AND direct route calls. The
security-event mirror now uses the masked entry.resource for target/message
(judge round-1 fix-first: the raw parameter leaked emails into
security-events.jsonl). logging.js exports maskEmails (export-only).
maskEmails clones — caller details objects are never mutated.
Judge: GLM-5.3 cold read (standing Sami authorization 2026-08-17; Codex
quota dead until 2026-08-29). Round 1 (deleg_ebb83285) C fix-first —
caught the event-store mirror leak. Round 2 (deleg_923f9076) after
in-commit fix + mirror test: grade A, ship. Verdict URN
urn:ump:mwtxoj6dbdq7bfeba3l2am2rgx34zjimcjde5f6sxz6tozsjbskq (GET
readback verified: grade A, topic codex-judge-verdict). Deferred
(judge-accepted): one-time scrub of historical raw-email lines in the
live 16MB security-events.jsonl — queued follow-up.
Tests: 123 suites / 2801 green (+6 DC-110 pins: resource+details mask,
non-mutation, middleware e2e with *** survival, idempotence, no-email
regression, masked mirror target/message).
DC-095 masks emails at every live log sink; the rotated archive was the
remaining belt-and-braces gap — any future sink that forgets masking would
persist raw PII in error.log.1 for a full rotation cycle. appendErrorLog
now scrubs the freshly rotated archive with the SAME canonical mask
(sa****@example.com) via atomic rewrite (sibling .redact-<pid> temp, wx
open preserving mode else 0600, fsync, rename). Scrub failure is caught
and logged; the new error line is still appended. Stale crash-leftover
.redact-<pid> temps are swept best-effort on every rotation. Also
documents scripts/redact-log-pii.js usage in README (queue item e).
Judge: GLM-5.3 cold read (deleg_e7cd7f8c), round-1 grade B ship — both
nits addressed in-commit: stale-temp sweep (new 5th test pins it),
quoted-local-part mask edge deferred as pre-existing DC-095 primitive.
Tests: 122 suites / 2790 green (+5 DC-108 pins; was 2785 post-DC-107).
If the atomicWriteJSON of rotated credentials failed after rotateKey() had
already persisted+cached the new key, the on-disk key could no longer decrypt
the on-disk credentials.json (permanent loss on restart). Catch-path now
restores the old key via new cryptoUtils.restoreKey() (canonical atomic-write,
0600, hex-validated) while still holding the proper-lockfile. Double-failure
(rollback throws) is contained and the lock is still released. Hard-crash
mid-rollback is covered by the existing .bak startup fallback.
Judge: GLM-4.6 stand-in, round-1 grade A, 0 blocking, 1 LOW polish (folded).
URN urn:ump:z2sz3x6abtcffpqyde2l2ssq34vy47t5h2gbmkr3wtmfxwkerinq
Tests: 121 suites / 2785 green (6 consecutive runs pre-fold; suite re-run post-fold).
All three writeFileSync sites in the encrypted-credentials manager now
delegate to src/utils/atomic-write.js atomicWriteJSON — rotateEncryptionKey
save, _ensureFileExists bootstrap, and the _lockedUpdate commit under the
proper-lockfile lock. A crash can no longer tear credentials.json
mid-write: power loss through the old path could leave an empty/short
file and silently drop every stored credential (DNS provider tokens etc).
Lock-safety pre-study: proper-lockfile only stats its own sibling .lock
dir, never the target file, so the rename swap cannot trip ECOMPROMISED.
Test mock extended to the fd-level fs API (openSync/writeSync/
fsyncSync/closeSync/renameSync + fdMap/closedTmp state) so the canonical
path is exercised end-to-end under the mock; write assertions moved from
writeFileSync.mock.calls to destination-state reads. New DC-106 pins:
wx+0600+fsync+rename discipline, plaintext-secret canary never on disk,
fd-lifecycle order fsync->close->rename via invocationCallOrder, and
atomic _ensureFileExists create at 0600. Eighth store migrated
(DC-099..DC-105 preceded).
Judge: GLM-5.3 cold read, round-1 B/ship (deleg_b3c038c2).
URN urn:ump:vscequdet7wt5un7jhtl2nlg6cfkabsbazy5m2tjnyyguu5wstxa
(readback verified: grade B, topic codex-judge-verdict).
Sole finding is pre-existing and non-blocking: rotateEncryptionKey
persists the new key before writing rotated creds (crash window) —
queued as DC-107 follow-up.
Full suite: 121 suites / 2783 tests green.
_saveState drops its private fixed-name .tmp + writeFileSync (no fsync)
copy and delegates to src/utils/atomic-write.js atomicWriteJSON — same
wx/fsync/rename/dir-fsync discipline as the six previously migrated
stores (DC-099..DC-104). A crash can no longer tear
caddy-upstreams.json (mute list + probe state): power loss through the
old path could leave an empty/short state file and silently drop every
mute; concurrent saves (60s probe loop vs setMuted) collided on the
shared tmp name.
Test mock extended with the fd-level fs API (openSync/writeSync/
fsyncSync/closeSync/unlinkSync + closedTmp stash) so the canonical
path is exercised under the existing file-wide fs mock; fsState renamed
mockFsState (jest.mock out-of-scope-variable hoist rule). New DC-105
pin: wx+fsync+rename required, no fixed .tmp, zero leftover tmp files,
destination JSON complete with mute preserved.
Judge: GLM-5.3 cold read, round-1 A/ship (deleg_5f57fcce).
URN urn:ump:iuhgj3ajr5llshjasttsvelnptv6iqaek6xji5l3klcl72nseqdq
Full suite: 121 suites / 2779 tests green.
writeEvents() in scripts/stripe-license-bridge.js drops its private
tmp+writeFileSync+rename copy (no fsync, Date.now() tmp names) and
delegates to src/utils/atomic-write.js atomicWriteJSON (exclusive-create
tmp, fsync, rename, parent-dir fsync, 0600). stripe-events.json is the
Stripe webhook idempotency log - a torn write silently drops event-ids,
so a Stripe retry re-runs delivery (duplicate license email; combined
with a torn fulfillment record, a duplicate key mint). readEvents is
JSON.parse-only, so the canonical serializer's trailing-newline drop is
unobservable. Sole writer confirmed by grep. +2 DC-104 pins: full
recordEvent->eventSeen dedupe cycle (0600/complete JSON/zero-leftovers)
and 8-step back-to-back mutation parse-complete check. 121 suites /
2778 tests green.
Judge: GLM-5.3 round-1 A (deleg_e8e1f9d0), URN urn:ump:frwh34wtlsunsymok6pzyujyehq6ikpvadypmsg7ndk7jjvzrp5a
writeState() drops its private tmp+writeFileSync+rename copy (no fsync,
Date.now() tmp names, best-effort chmod) and delegates to
src/utils/atomic-write.js atomicWriteJSON (exclusive-create tmp, fsync,
rename, parent-dir fsync, 0600). A torn stripe-fulfillments.json could
previously make a webhook retry mint a SECOND valid license key for an
order that already has one. Both consumers (routes/billing.js,
scripts/stripe-license-bridge.js) JSON.parse only - trailing-newline
drop in the canonical serializer is harmless. Unused crypto require
removed. +2 DC-103 pins: full lifecycle 0600/complete/zero-leftovers,
and dual-instance interleaved writes (bridge+API file-IPC) with no tmp
collisions. 121 suites / 2776 tests green.
Judge: GLM-5.3 round-1 A (deleg_bd49a97b), URN urn:ump:layk7h326sqymcsh6tiusrs2sdbqdcx6ygvcuwwoxqn4rf7ycoua
Drops share-store's private _atomicWriteJSON copy (pid+Date.now() tmp
names, no fsync, no failure cleanup) in favor of src/utils/atomic-write.js:
fsync'd same-dir tmp+rename, exclusive-create 0600, parent-dir fsync,
cleanup-on-failure. Also fixes a latent bug in the same file: .share-secret
signing-key persistence used plain fs.writeFileSync — a torn write would
silently rotate the HMAC key on next boot, invalidating every outstanding
share signature (links 404, no error logged). Now atomicWriteFile.
Sole consumers parse JSON / trim(), so the dropped trailing newline on
shares.json is unobservable. +2 regression tests pin 0600 on both files,
complete content, zero tmp leftovers, and HMAC-still-verifies via getRaw.
Judge: GLM-5.3 cold read, round-1 A, deleg_50d5c239 (41s, 4 calls).
Verdict: urn:ump:ehblegyr5eko4hgyfylnrqk72hh3crmc43o2yt5ragfgvyc4zrdq
Full suite: 121 suites / 2774 tests green.