mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
* feat(sandbox): unify command execution and enforcement * test(sandbox): make platform contracts portable * fix(sandbox): close execution lifecycle races Bound retained interactive output, preserve interruption state, and start workspace observation before process launch. Protect active runtime roots with cross-platform leases, capture transient protected-path creation events, and restore process attributes after PTY fallback. * fix(sandbox): restore strict macOS isolation Remove Seatbelt host-local network allowances after native validation showed localhost filters can reach host interfaces. Track process reap completion so retained sessions cannot signal a stale numeric PID.
144 lines
5.8 KiB
Go
144 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/agent"
|
|
"github.com/Gitlawb/zero/internal/execution"
|
|
"github.com/Gitlawb/zero/internal/hooks"
|
|
"github.com/Gitlawb/zero/internal/plugins"
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
// pluginActivation holds what plugin activation contributed to a bootstrap so the
|
|
// later dispatcher + skill wiring can consume it: the plugin hook definitions and
|
|
// the plugin skill search roots. The zero value (no plugins) still overlays the
|
|
// multi-root skill tool (primary + ~/.agents/skills) so discovery is identical
|
|
// with or without plugins. The embedded trustSkip reports whether the project
|
|
// plugin layer was dropped for an untrusted workspace, so the caller can fold it
|
|
// into the single combined trust notice.
|
|
type pluginActivation struct {
|
|
hooks []hooks.Definition
|
|
skillRoots []string
|
|
trustSkip
|
|
}
|
|
|
|
// activatePlugins loads the workspace's plugins and makes their declared
|
|
// extensions live: plugin tools are registered into registry here, and the
|
|
// returned activation carries the plugin hooks + skill roots for the caller to
|
|
// fold into the hook dispatcher and the skill tool.
|
|
//
|
|
// It fails OPEN: any load error (or a malformed plugin) is surfaced as a warning
|
|
// on stderr and otherwise skipped, so a broken plugin can never wedge startup —
|
|
// mirroring how newHookDispatcher and skills.Load tolerate bad input. That
|
|
// fail-OPEN handling of MALFORMED plugins is separate from the trust gate below.
|
|
//
|
|
// The trust check lives here, inside the chokepoint, so no caller can bypass it:
|
|
// the project plugin root is dropped (ExcludeProject) whenever trustRoot is empty,
|
|
// the trust store cannot be read, or the workspace is not trusted (fail-closed).
|
|
// trustRoot is the original launch directory (resolved before any --worktree
|
|
// reassignment). The returned activation carries the skip report so the caller can
|
|
// emit one combined notice; the notice itself is NOT emitted here.
|
|
func activatePlugins(workspaceRoot string, registry *tools.Registry, deps appDeps, stderr io.Writer, trustRoot string, runners ...*execution.Runner) pluginActivation {
|
|
excludeProject, trustCheckErrored := resolveTrust(trustRoot)
|
|
skip := trustSkip{
|
|
excludedProjectConfig: excludeProject && projectPluginsDirExists(workspaceRoot),
|
|
trustCheckErrored: trustCheckErrored,
|
|
}
|
|
|
|
loaded, err := deps.loadPlugins(plugins.LoadOptions{Cwd: workspaceRoot, ExcludeProject: excludeProject})
|
|
if err != nil {
|
|
writePluginActivationWarning(stderr, "failed to load plugins: "+err.Error())
|
|
// Still overlay multi-root discovery (primary + agents) so a plugin load
|
|
// failure cannot hide shared skills.
|
|
registry.Register(plugins.NewSkillTool(deps.skillsDir(), nil))
|
|
return pluginActivation{trustSkip: skip}
|
|
}
|
|
|
|
// Load fails OPEN per plugin: a malformed manifest is recorded as a diagnostic
|
|
// and the plugin is dropped rather than aborting the whole load. Surface those
|
|
// diagnostics so a skipped plugin is never silent.
|
|
for _, diagnostic := range loaded.Diagnostics {
|
|
writePluginActivationWarning(stderr, formatLoadDiagnostic(diagnostic))
|
|
}
|
|
|
|
var runner *execution.Runner
|
|
if len(runners) > 0 {
|
|
runner = runners[0]
|
|
}
|
|
result := plugins.Activate(registry, loaded.Plugins, plugins.ActivateOptions{Cwd: workspaceRoot, Execution: runner})
|
|
for _, warning := range result.Warnings {
|
|
writePluginActivationWarning(stderr, warning)
|
|
}
|
|
|
|
// Always overlay the multi-root skill tool so discovery is identical with or
|
|
// without plugins: primary DefaultDir + optional ~/.agents/skills + plugin
|
|
// roots (earlier wins). With empty SkillRoots and no agents dir this is
|
|
// byte-equivalent to the core single-dir skill surface.
|
|
registry.Register(plugins.NewSkillTool(deps.skillsDir(), result.SkillRoots))
|
|
|
|
return pluginActivation{hooks: result.Hooks, skillRoots: result.SkillRoots, trustSkip: skip}
|
|
}
|
|
|
|
// projectPluginsDirExists reports whether a ./.zero/plugins directory is present
|
|
// under workspaceRoot, so the caller only notices about config it actually
|
|
// skipped.
|
|
func projectPluginsDirExists(workspaceRoot string) bool {
|
|
if workspaceRoot == "" {
|
|
return false
|
|
}
|
|
info, err := os.Stat(filepath.Join(workspaceRoot, ".zero", "plugins"))
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
// skillInfos resolves the reusable skills the model can load via the skill tool —
|
|
// primary dir + optional ~/.agents/skills + plugin skill roots, the same set the
|
|
// multi-root skill tool resolves against — as plain data for the agent's system
|
|
// prompt. It returns nil when no skills are installed, so a skill-less run leaves
|
|
// the prompt byte-identical.
|
|
func (a pluginActivation) skillInfos(defaultDir string) []agent.SkillInfo {
|
|
merged, _ := plugins.MergedSkills(defaultDir, a.skillRoots)
|
|
if len(merged) == 0 {
|
|
return nil
|
|
}
|
|
infos := make([]agent.SkillInfo, 0, len(merged))
|
|
for _, skill := range merged {
|
|
name := strings.TrimSpace(skill.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
infos = append(infos, agent.SkillInfo{Name: name, Description: strings.TrimSpace(skill.Description)})
|
|
}
|
|
return infos
|
|
}
|
|
|
|
// formatLoadDiagnostic renders a plugin load diagnostic into a single warning
|
|
// line, prefixing the diagnostic kind and appending the most specific locator
|
|
// available (manifest path, then plugin path, then plugin ID) so an operator can
|
|
// locate the offending plugin even when the manifest path is unknown.
|
|
func formatLoadDiagnostic(diagnostic plugins.Diagnostic) string {
|
|
message := fmt.Sprintf("[%s] %s", diagnostic.Kind, diagnostic.Message)
|
|
locator := diagnostic.ManifestPath
|
|
if locator == "" {
|
|
locator = diagnostic.PluginPath
|
|
}
|
|
if locator == "" {
|
|
locator = diagnostic.PluginID
|
|
}
|
|
if locator != "" {
|
|
message += " (" + locator + ")"
|
|
}
|
|
return message
|
|
}
|
|
|
|
func writePluginActivationWarning(stderr io.Writer, message string) {
|
|
if stderr == nil {
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(stderr, "[zero] WARNING: plugin activation: %s\n", message)
|
|
}
|