Files
weichen f84733d128
ci / vet-and-test (pull_request) Successful in 43s
Add shared execution budgets and the action-authz matrix (U3 slice 2)
Why: plan U3 remainder (R8/KTD13) - execution admission must be bounded and shared (leases in the database, parents yield while waiting so budgets cannot deadlock parent-child), and project authorization must distinguish read/edit/approve/execute/admin with default-deny for unconfigured actions.

Evidence: full suite green on SQLite and against real PostgreSQL 18.6 + Redis (isolated instance); new tests - admission_pg_test (enforcement, per-user, concurrent no-oversell), runmgr admission tests (saturated-budget sync spawn progresses, background children share the budget, bounded queue 429), server action-matrix tests (default deny, legacy adapter read-only, action-aware authoritative + fail-closed).

Impact: admission_leases (v21) with atomic lease accounting under an advisory lock; runtime.execBudgetGlobal/PerUser/execQueueMax settings (all zero = off, historical behavior preserved); conversation turns, background and sync subagents share the pool, parents yield around synchronous children and HITL waits; queue-full rejects 429 before writing; server gains ProjectAction matrix + SetProjectActionAuthorizer with legacy EXT-8 read fallback.

Refs: proposals/2026-10-11-collaboration-storage-and-distribution.md U3 R8/KTD13; docs/guides/team-storage.md section 9.
2026-10-11 05:18:12 +00:00

90 lines
3.1 KiB
Go

package server
import (
"testing"
core "agent-system-core"
)
// 协作存储 U3 切片 2(KTD13):动作授权矩阵。未配置的动作默认拒绝;
// 旧只读 EXT-8 适配器只保留 read 能力;action-aware 适配器注册后权威。
func newMatrixServer(t *testing.T) *Server {
t.Helper()
s := &Server{}
t.Cleanup(func() {})
return s
}
// 无任何适配器:全部动作拒绝(默认拒绝;与 EXT-8 未注册时读取 NotFound 同基调)。
func TestActionMatrixDefaultDeny(t *testing.T) {
s := newMatrixServer(t)
for _, a := range []ProjectAction{ProjectRead, ProjectEdit, ProjectApprove, ProjectExecute, ProjectAdmin} {
if s.authorizeProjectAction("alice", "p1", a) {
t.Fatalf("未注册适配器时 %s 应默认拒绝", a)
}
}
}
// 旧只读适配器:read 放行、其余动作不得自动获得写入能力(KTD13 兼容语义)。
func TestActionMatrixLegacyAdapterReadOnly(t *testing.T) {
s := newMatrixServer(t)
s.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
return owner == "alice", nil
})
if !s.authorizeProjectAction("alice", "p1", ProjectRead) {
t.Fatal("alice read 应放行(旧适配器)")
}
for _, a := range []ProjectAction{ProjectEdit, ProjectApprove, ProjectExecute, ProjectAdmin} {
if s.authorizeProjectAction("alice", "p1", a) {
t.Fatalf("旧只读适配器不得自动获得 %s 能力", a)
}
}
// 他人(动作越权):read 同样拒绝。
if s.authorizeProjectAction("bob", "p1", ProjectRead) {
t.Fatal("bob 越权 read 应拒绝")
}
}
// action-aware 适配器:注册后权威(read 与显式配置的动作放行,未配置动作
// 默认拒绝;出错 fail-closed)。
func TestActionMatrixActionAwareAuthoritative(t *testing.T) {
s := newMatrixServer(t)
// 同时注册旧适配器:action-aware 存在时不得回落旧适配器(权威唯一)。
s.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
return true, nil
})
s.SetProjectActionAuthorizer(func(owner string, projectID core.ID, action ProjectAction) (bool, error) {
switch action {
case ProjectRead, ProjectEdit:
return owner == "alice", nil
default:
return false, nil // approve/execute/admin 未授予
}
})
if !s.authorizeProjectAction("alice", "p1", ProjectRead) || !s.authorizeProjectAction("alice", "p1", ProjectEdit) {
t.Fatal("alice read/edit 应放行")
}
for _, a := range []ProjectAction{ProjectApprove, ProjectExecute, ProjectAdmin} {
if s.authorizeProjectAction("alice", "p1", a) {
t.Fatalf("未授予的 %s 应默认拒绝", a)
}
}
if s.authorizeProjectAction("bob", "p1", ProjectEdit) {
t.Fatal("bob 越权 edit 应拒绝")
}
// 适配器出错:fail-closed(错误不区分,避免泄露存在性)。
s.SetProjectActionAuthorizer(func(owner string, projectID core.ID, action ProjectAction) (bool, error) {
return false, errBoom
})
if s.authorizeProjectAction("alice", "p1", ProjectRead) {
t.Fatal("适配器出错应 fail-closed")
}
}
var errBoom = &boomErr{}
type boomErr struct{}
func (*boomErr) Error() string { return "boom" }