Let compiled plugins prepare startup after host defaults merge

Why: RDM's sandbox policy must run after HostOptions are merged and be able to refuse startup when bwrap is missing.
Evidence: go test ./pkg/extension/ ./cmd/agent-system/ -count=1 passed on this tree (SELF).
Impact: InProc.Prepare and PrepareCompiled; serve applies plugin rewrites before app.Run.
Refs: feature/svc-plugin-host-options
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 15:15:16 +00:00
co-authored by Cursor
parent dee7a9e845
commit 4c9743ae42
4 changed files with 58 additions and 3 deletions
+13 -3
View File
@@ -120,13 +120,23 @@ func main() {
if !cliSet["execBackend"] && execBackendResolved == "" {
execBackendResolved = pluginDefaults.ExecBackend
}
extOpt := extension.Options{
Addr: resolved.Addr, DataDir: resolved.DataDir, PluginsDir: resolved.PluginsDir,
DisableUploadIndexing: pluginDefaults.DisableUploadIndexing,
DisableConvCardMirror: pluginDefaults.DisableConvCardMirror,
DisableExecTools: disableExec, ExecBackend: execBackendResolved,
}
if err := extension.PrepareCompiled(&extOpt); err != nil {
fmt.Fprintln(os.Stderr, "agent-system:", err)
os.Exit(1)
}
if err := app.Run(app.Options{
Addr: resolved.Addr, DataDir: resolved.DataDir, PluginsDir: resolved.PluginsDir, ForceMock: resolved.Mock,
StorageDSN: resolved.StorageDSN, ArtifactsDir: resolved.ArtifactsDir, AuthMode: resolved.AuthMode,
InstanceID: resolved.Instance, InsecureListen: resolved.InsecureListen, AllowedHosts: resolved.AllowedHosts(),
DisableUploadIndexing: pluginDefaults.DisableUploadIndexing,
DisableConvCardMirror: pluginDefaults.DisableConvCardMirror,
DisableExecTools: disableExec, ExecBackend: execBackendResolved, ExecDepsDir: resolved.ExecDepsDir,
DisableUploadIndexing: extOpt.DisableUploadIndexing,
DisableConvCardMirror: extOpt.DisableConvCardMirror,
DisableExecTools: extOpt.DisableExecTools, ExecBackend: extOpt.ExecBackend, ExecDepsDir: resolved.ExecDepsDir,
ExecWorkspace: resolved.ExecWorkspace,
Tier: resolved.Tier,
PGPool: store.PoolOptions{
+18
View File
@@ -184,6 +184,8 @@ type InProc struct {
HostOptions func() Options
// CLI 可选。组合二进制在基座 serve 旗标上挂接插件专属旗标(如 bootstrap-admin)。
CLI *CLIHook
// Prepare 可选。基座合并 HostOptions 之后、进入服务之前调用,可改写 opt 或拒绝启动。
Prepare func(opt *Options) error
}
// Options 是下游自有二进制的启动参数,字段与 agent-system serve 对齐。
@@ -277,6 +279,22 @@ func RunCLIHooks(opt Options) (bool, error) {
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()
+26
View File
@@ -40,6 +40,32 @@ func TestMergeHostOptionsORAndExecConflict(t *testing.T) {
}
}
func TestPrepareCompiledCanRejectStartup(t *testing.T) {
ResetCompiled()
t.Cleanup(ResetCompiled)
RegisterCompiled(InProc{
ID: "rdm",
Prepare: func(opt *Options) error {
if opt.ExecBackend == "sandbox" {
return errors.New("no bwrap")
}
opt.DisableExecTools = true
return nil
},
})
opt := Options{ExecBackend: "sandbox"}
if err := PrepareCompiled(&opt); err == nil {
t.Fatal("expected prepare error")
}
opt.ExecBackend = "direct"
if err := PrepareCompiled(&opt); err != nil {
t.Fatal(err)
}
if !opt.DisableExecTools {
t.Fatal("prepare should be able to rewrite options")
}
}
func TestCLIHookHandlesBeforeServe(t *testing.T) {
ResetCompiled()
t.Cleanup(ResetCompiled)
+1
View File
@@ -34,6 +34,7 @@ MergeHostOptions
NewHostClient
NewScopedHost
Options
PrepareCompiled
ProjectHost
ProjectHost.EnsureProject
ProjectHost.OwnedProject