Conversation
Follow-up to #678 (datastore lock deadlock fix) and #675 (non-TTY interactive command fix). Addresses three issues identified in adversarial review of those PRs: 1. TOCTOU race in lock release: Added `forceRelease(expectedNonce)` to the `DistributedLock` interface so each backend (S3Lock, FileLock) can verify the nonce and delete in the tightest possible sequence. The `datastore lock release` command now delegates to this method instead of branching on config type with separate delete logic. 2. Double marker file read in `requireInitializedRepo`: The refactoring in #678 caused `requireInitializedRepo` to read the marker file twice — once in `resolveDatastoreForRepo` and again directly. `resolveDatastoreForRepo` now returns the marker it already read, and `requireInitializedRepo` reuses it. 3. `extension_search.ts` missed in #675: The PR updated 10 interactive search commands to use `interactiveOutputMode()` but missed `extension_search`. Applied the same pattern so it won't crash with "Raw mode is not supported" in non-TTY contexts. Co-authored-by: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Review: Approved
This PR addresses three review findings well with proper domain-driven design patterns.
No Blocking Issues
Analysis
Domain-Driven Design:
- The
forceRelease(expectedNonce)method is properly added to theDistributedLockdomain interface, making the contract explicit - Each infrastructure implementation (S3Lock, FileLock) encapsulates backend-specific atomicity details
- Good separation of concerns: CLI delegates to domain interface without knowing implementation details
Code Quality:
- Well-typed with proper interface usage
- Named exports used correctly
- All files have AGPLv3 headers
- No new
anytypes introduced
Test Coverage:
- 6 new tests for
forceReleasecovering: matching nonce, mismatched nonce, no lock exists - Test updated for
resolveDatastoreForRepoto verify marker is returned
Security:
- TOCTOU window is properly minimized by combining nonce verification and deletion in a single operation
- Method is appropriately documented as a "breakglass" operation with clear limitations
Suggestions (non-blocking)
- datastore_lock.ts:102 - The
info.nonce!non-null assertion could theoretically throw if a legacy lock exists without a nonce. While unlikely in practice (all modern locks include a nonce), consider handling this case:This is minor since all current lock implementations always include a nonce.if (!info.nonce) { renderDatastoreLockRelease( { released: false, reason: "lock has no nonce — cannot verify ownership" }, ctx.outputMode, ); return; } const released = await lock.forceRelease(info.nonce);
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found. The code changes are sound.
Medium
-
s3_lock.ts:168-175 — S3Lock.forceRelease doesn't handle S3 errors
Unlike
release()(lines 148-152), the newforceRelease()method doesn't wrap the S3 delete in try-catch:async forceRelease(expectedNonce: string): Promise<boolean> { const current = await this.readLock(); if (!current || current.nonce !== expectedNonce) { return false; } await this.s3.deleteObject(this.lockKey); // Can throw return true; }
Breaking example:
swamp datastore lock release --forceduring an S3 outage or with expired AWS credentials results in a raw AWS SDK exception (e.g.,CredentialsProviderError,NetworkError) rather than a user-friendly error message.Comparison:
release()explicitly catches and ignores errors because "if S3 is unreachable, the lock expires via TTL." For a breakglass operation, you might want errors to surface, but the current code gives poor UX when they do.Suggested fix: Either wrap in try-catch with a descriptive error, or document that raw errors are intentional for breakglass visibility.
Low
-
datastore_lock.ts:102 — Non-null assertion on optional field
info.nonce!whereLockInfo.nonceis typed asstring | undefined. If a legacy lock exists without a nonce (created before this feature),undefinedwould be passed toforceRelease(). The comparison works correctly at runtime (undefined !== undefined→ false), so no functional bug, but the!assertion obscures the type contract. -
Inconsistent error handling between FileLock and S3Lock forceRelease
FileLock (lines 176-180) catches
NotFoundand ignores it; S3Lock catches nothing. Minor inconsistency—S3 DeleteObject on a non-existent key succeeds silently anyway, but the pattern differs. -
TOCTOU window remains (documented)
The read-then-delete pattern in both
forceReleaseimplementations still has a theoretical race window where another process could acquire between verify and delete. This is explicitly documented in the interface and acceptable for a breakglass operation.
Verdict
PASS — No blocking issues. The TOCTOU improvement over the previous inline logic is meaningful, the double-read optimization is correct, and the extension_search non-TTY fix follows the established pattern. The S3 error handling inconsistency (Medium #1) is worth addressing but not a blocker for a breakglass operation that operators would investigate manually anyway.
Summary
Follow-up to #678 (datastore lock deadlock fix) and #675 (non-TTY interactive command fix). Addresses three issues identified in adversarial review of those PRs:
TOCTOU race in lock release (
datastore_lock.ts): AddedforceRelease(expectedNonce)to theDistributedLockinterface so each backend (S3Lock, FileLock) can verify the nonce and delete in the tightest possible sequence. Thedatastore lock releasecommand now delegates to this method instead of branching on config type with separate delete logic. This also removed the redundantS3Clientinstantiation andjoinimport from the command handler.Double marker file read in
requireInitializedRepo(repo_context.ts): The refactoring in fix: prevent datastore lock commands from deadlocking on stuck lock #678 causedrequireInitializedRepoto read the.swamp.yamlmarker file twice — once insideresolveDatastoreForRepoand again directly.resolveDatastoreForReponow returns the marker it already read, andrequireInitializedReporeuses it, eliminating the redundant I/O.extension_search.tsmissed in fix: gracefully handle non-TTY context in interactive commands #675: PR fix: gracefully handle non-TTY context in interactive commands #675 updated 10 interactive search commands to useinteractiveOutputMode()but missedextension_search. Applied the same pattern so it won't crash with "Raw mode is not supported" in non-TTY contexts (e.g.echo "" | swamp extension search aws).Test plan
forceRelease(3 per lock backend): matching nonce, mismatched nonce, no lock existsresolveDatastoreForRepotest to verify marker is returneddeno checkpassesdeno lintpassesdeno fmt --checkpasses🤖 Generated with Claude Code