diff --git a/tools/floor-decide.mjs b/tools/floor-decide.mjs index 88862afa..e8054297 100644 --- a/tools/floor-decide.mjs +++ b/tools/floor-decide.mjs @@ -76,7 +76,10 @@ export function approvalOpen(command, approvedGitOps, now) { const target = normCmd(command); if (!target) return false; return approvedGitOps.some( - (op) => op && normCmd(op.command) === target && typeof op.ts === 'number' && now - op.ts <= APPROVE_WINDOW_MS, + // F-6: свежесть = ts в прошлом И в пределах окна. Нижняя граница (now - ts >= 0) отсекает + // одобрение с будущим ts (часовой сдвиг/подлог), которое иначе открывало бы дверь. + (op) => op && normCmd(op.command) === target && typeof op.ts === 'number' + && now - op.ts >= 0 && now - op.ts <= APPROVE_WINDOW_MS, ); } diff --git a/tools/floor-decide.test.mjs b/tools/floor-decide.test.mjs index 141e6019..224f8d84 100644 --- a/tools/floor-decide.test.mjs +++ b/tools/floor-decide.test.mjs @@ -99,6 +99,14 @@ describe('floorDecide — дверь владельца Δ1 (read-only approval, const r = floorDecide({ toolUse: bash('php artisan migrate:fresh'), approvedGitOps: [], now, normalizeImpl: id }); expect(r.block).toBe(true); }); + // F-6 (аудит 2026-06-07): floor-decide.mjs approvalOpen без нижней границы времени — + // одобрение с будущим ts (now - ts < 0 ≤ window) открывало дверь. Свежесть требует ts в прошлом. + it('одобрение с будущим ts (часовой сдвиг/подлог) → block (нижняя граница времени)', () => { + const cmd = 'php artisan migrate:fresh'; + const approvedGitOps = [{ command: cmd, ts: now + 60 * 1000 }]; + const r = floorDecide({ toolUse: bash(cmd), approvedGitOps, now, normalizeImpl: id }); + expect(r.block).toBe(true); + }); }); describe('floorDecide — observe-only / прочее не блокируется', () => {