diff --git a/.veritas-kanban/telemetry/events-2026-01-28.ndjson b/.veritas-kanban/telemetry/events-2026-01-28.ndjson index 384c90d5..2f6c53c6 100644 --- a/.veritas-kanban/telemetry/events-2026-01-28.ndjson +++ b/.veritas-kanban/telemetry/events-2026-01-28.ndjson @@ -1797,3 +1797,65 @@ {"type":"task.created","taskId":"task_20260128__MeCW1","project":"project-b","status":"todo","id":"evt_r4uHTWxguKEW","timestamp":"2026-01-28T18:08:08.502Z"} {"type":"task.created","taskId":"task_20260128_nmhW6e","status":"todo","id":"evt_rHh1Qe7ijCn0","timestamp":"2026-01-28T18:08:08.517Z"} {"type":"task.status_changed","taskId":"task_20260128_VxOHXP","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_bVKyHndjSs3o","timestamp":"2026-01-28T18:08:31.860Z"} +{"type":"task.status_changed","taskId":"task_20260126_legacy1","status":"blocked","previousStatus":"review","id":"evt_Qpb7BFExzGUE","timestamp":"2026-01-28T18:09:16.418Z"} +{"type":"task.created","taskId":"task_20260128_5Lu_em","status":"todo","id":"evt_b5ZtD3jzrTxb","timestamp":"2026-01-28T18:09:16.424Z"} +{"type":"task.created","taskId":"task_20260128_yfigJr","status":"todo","id":"evt_0AxRQpbsxYJl","timestamp":"2026-01-28T18:09:16.447Z"} +{"type":"task.status_changed","taskId":"task_20260128_yfigJr","status":"in-progress","previousStatus":"todo","id":"evt_yWHP7x7i2Dwm","timestamp":"2026-01-28T18:09:16.453Z"} +{"type":"task.created","taskId":"task_20260128_XEWZgR","status":"todo","id":"evt_urM1e3ZfuyXD","timestamp":"2026-01-28T18:09:16.463Z"} +{"type":"task.status_changed","taskId":"task_20260128_XEWZgR","status":"blocked","previousStatus":"todo","id":"evt_A_vF9k5-A62f","timestamp":"2026-01-28T18:09:16.471Z"} +{"type":"task.created","taskId":"task_20260128_tqx20I","status":"todo","id":"evt_LgzkV8lvDpIx","timestamp":"2026-01-28T18:09:16.472Z"} +{"type":"task.created","taskId":"task_20260128_W60YVG","project":"my-project","status":"todo","id":"evt_IyKYbz7fQAA1","timestamp":"2026-01-28T18:09:16.474Z"} +{"type":"task.status_changed","taskId":"task_20260128_tqx20I","status":"done","previousStatus":"todo","id":"evt_uX9nu3ku_Ok4","timestamp":"2026-01-28T18:09:16.475Z"} +{"type":"task.created","taskId":"task_20260128_88_N8c","status":"todo","id":"evt_JVBmwsgaSdhL","timestamp":"2026-01-28T18:09:16.478Z"} +{"type":"task.created","taskId":"task_20260128_JUdbHR","status":"todo","id":"evt_zCgD2QeDSY5J","timestamp":"2026-01-28T18:09:16.481Z"} +{"type":"task.created","taskId":"task_20260128_NrGG_n","status":"todo","id":"evt_bXxevOO_BB5R","timestamp":"2026-01-28T18:09:16.485Z"} +{"type":"task.status_changed","taskId":"task_20260126_idem1","status":"blocked","previousStatus":"review","id":"evt_FyBlwr_2cK5V","timestamp":"2026-01-28T18:09:16.498Z"} +{"type":"task.status_changed","taskId":"task_20260128_NrGG_n","status":"in-progress","previousStatus":"todo","id":"evt_pYq3ofWR0nPM","timestamp":"2026-01-28T18:09:16.500Z"} +{"type":"task.created","taskId":"task_20260128_iO98kh","status":"todo","id":"evt_ilRivf0t32CC","timestamp":"2026-01-28T18:09:16.507Z"} +{"type":"task.created","taskId":"task_20260128_6X8cvE","status":"todo","id":"evt_GbUvLJQ5n1aZ","timestamp":"2026-01-28T18:09:16.512Z"} +{"type":"task.status_changed","taskId":"task_20260126_multi1","status":"blocked","previousStatus":"review","id":"evt_L38zTSrXlZqp","timestamp":"2026-01-28T18:09:16.513Z"} +{"type":"task.status_changed","taskId":"task_20260126_multi2","status":"blocked","previousStatus":"review","id":"evt_7mDXkL1t_cms","timestamp":"2026-01-28T18:09:16.515Z"} +{"type":"task.created","taskId":"task_20260128_cuHDhS","status":"todo","id":"evt_Ya6COThGSYWF","timestamp":"2026-01-28T18:09:16.533Z"} +{"type":"task.archived","taskId":"task_20260128_cuHDhS","status":"todo","id":"evt_OQbkv1pwejU3","timestamp":"2026-01-28T18:09:16.536Z"} +{"type":"task.created","taskId":"task_20260128_45L6O_","status":"todo","id":"evt_JfOzI5PzANbp","timestamp":"2026-01-28T18:09:16.541Z"} +{"type":"task.restored","taskId":"task_20260126_arch1","status":"done","id":"evt_YdM_rAoRj_jm","timestamp":"2026-01-28T18:09:16.547Z"} +{"type":"task.status_changed","taskId":"task_20260126_arch1","status":"blocked","previousStatus":"done","id":"evt_jKCaFnAUw0mt","timestamp":"2026-01-28T18:09:16.549Z"} +{"type":"task.archived","taskId":"task_20260126_arch1","status":"blocked","id":"evt_b1usKTjGzDh4","timestamp":"2026-01-28T18:09:16.551Z"} +{"type":"task.created","taskId":"task_20260128_QY6xuP","status":"todo","id":"evt_5o1J5eAkaaHM","timestamp":"2026-01-28T18:09:16.551Z"} +{"type":"task.created","taskId":"task_20260128_cPLtZw","status":"todo","id":"evt_5G165e0OBAQw","timestamp":"2026-01-28T18:09:16.557Z"} +{"type":"task.created","taskId":"task_20260128_qbvgbD","status":"todo","id":"evt_62Ut-hvSQSZ2","timestamp":"2026-01-28T18:09:16.563Z"} +{"type":"task.created","taskId":"task_20260128_pOtThw","status":"todo","id":"evt_-RP2iOl7GS9A","timestamp":"2026-01-28T18:09:16.582Z"} +{"type":"task.created","taskId":"task_20260128_86qMBD","status":"todo","id":"evt_7SdZhXlgm3ca","timestamp":"2026-01-28T18:09:16.582Z"} +{"type":"task.created","taskId":"task_20260128_7tMVuL","status":"todo","id":"evt_h3kD_Cii2wyk","timestamp":"2026-01-28T18:09:16.592Z"} +{"type":"task.created","taskId":"task_20260128_yS5mXT","project":"test-project","status":"todo","id":"evt_hY3Fv60TXoSe","timestamp":"2026-01-28T18:09:16.594Z"} +{"type":"task.created","taskId":"task_20260128_1pz27_","status":"todo","id":"evt_QnxFQ5IGPxru","timestamp":"2026-01-28T18:09:16.596Z"} +{"type":"task.created","taskId":"task_20260128_ziFh-e","status":"todo","id":"evt_aCUfsqShIXuv","timestamp":"2026-01-28T18:09:16.602Z"} +{"type":"task.created","taskId":"task_20260128_ZHISuU","status":"todo","id":"evt_7bd4Vowa7Y5D","timestamp":"2026-01-28T18:09:16.618Z"} +{"type":"task.created","taskId":"task_20260128_9jikAS","status":"todo","id":"evt_s2PNEryux3n1","timestamp":"2026-01-28T18:09:16.619Z"} +{"type":"task.status_changed","taskId":"task_20260128_9jikAS","status":"in-progress","previousStatus":"todo","id":"evt_DzM8tqhHPY0d","timestamp":"2026-01-28T18:09:16.623Z"} +{"type":"task.created","taskId":"task_20260128__NAubi","status":"todo","id":"evt_KDlaF1bc4S-x","timestamp":"2026-01-28T18:09:16.629Z"} +{"type":"task.created","taskId":"task_20260128_gJuMBJ","status":"todo","id":"evt_xL-ssjcnoRpE","timestamp":"2026-01-28T18:09:16.629Z"} +{"type":"task.created","taskId":"task_20260128_0kgh5Y","status":"todo","id":"evt_aLqX9rTEP_zZ","timestamp":"2026-01-28T18:09:16.634Z"} +{"type":"task.created","taskId":"task_20260128_FXaQlh","status":"todo","id":"evt_vvsL_omYiBP-","timestamp":"2026-01-28T18:09:16.638Z"} +{"type":"task.created","taskId":"task_20260128_gOcY8H","status":"todo","id":"evt_LdvUl5W7qClP","timestamp":"2026-01-28T18:09:16.640Z"} +{"type":"task.created","taskId":"task_20260128_epYmqU","status":"todo","id":"evt_53tVHgyjJdhE","timestamp":"2026-01-28T18:09:16.645Z"} +{"type":"task.created","taskId":"task_20260128_yMhYrx","status":"todo","id":"evt_k1k10cVLyJzQ","timestamp":"2026-01-28T18:09:16.646Z"} +{"type":"task.created","taskId":"task_20260128_RT0jmZ","status":"todo","id":"evt_8KvtsOqyKSex","timestamp":"2026-01-28T18:09:16.653Z"} +{"type":"task.created","taskId":"task_20260128_G-_yl2","status":"todo","id":"evt_9H8Wg7f4mItL","timestamp":"2026-01-28T18:09:16.664Z"} +{"type":"task.created","taskId":"task_20260128_J09NBV","status":"todo","id":"evt_TbdT5oXdrOpO","timestamp":"2026-01-28T18:09:16.670Z"} +{"type":"task.created","taskId":"task_20260128_uucDAU","status":"todo","id":"evt_5fV-MpbHpbRC","timestamp":"2026-01-28T18:09:16.670Z"} +{"type":"task.created","taskId":"task_20260128_Xovvy1","status":"todo","id":"evt_yScb7A50qYWg","timestamp":"2026-01-28T18:09:16.678Z"} +{"type":"task.created","taskId":"task_20260128_INQ8yt","status":"todo","id":"evt_RkSaYk3sVSgf","timestamp":"2026-01-28T18:09:16.678Z"} +{"type":"task.created","taskId":"task_20260128_43H0qx","status":"todo","id":"evt_61KDkfVN89p4","timestamp":"2026-01-28T18:09:16.679Z"} +{"type":"task.created","taskId":"task_20260128_mMTk-8","status":"todo","id":"evt_3biIDHZNwm6w","timestamp":"2026-01-28T18:09:16.680Z"} +{"type":"task.created","taskId":"task_20260128_snGFll","status":"todo","id":"evt_kDmyyl36tcCT","timestamp":"2026-01-28T18:09:16.680Z"} +{"type":"task.created","taskId":"task_20260128_PI4zJ5","status":"todo","id":"evt_4eaZROeNyKQG","timestamp":"2026-01-28T18:09:16.690Z"} +{"type":"task.created","taskId":"task_20260128_BKI9Fk","status":"todo","id":"evt_iqrXhaHadKIw","timestamp":"2026-01-28T18:09:16.698Z"} +{"type":"task.created","taskId":"task_20260128_qsWjxE","status":"todo","id":"evt_ATrT-kaid79A","timestamp":"2026-01-28T18:09:16.704Z"} +{"type":"task.created","taskId":"task_20260128_JYjisO","status":"todo","id":"evt_vpepi3GtQfb7","timestamp":"2026-01-28T18:09:16.708Z"} +{"type":"task.created","taskId":"task_20260128_evvJc1","status":"todo","id":"evt_-xgCP8O0xm31","timestamp":"2026-01-28T18:09:16.720Z"} +{"type":"task.created","taskId":"task_20260128_cNpeKv","project":"project-a","status":"todo","id":"evt_2L58jPTJb2Mf","timestamp":"2026-01-28T18:09:16.729Z"} +{"type":"task.created","taskId":"task_20260128_B5K54H","project":"project-a","status":"todo","id":"evt_1YjG_klICRb6","timestamp":"2026-01-28T18:09:16.729Z"} +{"type":"task.created","taskId":"task_20260128_eDAP99","status":"todo","id":"evt_cwMs41pu6DbA","timestamp":"2026-01-28T18:09:16.729Z"} +{"type":"task.created","taskId":"task_20260128_kAmcUg","project":"project-b","status":"todo","id":"evt_UUJ844HuX3xQ","timestamp":"2026-01-28T18:09:16.730Z"} +{"type":"task.created","taskId":"task_20260128_Y3HGTt","status":"todo","id":"evt_7Nw9EKlZ8vu3","timestamp":"2026-01-28T18:09:16.744Z"} diff --git a/server/src/__tests__/jwt-rotation.test.ts b/server/src/__tests__/jwt-rotation.test.ts index dcd2e90b..d4acf57f 100644 --- a/server/src/__tests__/jwt-rotation.test.ts +++ b/server/src/__tests__/jwt-rotation.test.ts @@ -195,7 +195,7 @@ describe('JWT Secret Rotation', () => { expect(config.jwtSecrets!.find(s => s.version === 1)).toBeUndefined(); }); - it('should support custom grace period', () => { + it('should support custom grace period (short)', () => { const secret1 = crypto.randomBytes(32).toString('hex'); setConfig({ jwtSecrets: [ @@ -204,16 +204,39 @@ describe('JWT Secret Rotation', () => { jwtSecretVersion: 1, }); - // 0ms grace period - const result = rotateJwtSecret(0); + // 1 hour grace period + const oneHourMs = 60 * 60 * 1000; + const result = rotateJwtSecret(oneHourMs); expect(result.success).toBe(true); const config = getSecurityConfig(); const oldEntry = config.jwtSecrets!.find(s => s.version === 1); + expect(oldEntry).toBeDefined(); expect(oldEntry!.expiresAt).toBeDefined(); - // With 0 grace, the expiresAt should be approximately now + // expiresAt should be approximately 1 hour from now const expiresAt = new Date(oldEntry!.expiresAt!).getTime(); - expect(Math.abs(expiresAt - Date.now())).toBeLessThan(5000); // within 5 seconds + const expectedExpiry = Date.now() + oneHourMs; + expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(5000); // within 5 seconds + }); + + it('should immediately prune with 0ms grace period', () => { + const secret1 = crypto.randomBytes(32).toString('hex'); + setConfig({ + jwtSecrets: [ + { secret: secret1, version: 1, createdAt: '2026-01-01T00:00:00Z' }, + ], + jwtSecretVersion: 1, + }); + + // 0ms grace period — old secret should be immediately pruned + const result = rotateJwtSecret(0); + expect(result.success).toBe(true); + expect(result.prunedCount).toBe(1); + + const config = getSecurityConfig(); + // Only the new current secret should remain + expect(config.jwtSecrets).toHaveLength(1); + expect(config.jwtSecrets![0].version).toBe(2); }); }); diff --git a/web/src/components/shared/AgentStatusIndicator.tsx b/web/src/components/shared/AgentStatusIndicator.tsx index f591fb71..1cd42b8b 100644 --- a/web/src/components/shared/AgentStatusIndicator.tsx +++ b/web/src/components/shared/AgentStatusIndicator.tsx @@ -229,9 +229,10 @@ export function AgentStatusIndicator({ className = '' }: AgentStatusIndicatorPro if (error) return 'error'; if (!data) return 'idle'; if (data.status === 'error') return 'error'; - if (data.subAgentCount > 0 || data.status === 'sub-agent') return 'subagents'; - if (data.status === 'sub-agent') return 'subagents'; - return data.status as AgentState; + if (data.subAgentCount > 0 || data.status === ('sub-agent' as string)) return 'subagents'; + const s = data.status as string; + if (s === 'idle' || s === 'working' || s === 'thinking' || s === 'error') return s as AgentState; + return 'idle'; }, [data, error]); const config = STATE_CONFIG[state]; diff --git a/web/src/hooks/useAgentStatus.ts b/web/src/hooks/useAgentStatus.ts index 197c40f4..b8723ae1 100644 --- a/web/src/hooks/useAgentStatus.ts +++ b/web/src/hooks/useAgentStatus.ts @@ -11,7 +11,7 @@ const STALE_THRESHOLD_MS = 5 * 60 * 1000; /** How often to check for staleness (ms) */ const STALE_CHECK_INTERVAL_MS = 30_000; -export type AgentStatusState = 'idle' | 'working' | 'thinking' | 'error'; +export type AgentStatusState = 'idle' | 'working' | 'thinking' | 'sub-agent' | 'error'; export interface SubAgent { id: string;