fix(web): version chat window cache and cover cross-user session list
Drop window-cache payloads that lack a schema field. Prove a non-owner listing another user's sessions is forbidden. Correct the stale test schema note in CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
# 当前焦点
|
||||
|
||||
**更新**: 2026-09-19
|
||||
**production_ready**: yes(门禁 B1–B4 已签;dev schema 52;test `schema_version=48`;main `schema_version=52`、`env=main`。不把 fallback change report 写成完整 PI)
|
||||
**production_ready**: yes(门禁 B1–B4 已签;dev/test/main schema 52。不把 fallback change report 写成完整 PI)
|
||||
|
||||
## 焦点
|
||||
|
||||
|
||||
@@ -41,9 +41,20 @@ updated: 2026-09-19
|
||||
| L1 | done。健康检查 2 秒缓存 + 2 秒 DB 超时 |
|
||||
| L2 | done。SSE marshal 失败写 `event: error` 帧 |
|
||||
| L3 | done。`ReplayPlan.Digest` 含 `DryRun`;dry-run 摘要不能授权 apply |
|
||||
| M15 | done。生产 `claimSQL` 与 `MemoryBus` 共用 `VersionClaimable`:版本 0 可领,版本化事件不能跳过 checkpoint。checkpoint 只前进。`TestVersionClaimableMatchesMemoryBusGap`、`TestGapDoesNotSkipVersion` |
|
||||
| H3 负例 | done。`TestListChatSessions_CrossUserIs403` |
|
||||
| FE 缓存 | done(版本字段)。窗口缓存写入 `{schema,data}`,无 schema 的旧载荷丢弃。`window cache: 无 schema` |
|
||||
| FE lucide | 关闭理由:`package-lock.json` 锁定 `lucide-react@1.42.0`,来源 `registry.npmjs.org`,带 integrity。`^1.16.0` 是合法的 1.x 范围,不是来源不明的包 |
|
||||
| FE App.tsx | 仍开放。4600 行拆 hook 是独立重构,本轮未拆,避免在没有行为变更需求时改写工作台 |
|
||||
| H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 |
|
||||
|
||||
仍开放:M7、M15、FE,以及能力矩阵的 rdev 剩余、SWBOOT-10、FAV live、D7、首次加载 U1/U3–U7、晋升前置(rebuild / PG / 前端构建 / 403 负例)。SWBOOT-10、FAV live、D7 受红线约束,不能靠直接打开开关算完成。
|
||||
仍开放:M7 修订三阶段事务、FE `App.tsx` 拆分、rdev S2/S3 与 P2–P4、首次加载 U1/U3–U7、晋升前置(dev rebuild、治理 PG、`npm test`/`npm run build`)。跨用户会话列表 403 已有 `TestListChatSessions_CrossUserIs403`。`CURRENT.md` 文首 test schema 已改为 52。
|
||||
|
||||
红线关闭(不是实现):
|
||||
|
||||
- SWBOOT-10:保持 fail-closed stub。未获独立授权前不接真实仓库写入。
|
||||
- FAV live:禁止整支 merge;`FAV_DEGRADED_READ=false`;live 需要各阶段至少 7 天观察窗,本轮不放量。
|
||||
- D7:合规签字前禁止按留存口径物理删除。盘点已完成,口径仍 `pending-compliance-signoff`。
|
||||
|
||||
## 清单
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -114,3 +115,22 @@ func TestChatSessionOwnership_CrossUserHijackDenied_Memory(t *testing.T) {
|
||||
t.Fatalf("victim continued access broken: %d %s", rec5.Code, rec5.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChatSessions_CrossUserIs403(t *testing.T) {
|
||||
cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
handler := server.NewServer(cfg, svc).Routes()
|
||||
adminToken := loginTestAdmin(t, handler, cfg)
|
||||
suffix := time.Now().Format("150405.000")
|
||||
victimToken := createAndLoginUser(t, handler, adminToken, "list_victim_"+suffix, "pass12345")
|
||||
attackerToken := createAndLoginUser(t, handler, adminToken, "list_attacker_"+suffix, "pass12345")
|
||||
victimID := fetchAuthUserID(t, handler, victimToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/chat/sessions?user_id="+strconv.FormatInt(victimID, 10), nil)
|
||||
withBearerAuth(req, attackerToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("cross-user session list expected 403, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -13,6 +13,17 @@ import { enrichMessagesWithCategories } from '../engine/categoryHelper';
|
||||
// v6:修正 NeuroPulse 驳回追问误挂「疲劳寿命」旧答复;对话 LLM 每轮独立工作区
|
||||
const STORAGE_PREFIX = 'smdf_agent_workbench_v6_';
|
||||
|
||||
// CACHE_SCHEMA 是窗口缓存 JSON 的载荷版本。缺字段或版本不符时丢弃,避免旧结构被当成有效缓存。
|
||||
export const CACHE_SCHEMA = 1;
|
||||
|
||||
type VersionedCache<T> = { schema: number; data: T };
|
||||
|
||||
function isVersionedCache<T>(value: unknown): value is VersionedCache<T> {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const row = value as { schema?: unknown; data?: unknown };
|
||||
return typeof row.schema === 'number' && 'data' in row;
|
||||
}
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
SESSIONS: `${STORAGE_PREFIX}sessions`,
|
||||
ACTIVE_SESSION: `${STORAGE_PREFIX}active_session`,
|
||||
@@ -171,12 +182,14 @@ function updateWindowLRU(cacheKey: string): void {
|
||||
}
|
||||
|
||||
export function loadSessionWindowCache(sessionId: string, userId?: number): ChatDayBucket[] | null {
|
||||
return safeGetItem<ChatDayBucket[] | null>(getWindowCacheKey(sessionId, userId), null);
|
||||
const raw = safeGetItem<unknown>(getWindowCacheKey(sessionId, userId), null);
|
||||
if (!isVersionedCache<ChatDayBucket[]>(raw) || raw.schema !== CACHE_SCHEMA) return null;
|
||||
return raw.data;
|
||||
}
|
||||
|
||||
export function saveSessionWindowCache(sessionId: string, buckets: ChatDayBucket[], userId?: number): void {
|
||||
const cacheKey = getWindowCacheKey(sessionId, userId);
|
||||
safeSetItem(cacheKey, buckets);
|
||||
safeSetItem(cacheKey, { schema: CACHE_SCHEMA, data: buckets });
|
||||
updateWindowLRU(cacheKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,16 @@ test('window cache LRU: 驱逐带 userId 前缀的完整 cache key,不留孤
|
||||
}
|
||||
});
|
||||
|
||||
test('window cache: 无 schema 的旧载荷不当作有效缓存', () => {
|
||||
const { store, restore } = installMemoryLocalStorage();
|
||||
try {
|
||||
store.set('smdf_chat_window_u7_sess-old', JSON.stringify([{ dayKey: 'stale' }]));
|
||||
assert.equal(loadSessionWindowCache('sess-old', 7), null);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('window cache: 读写同一 userId 前缀,跨用户不串读', () => {
|
||||
const { restore } = installMemoryLocalStorage();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user