* 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.
* feat(skills): discover shared ~/.agents/skills with multi-root skill loading
Add ~/.agents/skills as a read-only global discovery root after the
primary
Zero skills dir and before plugin roots. Unify runtime and CLI discovery
on
one multi-root path (DiscoveryRoots / LoadFromRoots / GlobalRoots) and
always
register the multi-root skill tool so bare runs and plugin runs share
the
same load surface.
Install/remove/lock still target only the primary Zero skills directory.
Fixes#610
* docs(skills): note lock stays on the primary write root
Document that install/remove/lock target only the primary Zero skills
directory, matching multi-root discovery write-root isolation.
* fix(skills): bubble primary root load errors from LoadFromRoots
Return non-missing failures from the first non-empty skills root so a
broken primary dir is not reported as an empty skill list. Keep fail-open
behavior for optional roots (~/.agents/skills, plugins).
* fix(skills): treat non-directory skills root as error on Windows
Windows maps ENOTDIR to ErrNotExist, so ReadDir on a regular file looked
like a missing skills dir. Reclassify existing non-directories and assert
the portable load behavior in tests.
* fix(plugins): resolve relative executable paths against plugin root
When a plugin registers a tool or a hook command as a relative path (e.g. ./tools/helper.sh), running it in the caller's workspace CWD allows local execution hijacking by placing a malicious script at that path in the untrusted workspace. Update expandPluginRootPath to resolve relative executable paths containing path separators to absolute paths inside the plugin's install directory.
* fix(plugins): remove ineffectual permission initializer in toolSafety
The initial assignment to permission was always overwritten by one of
the switch arms below it, including a case that just reassigned the
same PermissionPrompt value the initializer already held. Flagged by
ineffassign in CI.
* 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