Skip to content

chore(audit): weekly cloud audit — projection wiring gaps, branch-cleanup prefix bug, sibling-name leaks#88

Merged
tzone85 merged 2 commits into
mainfrom
fix/audit-cloud-2026-06-29
Jun 29, 2026
Merged

chore(audit): weekly cloud audit — projection wiring gaps, branch-cleanup prefix bug, sibling-name leaks#88
tzone85 merged 2 commits into
mainfrom
fix/audit-cloud-2026-06-29

Conversation

@tzone85

@tzone85 tzone85 commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Weekly automated security/quality audit. All findings below were adversarially verified before fixing; each has a regression test. The audit swept concurrency, silent failures, event-sourcing projection wiring, and security/naming — only the items below were confirmed real (concurrency, silent-failure, and injection/path/SQL/secret checks came back clean).

Findings & fixes

Event-sourcing projection wiring gaps

There is no global auto-projection — every emit site must call ProjectionStore.Project() itself (the FileStore.OnAppend hook only feeds the web event bus). Several state-transition events were appended but never projected, silently desyncing the SQLite read model.

  • internal/engine/dispatcher.go · Dispatcher.DispatchWaveAGENT_SPAWNED was appended but never projected, and the Project() switch had no case for it. The only writer to the agents table was a test-only helper, so the table was always empty in production. Real because ListAgents is read by nxd agents, the dashboard agents panel (web/data.go, web/handlers.go), the TUI (dashboard/app.go), and crash recovery's session→story map (engine/recovery.go). Fix: added projectAgentSpawned + a Project() case and project the event at dispatch.

  • internal/state/sqlite.go · SQLiteStore.Project — added the AGENT_SPAWNED case (projectAgentSpawned, idempotent on replay) and corrected the InsertAgent doc comment that wrongly claimed the event is never projected.

  • internal/devdb/lifecycle.go · Lifecycle.Provision/Release/emitCreated/emitFailedSTORY_DB_CREATED/FAILED/DELETED were appended but never projected (the Lifecycle only held an appender), so the story_databases table stayed empty even though the projection switch handles those types. Real because web/data.go reads it for the dashboard DB column/panel. Fix: added an optional EventProjector to Lifecycle (WithProjector) and an emit helper; wired s.Proj in resume.go.

  • internal/devdb/lifecycle.go · Lifecycle.Release — the STORY_DB_DELETED (and release-failure STORY_DB_FAILED) events were emitted without StoryID, but projectStoryDBDeleted keys its UPDATE on story_id, so even once projected the delete matched zero rows and the row never left status='created'. Fix: recover the story ID from the DB name via ParseStoryID.

  • internal/engine/planner.go · Planner.planREQ_PLANNED was emitted with a raw Append (unlike the adjacent REQ_SUBMITTED/STORY_CREATED, which use emitAndProject), so on the default nxd req path the requirement row stayed at submitted in the projection. Real because nxd pause rejects requirements that "have not been planned yet" and the dashboard/web status buckets read the projected status. The --review path masked it because nxd approve re-emits and projects REQ_PLANNED. Fix: switch to emitAndProject.

Branch-cleanup bug + name leak

  • internal/engine/monitor_cleanup.go · danglingBranchesToClean — when a story's Branch was empty, the cleanup fallback built the branch name as "vxd/" + s.ID, but the dispatcher creates branches as "nxd/<id>" (Dispatcher.DispatchWave). Cleanup therefore targeted a branch that never existed and silently left the real dangling branch (and its open PR) behind. This was simultaneously a leak of the private sibling project's name. Fix: use "nxd/".

Sibling-project name leaks (offline NXD must not reference its private sibling)

Scrubbed every shipped reference (CLAUDE.md / docs/history dev-docs correctly retain the sanctioned sibling description):

  • internal/dashboard/app.go — TUI status bar "Made with ♥ by Vortex Dispatch" → "Nexus Dispatch".
  • internal/web/static/index.html — web footer label "Vortex Dispatch" → "Nexus Dispatch".
  • docs/demo.tape — demo line "Same pipeline as VXD…" reworded (renders into the demo GIF).
  • README.md — removed the attribution line linking the private vortex-dispatch repo.
  • internal/engine/{monitor,reviewer,sanitize_output}.go — stripped "VXD Phase 1.x" provenance tags and a ~/Sites/misc/vortex-dispatch/ path from source comments.
  • Neutralized coincidental vxd test fixtures (repolearn, devdb, engine checkpoint tests); kept the guard test in integration_fix_test.go that asserts prompts never emit vxd req.

Tests added/updated

  • state/sqlite_test.goTestSQLiteStore_ProjectAgentSpawned (+ idempotent replay).
  • engine/dispatcher_test.goTestDispatcher_ProjectsSpawnedAgents (dispatch populates the agents table).
  • devdb/lifecycle_test.go — projects STORY_DB_CREATED/DELETED, recovers StoryID, and an end-to-end round-trip into a real SQLiteStore (created → deleted).
  • engine/planner_test.goTestPlanner_Plan now asserts the requirement projects to planned.
  • engine/monitor_cleanup_test.go — fallback now asserts the nxd/ prefix.

Verification

  • go build ./... — pass
  • go vet ./... — pass
  • go test ./... -count=1 — pass (full suite)
  • golangci-lint run ./... — pass (0 issues; golangci-lint v2.12.2 rebuilt against go1.26.4)
  • govulncheck ./...could not run: the environment network policy blocks vuln.go.dev (gateway returns 403 to CONNECT), so the vulnerability DB cannot be fetched. No dependency changes were made in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PF5GHhwcLj1Q8DtrddZcLw


Generated by Claude Code

claude added 2 commits June 29, 2026 05:22
…prefix, scrub sibling-project name leaks

Event-sourcing wiring gaps (projection silently desynced):
- AGENT_SPAWNED was appended by the dispatcher but never projected and had
  no case in the SQLite Project() switch, so the agents table stayed empty in
  production. `nxd agents`, the dashboard agents panel, and crash recovery's
  session->story map all read an empty table. Add projectAgentSpawned + a
  Project() case and project the event at the dispatch site.
- STORY_DB_CREATED/FAILED/DELETED were appended by devdb.Lifecycle but never
  projected (no global auto-projection exists), so the story_databases table
  stayed empty and the dashboard DB column/panel never populated. Add an
  optional EventProjector to Lifecycle and wire s.Proj in resume.go.
- STORY_DB_DELETED was emitted without StoryID; projectStoryDBDeleted keys its
  UPDATE on story_id, so even once projected the delete matched zero rows.
  Recover the story ID from the DB name via ParseStoryID.

Branch-cleanup bug + name leak:
- danglingBranchesToClean's empty-Branch fallback used a "vxd/" prefix; the
  dispatcher creates branches as "nxd/<id>", so cleanup targeted a branch that
  never existed and left the real dangling branch behind. Use "nxd/".

Sibling-project name leaks (offline NXD must not reference its private sibling):
- Scrub user-facing strings (TUI status bar, web footer, demo.tape, README
  attribution) and source-comment provenance tags. Neutralize coincidental
  "vxd" test fixtures; keep the guard test that asserts prompts never leak it.

Tests: projectAgentSpawned + idempotent replay; dispatcher projects the agent;
devdb projects created/deleted and recovers StoryID, plus an end-to-end
round-trip into a real SQLiteStore; branch-cleanup fallback asserts "nxd/".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PF5GHhwcLj1Q8DtrddZcLw
…quirement

Planner.plan emitted REQ_PLANNED with a raw eventStore.Append (unlike the
adjacent REQ_SUBMITTED and STORY_CREATED, which use emitAndProject). There is
no global auto-projection, so on the default `nxd req` path the requirement row
stayed at "submitted" in the SQLite projection even though planning completed.
That broke `nxd pause` (it rejects requirements that "have not been planned
yet") and showed a stale status in the dashboard/web status buckets. The
--review path happened to recover because `nxd approve` re-emits and projects
REQ_PLANNED. Switch the emit to emitAndProject.

Test: TestPlanner_Plan now asserts the requirement projects to "planned".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PF5GHhwcLj1Q8DtrddZcLw
@tzone85 tzone85 merged commit 832f750 into main Jun 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants