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:
2026-09-06 17:32:47 +08:00
parent 47fe9a5e74
commit 1dc6223631
12 changed files with 592 additions and 15 deletions

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/agentmail/gateway/internal/db"
"github.com/agentmail/gateway/internal/models"
"github.com/google/uuid"
)
@ -320,6 +321,14 @@ func AdoptPlatformSession(ctx context.Context, agentName, platformID, slug, work
platformID, id); err != nil {
return uuid.Nil, err
}
// 显式写入档位与强制力:接管一条平台会话没有父会话,
// 只靠 DB 默认值会在「schema 列定义变动」或「迁移补列给了不同默认」时
// 静默偏离预期 —— 显式写 'workspace' 是唯一可靠表述「这条会话是新接管的,
// 没有继承来源」的方式。与 me.go 新建会话那条路径一致。
if _, err := SetSessionPermissionMode(ctx, id, models.DefaultPermissionMode); err != nil {
return uuid.Nil, err
}
_ = SetSessionEnforcement(ctx, id, AgentModeEnforcement(ctx, agentName))
// 别名尽量用 slug;撞名时 EnsureSessionAlias 自动加后缀
_, _ = EnsureSessionAlias(ctx, id, slug)
return id, nil