// Package extension 是基座对下游仓的公开扩展面。 // 下游只能依赖本包(以及 agent-system-core 的类型),不能导入 internal。 package extension import ( "context" "encoding/json" "errors" "flag" "fmt" "sync" core "agent-system-core" ) // SurfaceVersion 是本包导出面的实验版本。变更导出符号必须同时更新 surface.txt。 // 2.1.0-experimental:EXT-8 新增 ResourceAuthorizer 与 Registry.RegisterResourceAuthorizer。 // 2.2.0-experimental:EXT-9 新增 Host.KnowledgePublish/KnowledgeDelete 与 // Options.DisableUploadIndexing。 // 2.3.0-experimental:EXT-12 最小开关 Options.DisableConvCardMirror(会话卡不镜像 // 进项目公共流)。 // 2.4.0-experimental:EXT-11 新增 SetScopedHostFactory/NewScopedHost(按 owner+pluginID // 重建受限宿主句柄,供下游恢复路径)。 // 2.5.0-experimental:Options.DisableExecTools(执行原语启动级硬关闭)。 // 2.6.0-experimental:Options.ExecBackend(EXT-16 执行后端 direct|sandbox)。 // 2.7.0-experimental:Registry.AddIntent 与 IntentSpec(G2-2 意图声明面)。 // 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)。 // 2.12.0-experimental: optional ProjectHost/ProjectLifecycleRegistry and trusted // ResourceAuthzQuery.Source for per-document publication authorization. // 2.13.0-experimental: optional AccountDirectoryProvider and trusted in-process // AccountDirectoryRegistrar for scoped, current account candidate decisions. // 2.14.0-experimental: optional ProjectAuditHost and ProjectAuditRegistry (X2 // project-bound command-audit read slice and domain audit.read callback). // 2.15.0-experimental: X2 optional committed project-audit link publisher, // verifier, disclosure callback, and exact linked receipt read. // 2.16.0-experimental: trusted Request.OperationID before handler execution and // retryable ProjectAuditLinkRequest.HostAuditOffset=0 resolution. // 2.17.0-experimental: cryptographic operation IDs and registration-bound // ProjectAuditPublisherFactory; scoped Host string IDs cannot publish links. // 2.18.0-experimental: PostgreSQL project audit pages require a verified // committed-prefix ID generator; unsafe sequence/default drift rejects reads. // 2.19.0-experimental: all PostgreSQL audit writes require the database // insert guard and append-only policy before a committed-prefix claim. // 2.20.0-experimental: combine optional X1 project context/source recheck and // X2 project audit capabilities on one Backend surface. // 2.21.0-experimental: optional ProjectWorkObservationRegistry; host-origin // work-source observations and project action decisions (X3 contract). // 2.22.0-experimental: optional HumanDecisionVerifier. The host proves that // this dispatch is an authenticated human response to one exact pending card. // It does not consume the proof; the caller does that in its own transaction. // 2.23.0-experimental: InProc.HostOptions / CLIHook, plus MergeHostOptions, // RegisterCLIFlags、RunCLIHooks(组合二进制把插件声明并进基座启动,不覆盖显式参数)。 // 2.24.0-experimental:EXT-20 可选 RegulationsHost 与受信进程内 RegulationsRegistrar // (共享法规检索与网络证据;仅插件 kbreg 可注册提供方)。 // 2.25.0-experimental: IAM U4 optional RolePolicyProvider and // SystemActionProvider for the Backend-owned single-effective-role policy; // resource queries carry the current authority epoch so downstream caches can // reject stale role snapshots. // 2.26.0-experimental: IAM link U1 RolePolicySnapshot.Linked carries the // Backend-computed project.create own-project management link and its source. // 2.27.0-experimental: IAM link U3 optional CapabilityExplainProvider // (CapabilityExplain / MemberBasicCollaborate / DocumentQualify for the // Host-bound actor) and categorized WorkFailure for create, member add and // conversation work. // 2.28.0-experimental: optional RoleOptionsProvider (read-only enabled role // directory: RoleOption roleId+name, no permissions or bindings) and the // AccountDirectory purpose project.member-roster, an explicit project member // enumeration inside the trusted RDM scope. // 2.29.0-experimental: X2 project audit read completion. ProjectAuditQuery gains // optional linked resource/run locators, IncludeBody, and Export; receipts may // carry ProjectAuditReceiptResource only under the domain's per-object body // right; ProjectAuditCoverage states Export scope. Every addition is an optional // field or a new exported type: existing ProjectAuditHost/ProjectAuditRegistry // providers and queries keep compiling and behaving unchanged. const SurfaceVersion = "2.29.0-experimental" // ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。 var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link") // ResourceAuthorizer 是 EXT-8 资源授权适配器:基座在 owner 自有资源判定未命中时, // 回调询问「owner 对 projectID 是否有共享读取授权」。只回答布尔,不接触基座内部 // 状态;返回 error 视为拒绝(fail-closed)。仅作用于读取路径,写路径一律 owner 自有。 type ResourceAuthorizer func(owner string, projectID core.ID) (bool, error) // ResourceAuthzQuery 是 EXT-8b 资源级授权查询(U9):字段由执行端从受信上下文 // (认证会话、受信存储)构造,不采信请求自报的归属;下游只回答布尔,不接触基座 // 内部状态;返回 error 视为拒绝(fail-closed)。读取路径专用;写路径一律 owner 自有。 type ResourceAuthzQuery struct { Actor string `json:"actor"` // 认证主体(发起读取的会话) Action string `json:"action"` // read|download|list|search|stream|model-context ResourceType string `json:"resourceType"` // 基座资源类型(artifact/attachment/...) ResourceID core.ID `json:"resourceId"` // 资源稳定 ID Owner string `json:"owner"` // 资源属主(受信存储解析,非请求自报) // Source is optional trusted resource provenance, populated from stored knowledge rows. Source string `json:"source,omitempty"` ProjectID core.ID `json:"projectId,omitempty"` // 所属项目;空=无项目私域 // AuthorityEpoch is read from Backend authority_state at the point of use. // Downstream policy caches must reject snapshots from another epoch. AuthorityEpoch int64 `json:"authorityEpoch,omitempty"` // RolePolicy is the current Backend snapshot for Actor on trusted in-process // calls. It lets downstream resource authorization refresh without a command. RolePolicy *RolePolicySnapshot `json:"rolePolicy,omitempty"` } // ResourceAuthorizerV2 EXT-8b 资源级授权适配器:V1(owner×project 布尔)之上的 // 细粒度形态——注册后对域内读路径权威(含 owner 自有命中与共享读取)。 type ResourceAuthorizerV2 func(q ResourceAuthzQuery) (bool, error) // Registry 是进程内扩展的注册口。实现在基座内部,对下游只暴露这些方法。 type Registry interface { AddSkill(name, markdown string) error AddCommand(spec json.RawMessage) error // AddCommandHandler 注册插件命令规格并绑定进程内处理器(EXT-1): // 基座在分发时按请求 Owner 构造 Host 句柄调用 h。 AddCommandHandler(spec json.RawMessage, h Handler) error Note(kind, name string, payload json.RawMessage) error // RegisterResourceAuthorizer 注册 EXT-8 资源授权适配器(进程内扩展); // 首个注册者生效。声明式扩展不支持(回调需宿主进程内代码)。 RegisterResourceAuthorizer(authz ResourceAuthorizer) error // RegisterResourceAuthorizerV2 注册 EXT-8b 资源级授权适配器(进程内扩展); // 首个注册者生效;注册后读路径按资源粒度咨询(V1 为未注册时的兼容回落)。 RegisterResourceAuthorizerV2(authz ResourceAuthorizerV2) error // AddIntent 声明自然语言意图(G2-2):短语命中后路由到已注册命令—— // 经同一命令总线分发(同审计信封与 HITL 收口)。规格见 IntentSpec。 AddIntent(spec json.RawMessage) error // AddAdminZone 声明基座管理区展示分区(WP3-I3/KTD4):编译插件把自己的 // 领域工作台以分区形式挂进管理区;声明只携带展示元数据,分区入口不做 // 基座级权限判定——数据能力由分区组件自身的命令面 fail-closed。 // 规格见 AdminZoneSpec。 AddAdminZone(spec json.RawMessage) error } // AccountEntry 最小账号目录行(U3/KTD2):owner+有效态——授权管理所需的最小 // 身份。2026-10-13 P3 扩展(提案 rdm-settings-members-duties-perms-increment): // Name 向授权管理读面下发(同名区分与按姓名查找是成员管理必需;扩展申请四要素 // 见 Dev 任务卡 feature/rdm-increment-settings-duties)。邮箱/联系方式仍一律 // 不下发(隐私边界不变)。 type AccountEntry struct { Owner string `json:"owner"` Name string `json:"name,omitempty"` Valid bool `json:"valid"` } // AccountLister EXT-19 旧接口:仅有受信系统管理员分发上下文时可读取。 // 新领域消费者必须使用 AccountDirectoryProvider,不得以本接口作为降级路径。 type AccountLister interface { ListAccounts(cursor string, limit int) (entries []AccountEntry, next string, err error) } // AccountSearcher EXT-19 扩展可选能力(P3,2026-10-13):按账号或姓名检索账号 // 目录。与 AccountLister 相同,它不含领域定位,只允许受信系统管理员上下文; // 新领域消费者不得回落到此接口。 type AccountSearcher interface { SearchAccounts(query string, limit int) (entries []AccountEntry, err error) } // AdminZoneSpec 管理区分区声明(WP3-I3):ID 是分区身份(建议形如 // .
,前端按 ID 映射内置分区组件);Title/Icon/Desc 为展示 // 元数据(icon 取基座前端 IconName 词表,未知值前端回退通用图标);Order // 越小越靠前,同 Order 按 ID 字典序。 type AdminZoneSpec struct { ID string `json:"id"` Title string `json:"title"` Icon string `json:"icon,omitempty"` Desc string `json:"desc,omitempty"` Order int `json:"order,omitempty"` } // SurfaceSaveRequest 是一次页面提交(EXT-17):Spec 为 core.SurfaceSpec 的 // 严格 JSON(未知字段拒绝);身份信封由宿主按分发 Owner/PluginID 绑定, // 不采信请求内自报的 owner/pluginId。 type SurfaceSaveRequest struct { SurfaceID core.ID `json:"surfaceId,omitempty"` ConversationID core.ID `json:"conversationId,omitempty"` SourceRunID core.ID `json:"sourceRunId,omitempty"` BaseRevision int `json:"baseRevision,omitempty"` ObservedEpoch int64 `json:"observedEpoch,omitempty"` ContextVersion string `json:"contextVersion,omitempty"` SemanticText string `json:"semanticText,omitempty"` Spec json.RawMessage `json:"spec"` } // SurfaceHost 是 EXT-17 可选能力接口:插件经宿主保存/读取生成页面。 // 通过对分发 Host 句柄做类型断言获取(断言失败=能力缺失,明确报错不降级)。 type SurfaceHost interface { // SurfaceSave 校验并保存页面;禁用/epoch 失配/规格违规/revision 冲突 // 分别返回确定错误,携带字段级清单时调用方可修复后至多重提一次。 SurfaceSave(ctx context.Context, req SurfaceSaveRequest) (core.SurfaceSnapshot, error) // SurfaceGet 读取整页快照(owner 作用域;历史页面禁用后仍可读)。 SurfaceGet(ctx context.Context, surfaceID core.ID) (core.SurfaceSnapshot, error) // SurfaceList 列出当前 owner 名下(可按会话过滤)的页面元数据。 SurfaceList(ctx context.Context, conversationID core.ID) ([]core.SurfaceMeta, error) // SurfaceActivation 观察本插件在当前 owner 的启禁与 epoch(装配能力时 // 快照 ObservedEpoch,拦截禁用后迟到的在途提交)。 SurfaceActivation(ctx context.Context) (enabled bool, epoch int64, err error) } // IntentSpec 意图声明规格(G2-2):id 形如 .;phrases 是触发 // 短语(宿主归一后包含匹配,最长短语优先);command 须为已注册命令 id; // params 为参数模板(调用方可覆盖)。宿主侧路由入口:system.intent 命令。 type IntentSpec struct { ID string `json:"id"` Phrases []string `json:"phrases"` Command string `json:"command"` Params map[string]any `json:"params,omitempty"` Description string `json:"description,omitempty"` } // CLIHook 让编译插件在基座 serve 旗标集上登记自己的旗标,并在解析后决定是否接管本次进程。 // 插件只登记自己的旗标,不重复基座已有旗标。基座在 flag.Parse 前调用 Register,解析后调用 AfterParse。 type CLIHook struct { Register func(fs *flag.FlagSet) AfterParse func(opt Options) (handled bool, err error) } // InProc 是随下游二进制编译进来的扩展。 type InProc struct { ID string Register func(reg Registry) error // HostOptions 可选。组合二进制把它当作启动默认值:布尔开关按“任一为 true 则为 true” // 合并,且不覆盖调用方显式传入的参数。未设置则无产品默认。 HostOptions func() Options // CLI 可选。组合二进制在基座 serve 旗标上挂接插件专属旗标(如 bootstrap-admin)。 CLI *CLIHook // Prepare 可选。基座合并 HostOptions 之后、进入服务之前调用,可改写 opt 或拒绝启动。 Prepare func(opt *Options) error } // Options 是下游自有二进制的启动参数,字段与 agent-system serve 对齐。 type Options struct { Addr string DataDir string PluginsDir string Mock bool StorageDSN string ArtifactsDir string AuthMode string // RequireProjectContext makes startup fail if no in-process X1 provider // registered. Downstream products opt in when project context is mandatory. RequireProjectContext bool // DisableUploadIndexing 关闭上传端点的自动检索索引(EXT-9;零值=false=现状: // 可解析文本照旧进索引。下游以发布状态机自管索引时置 true)。 DisableUploadIndexing bool // DisableConvCardMirror 关闭会话卡向项目公共流的镜像(EXT-12 最小开关; // 零值=false=现状全量镜像。true 时会话卡只进本人会话流,不发布即不持久, // 项目流重放不含会话卡;项目级卡不受影响)。 DisableConvCardMirror bool // DisableExecTools 启动级硬关闭执行原语(run_shell / run_python / // register_artifact 及小队内核执行配置):用户设置 runtime.exec 无法覆盖。 // 零值=false=现状。多用户共享部署在执行隔离就位前应置 true。 DisableExecTools bool // ExecBackend 执行后端(EXT-16):空/direct=现状直启;sandbox=bwrap 隔离档 // (模型生成代码按不可信对待:断网、只读系统根、tmpfs 遮蔽 /home 与 /tmp、 // 仅会话工作目录与技能目录挂载、进程数限额、超时杀整个沙箱)。 // DisableExecTools 优先级高于此选择。 ExecBackend string // ExecDepsDir、ExecWorkspace、ExecEnvRoot 与基座 serve 同名。 // 产品入口不解析这三项旗标时留空,link 改读同名环境变量 //(AGENT_SYSTEM_EXEC_DEPS_DIR / AGENT_SYSTEM_EXEC_WORKSPACE / AGENT_SYSTEM_EXEC_ENV_ROOT)。 ExecDepsDir string ExecWorkspace string ExecEnvRoot string } var ( mu sync.Mutex compiled []InProc runner func(Options) error ) // MergeHostOptions 把已登记插件的 HostOptions 合并进 base。 // 布尔开关按“任一为 true 则为 true”。ExecBackend 取首个非空值,彼此冲突则返回错误。 // Addr、DataDir、PluginsDir、StorageDSN、AuthMode 等运行参数不从插件合并,避免覆盖调用方。 func MergeHostOptions(base Options) (Options, error) { for _, e := range Compiled() { if e.HostOptions == nil { continue } opt := e.HostOptions() if opt.DisableUploadIndexing { base.DisableUploadIndexing = true } if opt.DisableConvCardMirror { base.DisableConvCardMirror = true } if opt.DisableExecTools { base.DisableExecTools = true } if opt.ExecBackend == "" { continue } if base.ExecBackend != "" && base.ExecBackend != opt.ExecBackend { return base, fmt.Errorf("extension: plugin %s exec backend %q conflicts with %q", e.ID, opt.ExecBackend, base.ExecBackend) } if base.ExecBackend == "" { base.ExecBackend = opt.ExecBackend } } return base, nil } // RegisterCLIFlags 让每个已登记插件往 fs 登记自己的旗标。须在 Parse 之前调用。 func RegisterCLIFlags(fs *flag.FlagSet) { if fs == nil { return } for _, e := range Compiled() { if e.CLI != nil && e.CLI.Register != nil { e.CLI.Register(fs) } } } // RunCLIHooks 在旗标解析之后依次调用插件 AfterParse。 // 第一个 handled 的插件接管本次进程,基座不应再进入服务循环。 func RunCLIHooks(opt Options) (bool, error) { for _, e := range Compiled() { if e.CLI == nil || e.CLI.AfterParse == nil { continue } handled, err := e.CLI.AfterParse(opt) if err != nil || handled { return handled, err } } return false, nil } // PrepareCompiled 在 HostOptions 合并进 opt 之后调用各插件 Prepare。任一错误拒绝启动。 func PrepareCompiled(opt *Options) error { if opt == nil { return errors.New("extension: nil options") } for _, e := range Compiled() { if e.Prepare == nil { continue } if err := e.Prepare(opt); err != nil { return fmt.Errorf("extension: %s: %w", e.ID, err) } } return nil } // RegisterCompiled 在 init 中登记进程内扩展。 func RegisterCompiled(e InProc) { mu.Lock() defer mu.Unlock() compiled = append(compiled, e) } // Compiled 返回已登记扩展的副本。 func Compiled() []InProc { mu.Lock() defer mu.Unlock() out := make([]InProc, len(compiled)) copy(out, compiled) return out } // ResetCompiled 只给测试清状态。 func ResetCompiled() { mu.Lock() compiled = nil mu.Unlock() } // SetRunner 由 link 包在 init 时注入。下游不要调用。 func SetRunner(fn func(Options) error) { mu.Lock() runner = fn mu.Unlock() } // scopedHostFactory 由组合根在装配 hostcaps 后注入(EXT-11):按 owner+pluginID // 构造与分发期同构的受限 Host。下游不要调用 SetScopedHostFactory。 var scopedHostFactory func(owner, pluginID string) Host // SetScopedHostFactory 组合根注入(EXT-11)。下游不要调用。 func SetScopedHostFactory(fn func(owner, pluginID string) Host) { mu.Lock() scopedHostFactory = fn mu.Unlock() } // NewScopedHost 按 owner+pluginID 重建受限宿主句柄(EXT-11):供下游任务恢复等 // 非分发期路径获取合法句柄;能力与分发期同构(owner 作用域、来源前缀强制、 // KV/任务台账隔离)。未 link 或未注入时返回错误(fail-closed,不降级)。 func NewScopedHost(owner, pluginID string) (Host, error) { mu.Lock() fn := scopedHostFactory mu.Unlock() if fn == nil { return nil, ErrNotLinked } if owner == "" || pluginID == "" { return nil, errors.New("extension: scoped host requires owner and pluginID") } h := fn(owner, pluginID) if h == nil { return nil, errors.New("extension: scoped host factory returned nil") } return h, nil } // Main 启动带有已登记扩展的服务。 func Main(opt Options) error { mu.Lock() fn := runner mu.Unlock() if fn == nil { return ErrNotLinked } return fn(opt) }