feat(TASK-039): wire hooks into bundle installs and reconcile builtin hook dirs

Bundles: RemoteLayout.hooks + scan/is_empty/plan routing into hooks_dir,
a functional --filter hooks arm replacing the inert interim arm,
TopCategory::Hooks provenance rows, and per-file 'bundle defines hook
script' summary lines (visibility only, no approval prompt).

Reconciliation: new builtin_manifest module — a hidden per-directory
.builtin-manifest records the files the builtin installer actually owns
(written this run, previously owned, or pending failed deletions);
deletion is strictly manifest-minus-embed so user-created files in the
same directory are never touched. Wired into install_builtin_agents for
agents/<name>/hooks/ and a greenfield minimal Role::install_builtin_role_hooks
for roles/hooks/.

Hardening: bundles are barred from shipping any path component that
aliases .builtin-manifest (Unicode case fold + trailing dot/space,
directories included); manifest entries reuse the bundle store's
is_safe_component rules; malformed manifests fail safe toward keeping
files; role-side reconcile failures degrade to a warning like the
agent side.
This commit is contained in:
Dark-Alex-17 committed 2026-09-16 13:44:35 -06:00
1 parent e62502c862
commit 42380d4b82
7 files changed
+893 -8

No files matched your search

+10
View File
@@ -965,6 +965,12 @@ network:
let hook_mixin = roles.join("hooks").join("notify.sbx-mixin.yaml");
fs::write(&role_mixin, "kind: mixin\n").unwrap();
fs::write(&hook_mixin, "kind: mixin\n").unwrap();
// The builtin installer's hidden manifest lives in the same dir but
// is not a mixin and must never be picked up by the flat scan.
let manifest = roles
.join("hooks")
.join(crate::config::builtin_manifest::BUILTIN_MANIFEST_FILE);
fs::write(&manifest, "notify.sh\n").unwrap();
let config_env = get_env_name("config_dir");
let roles_env = get_env_name("roles_dir");
@@ -998,6 +1004,10 @@ network:
found.contains(&hook_mixin),
"missing role hook mixin: {found:?}"
);
assert!(
!found.contains(&manifest),
"builtin manifest must not be scanned as a mixin: {found:?}"
);
let _ = fs::remove_dir_all(&root);
}