mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* stage 09: finish plugin wiring (tools/hooks/skills)
Activate resolved plugin manifests so their declared tools, hooks, and
skills take effect at runtime instead of being parsed-but-unconsumed.
- internal/plugins/activate.go: Activate() registers each plugin tool into
the tools.Registry via a command-runner adapter (JSON args on stdin,
stdout/exit -> tools.Result, ${AGENT_PLUGIN_ROOT} expansion + env),
builds hooks.Definition values for plugin hooks, and exposes plugin
skill search roots. Permission maps allow/prompt/deny -> tools.Safety;
allow only survives load-time clamping when manifest auto-approval is
enabled, so mutating plugin tools are not auto-approved by default. A
plugin-aware skill tool merges the default skills dir with plugin roots
(recording duplicate names, never crashing). Malformed plugins/extensions
are skipped with a warning; activation order is deterministic.
- internal/cli: activatePlugins() invokes activation in both bootstrap
paths (interactive TUI in app.go and one-shot exec in exec.go), after
core/specialist/MCP registration. newHookDispatcherWithExtra folds plugin
hooks into the dispatcher's hook set. Fails open: a bad plugin warns and
is skipped, never wedging startup.
- internal/plugins/plugins.go: update the stale 'not yet registered' note.
* stage 09: normalize plugin command path separators for Windows
expandPluginRoot kept the manifest's forward slashes, so a ${AGENT_PLUGIN_ROOT}/bin/x command became a mixed-separator path on Windows (C:\...\001/bin/x), failing the path tests. Added expandPluginRootPath (filepath.FromSlash on the placeholder-expanded executable path only; args keep their slashes since they may be URLs/flags) and used it for the tool and hook command paths.
* stage 09: address plugin wiring review
- surface per-plugin load diagnostics on stderr (not just top-level errors)
- skip plugin tools whose name collides with an already-registered tool
- resolve manifest-relative plugin skill paths against the plugin dir
* stage 09: fall back to plugin path/id when manifest path unknown in load diagnostic
57 lines
2.2 KiB
Go
57 lines
2.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"github.com/Gitlawb/zero/internal/hooks"
|
|
)
|
|
|
|
// newHookDispatcher builds the per-session hooks dispatcher for a workspace,
|
|
// merging user + project hooks.json and wiring the audit store. It fails OPEN:
|
|
// any load or setup error yields a nil dispatcher, which Dispatch treats as a
|
|
// no-op, so a malformed hooks config can never wedge tool execution. With no
|
|
// hooks configured the dispatcher selects nothing and runs no commands, so the
|
|
// hot path stays free of overhead until a user opts in via hooks.json.
|
|
func newHookDispatcher(workspaceRoot string) *hooks.Dispatcher {
|
|
return newHookDispatcherWithExtra(workspaceRoot, nil)
|
|
}
|
|
|
|
// newHookDispatcherWithExtra builds the dispatcher like newHookDispatcher but also
|
|
// folds plugin-activated hook definitions into the active hook set, so a plugin's
|
|
// declared hooks run alongside the user/project hooks.json hooks. Plugin hooks are
|
|
// appended after the configured hooks; their ids are plugin-namespaced (plugin
|
|
// id + hook name) so they never collide with hooks.json ids. A nil/empty extra
|
|
// slice is byte-equivalent to newHookDispatcher.
|
|
func newHookDispatcherWithExtra(workspaceRoot string, extra []hooks.Definition) *hooks.Dispatcher {
|
|
loaded, err := hooks.LoadConfig(hooks.LoadOptions{Cwd: workspaceRoot})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var audit *hooks.AuditStore
|
|
if store, err := hooks.NewAuditStore(hooks.AuditStoreOptions{}); err == nil {
|
|
audit = store
|
|
}
|
|
config := loaded.Config
|
|
if len(extra) > 0 {
|
|
// Plugin hooks only run when hooks are enabled overall; an explicit
|
|
// `enabled:false` in hooks.json still disables the whole hook surface.
|
|
merged := append([]hooks.Definition{}, config.Hooks...)
|
|
existing := make(map[string]bool, len(merged))
|
|
for _, hook := range merged {
|
|
existing[hook.ID] = true
|
|
}
|
|
for _, hook := range extra {
|
|
// A hooks.json hook with the same (namespaced) id wins, so an operator can
|
|
// still disable a plugin hook by id without the plugin re-enabling it.
|
|
if existing[hook.ID] {
|
|
continue
|
|
}
|
|
merged = append(merged, hook)
|
|
}
|
|
config.Hooks = merged
|
|
}
|
|
return hooks.NewDispatcher(hooks.DispatcherOptions{
|
|
Config: config,
|
|
Audit: audit,
|
|
Cwd: workspaceRoot,
|
|
})
|
|
}
|