fix(api): redact P4 sync errors, restore merge on failure, close audit gaps - #58
Merged
Conversation
…t gaps Six defects found by a clean-code audit of the largest API modules, each verified against the surrounding code before being changed. Credential leak (Iron Rule 5). executeP4Sync returned p4d's raw stderr, which can echo the connection string back including P4PASSWD. Unlike checkP4Connection and the pre-sync verification catch in the same file, it never called sanitizeCredentials -- and the message is persisted to scmSources.lastSyncError and shipped to the sync-error webhook, so the password left the instance. Non-atomic writes. mergeMemoryTopics rewrote the merge target before archiving its sources with no rollback, so a mid-loop failure left the target holding a still-active source's facts and made the retry fail permanently (TOPIC_NOT_FOUND); splitMemoryTopic already compensates, and merge now does the same. importAgentFromZip wrote skill files after its transaction committed, so an ENOSPC left committed skills rows whose storagePath was empty -- the writes move inside the transaction so a failure rolls the rows back. Audit gaps. Both API-key regeneration routes, POST /scm-sources/:id/sync and POST /scm-sources/:id/check mutated state or made an outbound connection with a stored credential while writing no audit entry. The sync/rerun entries are now written after their status CAS wins, matching POST /runs/:id/execute, so a request rejected for a full queue leaves no entry claiming work that never ran. Resource leaks. A Feishu topic-root message carrying both images and a file overwrote its single rootTempDir, leaking the image download root; the three cleanup calls were also fire-and-forget, so a run reported terminal while its directories were still being torn down. Gates: typecheck green, lint 0 errors (749 warnings, unchanged), arch R1-R9 pass, pnpm test 8401 passed across shared/cli/web/api.
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.
What
Six defects surfaced by a clean-code audit of the largest API modules. Each was verified against the surrounding code before being changed — in every case a sibling code path in the same file already did the right thing, which is what makes these bugs rather than style debt.
Credential leak — Iron Rule 5
executeP4Syncreturned p4d's rawstderr, which echoes the connection string back on failure includingP4PASSWD.checkP4Connectionand the pre-sync verification catch in the same file both wrap throughsanitizeCredentials(which has a dedicatedP4PASSWDrule); this path did not. The message is persisted toscmSources.lastSyncErrorand shipped to the sync-error webhook, so the password left the instance entirely.Non-atomic writes
mergeMemoryTopicsrewrote the merge target before archiving its sources, with no rollback. A mid-loop failure left the target holding a still-active source's facts, and the retry then failed permanently withTOPIC_NOT_FOUND.splitMemoryTopicalready compensates; merge now does the same.importAgentFromZipwrote skill files after its transaction committed. AnENOSPCthere left committedskillsrows whosestoragePathwas empty, so later runs silently mounted an incomplete skill package. The writes move inside the transaction, so a failure rolls the rows back.Audit gaps — Iron Rule 5
POST /agents/:id/regenerate-api-key,POST /agents/:id/regenerate-a2a-api-key,POST /scm-sources/:id/syncandPOST /scm-sources/:id/checkeach mutated state or made an outbound connection with a stored credential while writing no audit entry./checkis the sharpest case: its sibling/probeaudits, rate-limits, and pins the endpoint precisely so a stored credential cannot be aimed anywhere.The sync and rerun entries are now written after their status CAS wins, matching
POST /runs/:id/execute, so a request rejected for a full queue no longer leaves an audit entry claiming work that never ran.Resource leaks
A Feishu topic-root message carrying both images and a file overwrote its single
rootTempDir, leaking the image download root (30 MB per attachment) until the disk filled. The three cleanup calls were also fire-and-forget, so a run reported terminal while its directories were still being torn down.Tests
TDD on the two behavioural fixes: a failing test first, then the fix.
result.messageEISDIRmid-archive and asserts the target and both sources are restoredGates
typecheck green · lint 0 errors (749 warnings, unchanged from
main) · arch R1–R9 pass · full suite 8401 passed, 0 failedNot included — two known P0s remain open
1. Per-Agent worktree bypasses the removal-reservation protocol (
agent-helpers.ts,resolvePerAgentWorkspace).task-queue-db.tsdefers the check with the comment "Runs without an explicit name are gated later, at resolveWorkDir" — that gate does not exist. The onlyfindPendingWorkspaceRemovalcall inagent-helpers.tsis on the explicit-worktree path. An admin workspace delete can therefore rungit worktree removewhile an incoming message resolves the same directory.I implemented the fix and reverted it: the added query desynchronised the
mockReturnValueOncequeues inagent-helpers-workdir.test.ts, taking it from 39/39 to 15 failures (confirmed against a clean baseline — the failures were mine, not pre-existing). Fixing it properly means reworking that file's mock sequencing, which deserves its own PR rather than riding along here.2. The two mutex keys for one directory are still inconsistent.
agent-helpers.tstakesworkspace:${wsPath}whilegit-workspace.tstakesscm-worktree:${join(wsRoot, name)}, so the locks do not exclude each other. They cannot simply be unified:withKeyedLockis not reentrant and bothcreateWorkspaceandremoveWorkspacetake the git-workspace key internally, so unifying deadlocks. This needs a lock-free internal variant.Both are filed here rather than silently dropped; neither is made worse by this PR.
Review notes
POST /agentsandPATCH /agents/:idresolvescmSourceIdwith no owner filter, whilekbDocumentsandskillGroupsthree lines away are owner-scoped. The hole is real, but exploiting it requires an authenticated colleague deliberately targeting another's resource — the exact caseiron-rules.mdsays not to harden against. Worth fixing as consistency with the adjacent validators; flagging for a maintainer call rather than deciding unilaterally.pnpm lintlocally reports thousands of errors for anyone who has run Playwright. They all come fromplaywright-report/, which is gitignored but not excluded inbiome.jsonfiles.includes. CI is unaffected (no artifact). Not fixed here to keep this PR scoped, but it is a one-line addition next to the existingtest-results/coverageentries.scripts/e2e/restart-recovery.sh) was not run. Reverting the worktree change removed the reason it mattered, but it is worth a pre-merge run.