会话边界(INV-SESS-01~03)

wesgine 中会话(Session)的 Actor 边界设计,确保用户只能访问自己的会话和检查点。


核心问题

Session 是 Actor 边界的第二个面。归属键曾是 Tags["actor"],现已提列为 wes_sessions.actor 独立列。


三条不变量

INV-SESS-01:变更走 cell:chat

会话变更的授权面是 cell:chat 不是 cell:read。

/sessions 曾整段缺席 defaultCellRequiredScope,每个变更落到只读默认值。

Cell 级批量操作(interrupt-all / mark-interrupted / delete-by-prefix)不指名 session、无归属可查,门只能是 admin token。

INV-SESS-02:归属单点 AssertActor

归属只有一个执行点 SessionHandle.AssertActor,且 HTTP 门开在路由漏斗 dispatchCellSessionByID 而非各 handler。

// 漏斗入口——所有 /sessions/{sid}/* 子路由的唯一入口
func dispatchCellSessionByID(w, r, cellHandle, sessionID) {
    if err := cellHandle.Sessions().AssertActor(ctx, sessionID, actor); err != nil {
        http.Error(w, "not found", 404)
        return
    }
    // 分发到具体 handler
}

为什么用漏斗不用各 handler:在漏斗上判据使"新增子路由忘了检查"不可表达。在 handler 上判据产出的正是 4/12 的覆盖率。

INV-SESS-03:行身份 (session_id, id)

Checkpoint / Branch 的行身份是 (session_id, id),SQL 与方法签名都必须带 session。

-- ✅ 正确
DELETE FROM wes_checkpoints WHERE id = ? AND session_id = ?

-- ❌ 危险
DELETE FROM wes_checkpoints WHERE id = ?
-- 归属门只能为 session 那一半作证,id 那一半是调用方给的

攻击场景:DELETE /sessions/我的/checkpoints/受害者的 会通过一个回答正确的门("这个会话是你的")然后删掉别人的行。


归属提列

actor 从 Tags["actor"] 提成 wes_sessions.actor 独立列:

不从 Tags 兜底的理由:Tags 是调用方给的审计元数据,从它读归属会让忘填 Actor 的写入方静默成功。


删除的幂等性

操作跨 session不存在
Delete checkpoint幂等成功幂等成功
Restore checkpointErrCheckpointNotInSession → 404404
Fork checkpointErrNotInSession → 404404

区分"不存在"与"存在但不是你的"= 存在性预言机,所以删除不区分。

只有 Restore / Fork 硬失败——它们的成功会返回或复制内容。


消息查询

GET /cells/{id}/sessions/{sid}/messages 支持分页:

?before=<message_id>&limit=50

消息列表在 AssertActor 通过后才执行查询。


会话中断

端点权限说明
POST /sessions/{sid}/interruptcell:chat + actor 归属中断单个会话的 Run
POST /sessions/interrupt-allcell:admin中断 Cell 内所有 Run
POST /sessions/mark-interruptedcell:admin崩溃后批量标记

与其他面的关系

面归属判据执行点
记忆ActorVisiblesaveCore / 读查询
会话AssertActordispatchCellSessionByID
观测actorClauseSQL 谓词
CronjobVisible / ownedJobManager 方法