fix(gsd): block direct writes to gsd.db via hooks to prevent corruption (#3674)
* fix(gsd): block direct writes to gsd.db via hooks to prevent corruption When gsd_complete_task tool was unavailable, agents fell back to shell- based sqlite3/sql.js writes to .gsd/gsd.db, corrupting the WAL-backed database. Extend write-intercept to block: - File writes to gsd.db, gsd.db-wal, gsd.db-shm - Bash commands using sqlite3/sql.js/better-sqlite3 targeting gsd.db - Shell redirects/cp/mv targeting gsd.db Closes #3625 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add regression test for blocking direct gsd.db writes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67d68e2684
commit
05edc2f484
2 changed files with 73 additions and 1 deletions
63
src/resources/extensions/gsd/tests/block-db-writes.test.ts
Normal file
63
src/resources/extensions/gsd/tests/block-db-writes.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Regression test for #3674 — block direct writes to gsd.db
|
||||
*
|
||||
* When gsd_complete_task was unavailable, agents fell back to shell-based
|
||||
* sqlite3 writes, corrupting the WAL-backed database. The fix extends
|
||||
* write-intercept to block file writes and bash commands targeting gsd.db.
|
||||
*/
|
||||
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isBlockedStateFile, isBashWriteToStateFile } from '../write-intercept.ts';
|
||||
|
||||
describe('isBlockedStateFile blocks gsd.db paths (#3674)', () => {
|
||||
test('blocks .gsd/gsd.db', () => {
|
||||
assert.ok(isBlockedStateFile('/project/.gsd/gsd.db'));
|
||||
});
|
||||
|
||||
test('blocks .gsd/gsd.db-wal', () => {
|
||||
assert.ok(isBlockedStateFile('/project/.gsd/gsd.db-wal'));
|
||||
});
|
||||
|
||||
test('blocks .gsd/gsd.db-shm', () => {
|
||||
assert.ok(isBlockedStateFile('/project/.gsd/gsd.db-shm'));
|
||||
});
|
||||
|
||||
test('blocks resolved symlink path under .gsd/projects/', () => {
|
||||
assert.ok(isBlockedStateFile('/home/user/.gsd/projects/myproj/gsd.db'));
|
||||
});
|
||||
|
||||
test('still blocks STATE.md', () => {
|
||||
assert.ok(isBlockedStateFile('/project/.gsd/STATE.md'));
|
||||
});
|
||||
|
||||
test('does not block other .gsd files', () => {
|
||||
assert.ok(!isBlockedStateFile('/project/.gsd/DECISIONS.md'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBashWriteToStateFile blocks DB shell commands (#3674)', () => {
|
||||
test('blocks sqlite3 targeting gsd.db', () => {
|
||||
assert.ok(isBashWriteToStateFile('sqlite3 .gsd/gsd.db "INSERT INTO ..."'));
|
||||
});
|
||||
|
||||
test('blocks better-sqlite3 targeting gsd.db', () => {
|
||||
assert.ok(isBashWriteToStateFile('node -e "require(\'better-sqlite3\')(\'.gsd/gsd.db\')"'));
|
||||
});
|
||||
|
||||
test('blocks shell redirect to gsd.db', () => {
|
||||
assert.ok(isBashWriteToStateFile('echo data > .gsd/gsd.db'));
|
||||
});
|
||||
|
||||
test('blocks cp to gsd.db', () => {
|
||||
assert.ok(isBashWriteToStateFile('cp backup.db .gsd/gsd.db'));
|
||||
});
|
||||
|
||||
test('blocks mv to gsd.db', () => {
|
||||
assert.ok(isBashWriteToStateFile('mv temp.db .gsd/gsd.db'));
|
||||
});
|
||||
|
||||
test('does not block reading gsd.db with cat', () => {
|
||||
assert.ok(!isBashWriteToStateFile('cat .gsd/gsd.db'));
|
||||
});
|
||||
});
|
||||
|
|
@ -24,6 +24,9 @@ const BLOCKED_PATTERNS: RegExp[] = [
|
|||
/(^|[/\\])\.gsd[/\\]STATE\.md$/i,
|
||||
// Also match resolved symlink paths under ~/.gsd/projects/ (Pitfall #6)
|
||||
/(^|[/\\])\.gsd[/\\]projects[/\\][^/\\]+[/\\]STATE\.md$/i,
|
||||
// gsd.db and WAL/SHM files — single-writer WAL connection managed by engine (#3625)
|
||||
/(^|[/\\])\.gsd[/\\]gsd\.db(-wal|-shm)?$/i,
|
||||
/(^|[/\\])\.gsd[/\\]projects[/\\][^/\\]+[/\\]gsd\.db(-wal|-shm)?$/i,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -41,6 +44,12 @@ const BASH_STATE_PATTERNS: RegExp[] = [
|
|||
/\bsed\b.*-i.*STATE\.md/i,
|
||||
// dd output to STATE.md
|
||||
/\bdd\b.*of=\S*STATE\.md/i,
|
||||
// Direct DB access via sqlite3/sql.js/better-sqlite3 targeting gsd.db (#3625)
|
||||
/\b(sqlite3|sql\.js|better-sqlite3|node:sqlite)\b.*gsd\.db/i,
|
||||
/\bgsd\.db\b.*\b(sqlite3|sql\.js|better-sqlite3)\b/i,
|
||||
// Shell writes targeting gsd.db files
|
||||
/[>|]+\s*\S*gsd\.db/i,
|
||||
/\b(cp|mv|dd)\b.*gsd\.db/i,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -81,7 +90,7 @@ function matchesBlockedPattern(path: string): boolean {
|
|||
* Error message returned when an agent attempts to directly write an authoritative .gsd/ state file.
|
||||
* Directs the agent to use engine tool calls instead.
|
||||
*/
|
||||
export const BLOCKED_WRITE_ERROR = `Direct writes to .gsd/STATE.md are blocked. Use engine tool calls instead:
|
||||
export const BLOCKED_WRITE_ERROR = `Direct writes to .gsd/STATE.md and .gsd/gsd.db are blocked. Use engine tool calls instead:
|
||||
- To complete a task: call gsd_complete_task(milestone_id, slice_id, task_id, summary)
|
||||
- To complete a slice: call gsd_complete_slice(milestone_id, slice_id, summary, uat_result)
|
||||
- To save a decision: call gsd_save_decision(scope, decision, choice, rationale)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue