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:
@ -165,7 +165,7 @@ func fireEvent(ctx context.Context, e models.CalendarEvent) {
|
||||
// 一起发:首个是主收件人,其余进 cc_list —— 所有人共享同一条线索,
|
||||
// 能看到彼此的回复。适合「pi 主办、dsh 知情」这种有主次的协作。
|
||||
primary, cc := recipients[0], recipients[1:]
|
||||
if err := SendCalendarMail(ctx, e.EventID, primary, subject, body, e.CreatedBy, cc...); err != nil {
|
||||
if err := SendCalendarMail(ctx, e.EventID, primary, subject, body, e.CreatedBy, e.PermissionMode, cc...); err != nil {
|
||||
log.Printf("[scheduler] 投递提醒失败(%s → %s +%d抄送): %v",
|
||||
e.Title, primary, len(cc), err)
|
||||
} else {
|
||||
@ -182,7 +182,7 @@ func fireEvent(ctx context.Context, e models.CalendarEvent) {
|
||||
// 另外两个仍该收到提醒。
|
||||
ok, failed := 0, 0
|
||||
for _, addr := range recipients {
|
||||
if err := SendCalendarMail(ctx, e.EventID, addr, subject, body, e.CreatedBy); err != nil {
|
||||
if err := SendCalendarMail(ctx, e.EventID, addr, subject, body, e.CreatedBy, e.PermissionMode); err != nil {
|
||||
failed++
|
||||
log.Printf("[scheduler] 投递提醒失败(%s → %s): %v", e.Title, addr, err)
|
||||
continue
|
||||
@ -231,7 +231,7 @@ func short(id string) string {
|
||||
// - `agent@/path` → 指定工作目录的默认会话
|
||||
// - `agent@/path.alias` → 指定已存在的会话(不存在则报错,不静默新建)
|
||||
// - `agent@/path.new` → 每次提醒开一条新会话(适合互不相关的一次性任务)
|
||||
func SendCalendarMail(ctx context.Context, eventID, toAddr, subject, body, createdBy string, ccAddrs ...string) error {
|
||||
func SendCalendarMail(ctx context.Context, eventID, toAddr, subject, body, createdBy, permMode string, ccAddrs ...string) error {
|
||||
addr, err := models.ParseAddress(toAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("收件地址 %q 无法解析: %w", toAddr, err)
|
||||
@ -255,7 +255,7 @@ func SendCalendarMail(ctx context.Context, eventID, toAddr, subject, body, creat
|
||||
ccList = append(ccList, ca)
|
||||
}
|
||||
|
||||
sessionID, err := resolveCalendarSession(ctx, addr, subject)
|
||||
sessionID, err := resolveCalendarSession(ctx, addr, subject, permMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -335,7 +335,27 @@ const calendarSender = "calendar"
|
||||
//
|
||||
// 与 handler.resolveTarget 同一套三态语义,但**不受新建会话速率限制**:
|
||||
// 那条限制是防 Agent 暴开线索的,而日历事件的数量由人在界面上决定。
|
||||
func resolveCalendarSession(ctx context.Context, addr models.Address, subject string) (uuid.UUID, error) {
|
||||
//
|
||||
// 档位规则(见 PLAN 7.11 P1):
|
||||
// - 新建会话 → 用事件档位定死(permMode,已规范化)
|
||||
// - 复用已有会话 → ModeAtMost(会话现档, 事件档),取更严,
|
||||
// 不允许因复用而提权(plan 档 Agent 建的日程触发时拿 workspace 就绕开了 plan)
|
||||
func resolveCalendarSession(ctx context.Context, addr models.Address, subject, permMode string) (uuid.UUID, error) {
|
||||
// permMode 由调用方已规范化过,这里再保一次(直接调用本函数的路径上该一样)
|
||||
eventMode := models.NormalizePermissionMode(permMode)
|
||||
apply := func(sessionID uuid.UUID, created bool) {
|
||||
if created {
|
||||
_, _ = repo.SetSessionPermissionMode(ctx, sessionID, eventMode)
|
||||
_ = repo.SetSessionEnforcement(ctx, sessionID, repo.AgentModeEnforcement(ctx, addr.Name))
|
||||
return
|
||||
}
|
||||
// 复用已有会话:取更严。ModeAtMost 已判过会话现档与事件档,取严的那个。
|
||||
cur := repo.SessionPermissionMode(ctx, sessionID)
|
||||
merged := models.ModeAtMost(cur, eventMode)
|
||||
if merged != cur {
|
||||
_, _ = repo.SetSessionPermissionMode(ctx, sessionID, merged)
|
||||
}
|
||||
}
|
||||
switch addr.Mode() {
|
||||
case models.SessionNew:
|
||||
id, err := repo.CreateSession(ctx, nil, calendarSender, subject, addr.Path)
|
||||
@ -345,6 +365,7 @@ func resolveCalendarSession(ctx context.Context, addr models.Address, subject st
|
||||
// 与发信路径一致:`.new` 建完必须立刻有别名,否则这条会话
|
||||
// 除了回复那一封之外再也无法寻址(未命名会话查不到也补全不出来)。
|
||||
_, _ = repo.EnsureSessionAlias(ctx, id, repo.AutoAliasFor(addr.Name, subject))
|
||||
apply(id, true)
|
||||
return id, nil
|
||||
|
||||
case models.SessionNamed:
|
||||
@ -358,14 +379,16 @@ func resolveCalendarSession(ctx context.Context, addr models.Address, subject st
|
||||
return uuid.Nil, err
|
||||
}
|
||||
repo.TouchSession(ctx, id)
|
||||
apply(id, false)
|
||||
return id, nil
|
||||
|
||||
default: // SessionDefault
|
||||
id, err := repo.FindOrCreateDefaultSession(ctx, addr.Name, addr.Path, calendarSender, subject)
|
||||
id, created, err := repo.FindOrCreateDefaultSessionCreated(ctx, addr.Name, addr.Path, calendarSender, subject)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
_, _ = repo.EnsureSessionAlias(ctx, id, repo.AutoAliasFor(addr.Name, subject))
|
||||
apply(id, created)
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user