chore(audit): weekly cloud audit — projection wiring gaps, branch-cleanup prefix bug, sibling-name leaks#88
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (theFileStore.OnAppendhook 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.DispatchWave—AGENT_SPAWNEDwas appended but never projected, and theProject()switch had no case for it. The only writer to theagentstable was a test-only helper, so the table was always empty in production. Real becauseListAgentsis read bynxd 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: addedprojectAgentSpawned+ aProject()case and project the event at dispatch.internal/state/sqlite.go·SQLiteStore.Project— added theAGENT_SPAWNEDcase (projectAgentSpawned, idempotent on replay) and corrected theInsertAgentdoc comment that wrongly claimed the event is never projected.internal/devdb/lifecycle.go·Lifecycle.Provision/Release/emitCreated/emitFailed—STORY_DB_CREATED/FAILED/DELETEDwere appended but never projected (theLifecycleonly held an appender), so thestory_databasestable stayed empty even though the projection switch handles those types. Real becauseweb/data.goreads it for the dashboard DB column/panel. Fix: added an optionalEventProjectortoLifecycle(WithProjector) and anemithelper; wireds.Projinresume.go.internal/devdb/lifecycle.go·Lifecycle.Release— theSTORY_DB_DELETED(and release-failureSTORY_DB_FAILED) events were emitted withoutStoryID, butprojectStoryDBDeletedkeys itsUPDATEonstory_id, so even once projected the delete matched zero rows and the row never leftstatus='created'. Fix: recover the story ID from the DB name viaParseStoryID.internal/engine/planner.go·Planner.plan—REQ_PLANNEDwas emitted with a rawAppend(unlike the adjacentREQ_SUBMITTED/STORY_CREATED, which useemitAndProject), so on the defaultnxd reqpath the requirement row stayed atsubmittedin the projection. Real becausenxd pauserejects requirements that "have not been planned yet" and the dashboard/web status buckets read the projected status. The--reviewpath masked it becausenxd approvere-emits and projectsREQ_PLANNED. Fix: switch toemitAndProject.Branch-cleanup bug + name leak
internal/engine/monitor_cleanup.go·danglingBranchesToClean— when a story'sBranchwas 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 privatevortex-dispatchrepo.internal/engine/{monitor,reviewer,sanitize_output}.go— stripped "VXD Phase 1.x" provenance tags and a~/Sites/misc/vortex-dispatch/path from source comments.vxdtest fixtures (repolearn,devdb,enginecheckpoint tests); kept the guard test inintegration_fix_test.gothat asserts prompts never emitvxd req.Tests added/updated
state/sqlite_test.go—TestSQLiteStore_ProjectAgentSpawned(+ idempotent replay).engine/dispatcher_test.go—TestDispatcher_ProjectsSpawnedAgents(dispatch populates the agents table).devdb/lifecycle_test.go— projectsSTORY_DB_CREATED/DELETED, recoversStoryID, and an end-to-end round-trip into a realSQLiteStore(created → deleted).engine/planner_test.go—TestPlanner_Plannow asserts the requirement projects toplanned.engine/monitor_cleanup_test.go— fallback now asserts thenxd/prefix.Verification
go build ./...— passgo vet ./...— passgo 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 blocksvuln.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