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:
7 files changed
+893
-8
No files matched your search
@@ -9290,6 +9290,53 @@ mod tests {
|
||||
assert!(agents_dir.join("architect").join("config.yaml").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn install_builtin_agents_reconciles_hooks_subdir_via_manifest() {
|
||||
use crate::config::builtin_manifest::BUILTIN_MANIFEST_FILE;
|
||||
|
||||
let _guard = TestConfigDirGuard::new();
|
||||
|
||||
Agent::install_builtin_agents(false).unwrap();
|
||||
|
||||
let agents_dir = paths::agents_data_dir();
|
||||
// Simulate an upgrade from an older install whose coder shipped a
|
||||
// hook script that the current embed no longer contains: the manifest
|
||||
// records it, so reconciliation removes it — while a user-created
|
||||
// script in the same directory (absent from the manifest) survives.
|
||||
let hooks_dir = agents_dir.join("coder").join("hooks");
|
||||
create_dir_all(&hooks_dir).unwrap();
|
||||
write(hooks_dir.join(BUILTIN_MANIFEST_FILE), "old-hook.sh\n").unwrap();
|
||||
write(hooks_dir.join("old-hook.sh"), "stale").unwrap();
|
||||
write(hooks_dir.join("user-hook.sh"), "user-owned").unwrap();
|
||||
// A custom agent outside the bundle is never reconciled, even with a
|
||||
// manifest present.
|
||||
let custom_hooks = agents_dir.join("myagent").join("hooks");
|
||||
create_dir_all(&custom_hooks).unwrap();
|
||||
write(custom_hooks.join(BUILTIN_MANIFEST_FILE), "custom.sh\n").unwrap();
|
||||
write(custom_hooks.join("custom.sh"), "keep").unwrap();
|
||||
|
||||
Agent::install_builtin_agents(false).unwrap();
|
||||
|
||||
assert!(
|
||||
!hooks_dir.join("old-hook.sh").exists(),
|
||||
"manifest-listed hook absent from the embed must be removed"
|
||||
);
|
||||
assert!(
|
||||
hooks_dir.join("user-hook.sh").exists(),
|
||||
"user-created hook files must survive reconciliation"
|
||||
);
|
||||
assert!(
|
||||
!hooks_dir.join(BUILTIN_MANIFEST_FILE).exists(),
|
||||
"an empty shipped set removes the manifest itself"
|
||||
);
|
||||
assert!(
|
||||
custom_hooks.join("custom.sh").exists(),
|
||||
"custom agents outside the bundle must survive reconciliation"
|
||||
);
|
||||
assert!(custom_hooks.join(BUILTIN_MANIFEST_FILE).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn install_builtin_skills_force_overwrites_only_with_force() {
|
||||
|
||||
Reference in new issue
Block a user