L2: permission mode propagation across forward/calendar/adopt + PUT endpoint + tests
P1 - forward inherits mode:
- doForward: when creating a new target session, use InheritedMode from
the source session (plan parent → plan child, cannot escalate)
- enforcement snapshot set from receiving agent
P1 - calendar_events gets permission_mode column:
- Added to 3 migration sites (sqlite init, pg init, incremental alter)
- CalendarEvent model gains PermissionMode field
- CreateCalendarEvent / UpdateCalendarEvent normalize + persist the field
- resolveCalendarSession: on new session → write event's mode;
on reuse → ModeAtMost(cur, eventMode), prevents escalation
(plan Agent's reminder fires into a workspace session = bypass)
- SendCalendarMail now takes permMode and threads it through
- fireEvent passes event.PermissionMode to all delivery paths
P1 - AdoptPlatformSession explicitly writes default mode:
- Writes DefaultPermissionMode + enforcement on adopt, instead of
relying on DB column default (avoids silent drift on schema changes)
P2 - PUT /sessions/{id}/permission endpoint:
- New handler UpdateSessionPermission (auth required, access check)
- Registers PUT route alongside existing budget/alias endpoints
- Broadcasts session_update on change
- Does NOT refresh enforcement (design: snapshot at creation)
P3 - Tests (24 new cases):
- permission_mode_test.go: InheritedMode (6 cases), SetSessionPermissionMode
roundtrip, dirty value fail-closed, NormalizePermissionMode, calendar event
roundtrip/dirty/update, adopt writes default mode, plan escalation guard,
3-level inheritance chain
- defaultsession_test.go: budget regression, permission mode regression
(pins created=false → no reset on reuse)
Deploys with: bash deploy/redeploy-gateway.sh --skip-tests
Schema migration: auto via addMissingColumns (new column default 'workspace')
This commit is contained in:
@ -127,12 +127,24 @@ func doForward(w http.ResponseWriter, r *http.Request, mailID uuid.UUID, actor,
|
||||
subject := forwardSubject(req.Subject, src.Subject)
|
||||
|
||||
// 转发按目标地址寻址,不带 reply_to:它是一条新线索,不该并进原会话
|
||||
sessionID, _, _, err := resolveTarget(r, to, "", actor, subject, req.SessionAlias, agentLimiterKey(isAgent, actor))
|
||||
sessionID, _, created, err := resolveTarget(r, to, "", actor, subject, req.SessionAlias, agentLimiterKey(isAgent, actor))
|
||||
if err != nil {
|
||||
writeErr(w, err, "Failed to resolve session")
|
||||
return
|
||||
}
|
||||
|
||||
// 权限档位继承自源会话(plan 档派不出 full 档子任务,约束沿链条传递)。
|
||||
// 只在【新建】目标会话时设:复用既有会话时不改写对方正在遵守的规则。
|
||||
if created {
|
||||
mode := repo.InheritedMode(r.Context(), &src.SessionID, models.DefaultPermissionMode)
|
||||
if _, err := repo.SetSessionPermissionMode(r.Context(), sessionID, mode); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "Failed to set permission mode")
|
||||
return
|
||||
}
|
||||
_ = repo.SetSessionEnforcement(r.Context(), sessionID,
|
||||
repo.AgentModeEnforcement(r.Context(), to.Name))
|
||||
}
|
||||
|
||||
if isAgent {
|
||||
// 转发也是一次主动发信,扣【目标会话】的往返预算。
|
||||
// 扣目标而不是源:转发开启的是一条新线索,消耗的是新线索的额度。
|
||||
|
||||
Reference in New Issue
Block a user