From f718d36d23adcd0567c908ba5a8ddcd97fe8e453 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 13:43:24 +0000 Subject: [PATCH 1/2] Add red test for EXT-19 account lister surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: 方案 r1 U3/KTD2——最小账号目录(owner+有效态+分页)是 RDM 成员管理的公开前置。 Evidence: go vet 编译红(AccountLister/AccountEntry/SurfaceVersion 2.11.0 未定义)。 Impact: 仅测试。 Refs: 方案 r1 U3/KTD2;Dev#477 identity CLI --- pkg/extension/account_lister_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pkg/extension/account_lister_test.go diff --git a/pkg/extension/account_lister_test.go b/pkg/extension/account_lister_test.go new file mode 100644 index 0000000..d448879 --- /dev/null +++ b/pkg/extension/account_lister_test.go @@ -0,0 +1,26 @@ +package extension + +// EXT-19 红测(方案 r1 U3/KTD2 最小账号目录):公开面新增可选能力接口 +// AccountLister(ListAccounts:owner+有效态+游标分页——授权管理所需的最小身份, +// 不含姓名/邮箱/联系方式)。红:类型未定义,编译红先行。 +import ( + "encoding/json" + "strings" + "testing" +) + +func TestAccountListerSurface(t *testing.T) { + if !strings.HasPrefix(SurfaceVersion, "2.11.0") { + t.Fatalf("SurfaceVersion 应升至 2.11.0-experimental(新增导出符号须升版),got %s", SurfaceVersion) + } + // 接口方法签名即契约:游标分页 + 最小字段。 + var _ AccountLister = (AccountLister)(nil) + e := AccountEntry{Owner: "u1", Valid: true} + b, err := json.Marshal(e) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(b), `"owner":"u1"`) || !strings.Contains(string(b), `"valid":true`) { + t.Fatalf("AccountEntry 序列化不符(最小字段 owner/valid): %s", b) + } +} -- 2.54.0 From bff6820e5f2e08082b17e4ef8ad73b5b5296cb68 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 13:46:15 +0000 Subject: [PATCH 2/2] Add EXT-19 account lister host capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: 方案 r1 U3/KTD2——RDM 成员管理需最小账号目录(owner+有效态+分页);此前无公开能力,下游只能盲加成员字符串。 Evidence: 红(AccountLister 未定义编译红);绿:surface 棘轮 2.11.0 + TestInprocHostListAccounts(排序/游标/limit 钳制/真实 store);go test ./... 全绿。 Impact: 公开面 AccountEntry{Owner,Valid}+AccountLister(游标分页,limit 1-200 缺省 50);inprocHost 实现(users 表,无姓名/联系方式下发);既有 EXT-8b 版本断言放宽为下限。 Refs: 方案 r1 U3/KTD2;Dev#477/#180 identity CLI --- internal/app/account_lister_test.go | 53 +++++++++++++++++++++++++ internal/app/hostcaps.go | 40 +++++++++++++++++++ pkg/extension/extension.go | 18 ++++++++- pkg/extension/resource_authz_v2_test.go | 4 +- pkg/extension/surface.txt | 3 ++ 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 internal/app/account_lister_test.go diff --git a/internal/app/account_lister_test.go b/internal/app/account_lister_test.go new file mode 100644 index 0000000..30821a7 --- /dev/null +++ b/internal/app/account_lister_test.go @@ -0,0 +1,53 @@ +package app + +// EXT-19 消费测试:inprocHost.ListAccounts——最小目录(owner/valid)、游标分页、 +// limit 钳制(1-200,缺省 50)。数据来自 users 表(真实 store,非桩)。 +import ( + "testing" + + "agent-system-backend/internal/store" +) + +func TestInprocHostListAccounts(t *testing.T) { + dir := t.TempDir() + st, err := store.OpenSQLite(dir + "/test.db") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { st.Close() }) + for _, o := range []string{"carol", "alice", "bob", "dave"} { + if _, err := st.CreateAPIKey(o, "e2e"); err != nil { + t.Fatal(err) + } + } + h := inprocHost{caps: hostcaps{st: st}} + + // 首页(limit=2):alice/bob,next=bob。 + page, next, err := h.ListAccounts("", 2) + if err != nil { + t.Fatal(err) + } + if len(page) != 2 || page[0].Owner != "alice" || page[1].Owner != "bob" { + t.Fatalf("首页不符: %+v", page) + } + for _, e := range page { + if !e.Valid { + t.Fatalf("现有账号应 valid: %+v", e) + } + } + if next != "bob" { + t.Fatalf("next 应为 bob, got %q", next) + } + // 次页:carol/dave,next=""(末页)。 + page2, next2, _ := h.ListAccounts(next, 2) + if len(page2) != 2 || page2[0].Owner != "carol" || page2[1].Owner != "dave" || next2 != "" { + t.Fatalf("次页不符: %+v next=%q", page2, next2) + } + // limit 钳制:0 → 50 缺省;超 200 → 200(不炸)。 + if p, _, _ := h.ListAccounts("", 0); len(p) != 4 { + t.Fatalf("limit=0 应取缺省(4 账号全量), got %d", len(p)) + } + if _, _, err := h.ListAccounts("", 999); err != nil { + t.Fatalf("limit 超限应钳制不报错: %v", err) + } +} diff --git a/internal/app/hostcaps.go b/internal/app/hostcaps.go index 0e9ce60..89c440e 100644 --- a/internal/app/hostcaps.go +++ b/internal/app/hostcaps.go @@ -171,6 +171,46 @@ func (h inprocHost) IsAdmin() bool { return state.IsAdmin } +// ListAccounts 实现 EXT-19 AccountLister(最小账号目录):仅授权管理读面用。 +// 有效态=账号存在且非停用(当前 users 表无停用位——must_change_password 不影响 +// 有效性;后续加停用位在此映射)。游标=owner 升序偏移。 +func (h inprocHost) ListAccounts(cursor string, limit int) ([]extension.AccountEntry, string, error) { + if limit <= 0 || limit > 200 { + limit = 50 + } + users, err := h.caps.st.ListUsersAdmin() + if err != nil { + return nil, "", err + } + entries := make([]extension.AccountEntry, 0, len(users)) + for _, u := range users { + entries = append(entries, extension.AccountEntry{Owner: u.Owner, Valid: true}) + } + // 排序+游标(owner 升序;cursor 为上一页末 owner,空=首页)。 + for i := 0; i < len(entries); i++ { + for j := i + 1; j < len(entries); j++ { + if entries[j].Owner < entries[i].Owner { + entries[i], entries[j] = entries[j], entries[i] + } + } + } + start := 0 + if cursor != "" { + for start < len(entries) && entries[start].Owner <= cursor { + start++ + } + } + end := start + limit + if end > len(entries) { + end = len(entries) + } + next := "" + if end < len(entries) { + next = entries[end-1].Owner + } + return entries[start:end], next, nil +} + func (h inprocHost) KVPut(key, value string) error { return h.caps.st.PutPluginKV(h.pluginID, h.owner, key, value) } diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index d8dbf72..93939a0 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -25,12 +25,14 @@ import ( // 2.8.0-experimental:EXT-17 新增可选能力接口 SurfaceHost(SurfaceSave/Get/List/ // Activation;Generative UI U2)。实现由基座 hostcaps 注入;未装配时类型断言 // 不成立即明确拒绝,不给必实现 Host 接口加方法(不破坏既有消费者)。 +// 2.11.0-experimental:EXT-19 新增可选能力接口 AccountLister(ListAccounts: +// 最小账号目录——owner+有效态+游标分页,U3/KTD2;不含姓名/联系方式)。 // 2.10.0-experimental:EXT-8b 新增 ResourceAuthzQuery/ResourceAuthorizerV2 与 // Registry.RegisterResourceAuthorizerV2(资源级授权,U9 首切片)。 // 2.9.0-experimental:Registry.AddAdminZone 与 AdminZoneSpec(WP3-I3/KTD4:编译 // 插件向基座管理区声明展示分区;分区入口不做基座级权限判定,数据能力由 // 分区组件自身的命令面 fail-closed)。 -const SurfaceVersion = "2.10.0-experimental" +const SurfaceVersion = "2.11.0-experimental" // ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。 var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link") @@ -80,6 +82,20 @@ type Registry interface { AddAdminZone(spec json.RawMessage) error } +// AccountEntry 最小账号目录行(U3/KTD2):owner+有效态——授权管理所需的最小 +// 身份;姓名/邮箱/联系方式一律不下发(隐私边界)。 +type AccountEntry struct { + Owner string `json:"owner"` + Valid bool `json:"valid"` +} + +// AccountLister EXT-19 可选能力接口(实现由宿主注入):游标分页列举系统账号。 +// 未实现时下游回落现行行为(成员添加不做账号有效性校验);实现后仅用于 +// 授权管理读面——不授予任何写权。 +type AccountLister interface { + ListAccounts(cursor string, limit int) (entries []AccountEntry, next string, err error) +} + // AdminZoneSpec 管理区分区声明(WP3-I3):ID 是分区身份(建议形如 // .
,前端按 ID 映射内置分区组件);Title/Icon/Desc 为展示 // 元数据(icon 取基座前端 IconName 词表,未知值前端回退通用图标);Order diff --git a/pkg/extension/resource_authz_v2_test.go b/pkg/extension/resource_authz_v2_test.go index 395287f..9feb0c4 100644 --- a/pkg/extension/resource_authz_v2_test.go +++ b/pkg/extension/resource_authz_v2_test.go @@ -13,8 +13,8 @@ import ( ) func TestResourceAuthzV2SurfacePresent(t *testing.T) { - if !strings.HasPrefix(SurfaceVersion, "2.10.0") { - t.Fatalf("SurfaceVersion 应升至 2.10.0-experimental(新增导出符号须升版),got %s", SurfaceVersion) + if SurfaceVersion < "2.10.0" { + t.Fatalf("SurfaceVersion 应 ≥2.10.0-experimental(EXT-8b 起生效),got %s", SurfaceVersion) } var _ ResourceAuthorizerV2 = func(q ResourceAuthzQuery) (bool, error) { return q.Actor != "" && q.Action != "" && q.ResourceType != "" && q.ResourceID != "", nil diff --git a/pkg/extension/surface.txt b/pkg/extension/surface.txt index 327acb4..a893299 100644 --- a/pkg/extension/surface.txt +++ b/pkg/extension/surface.txt @@ -1,3 +1,6 @@ +AccountEntry +AccountLister +AccountLister.ListAccounts AdminProvider AdminProvider.IsAdmin AdminZoneSpec -- 2.54.0