feat(TASK-048): install polish, hooks-dir mixin scan, script and probe hardening

- one shared exec-bit predicate (Language-recognized extensions => 0755) used by
  every hook/agent installer; non-script files in hooks dirs stay non-executable
- mixin discovery scans the global hooks dir and each agent's hooks dir;
  .builtin-manifest is never scanned
- NTFS 8.3 short-name aliases rejected as builtin-manifest entries
- hook-script install announcements restricted to real hook locations
- COYOTE_HOOKS_DIR override warning compares canonicalized paths
- embedded assets decode via checked from_utf8 instead of from_utf8_unchecked
- log-events.sh defaults under ${XDG_STATE_HOME:-$HOME/.local/state}/coyote/hooks.log
  keeping 0600/symlink-refusal/secret-filter hardening; notify.sh tty fallback strips
  control bytes; both use UTC ISO-8601 timestamps
- payload files orphaned by a write failure are unlinked; stale coyote-hook-*.json
  payloads swept best-effort at startup
- sweep_removed_agent_hooks never follows symlinked agent dirs
- usage-probe-hooks.sh replaces bare sleeps with a bounded poll, re-verifies 0755 on
  restored scripts, and covers roles/agent hook locations
- TestConfigDirGuard hoisted into crate::testing
- .gitattributes pins LF line endings for bundled scripts

hooks is now a reserved bundle name.
This commit is contained in:
Dark-Alex-17 committed 2026-09-16 18:30:02 -06:00
1 parent e7bd75947c
commit 89a9398c28
16 files changed
+627 -353

No files matched your search

+5 -47
View File
@@ -1132,14 +1132,12 @@ mod integration_tests {
use crate::hooks::HookDef;
#[cfg(unix)]
use crate::supervisor::{JobHandle, JobResult, JobState, JobStatus, Supervisor, notification};
use crate::utils::{create_abort_signal, get_env_name, temp_file};
use crate::utils::{create_abort_signal, temp_file};
use parking_lot::Mutex;
use serial_test::serial;
use std::env;
use std::fs;
#[cfg(unix)]
use std::mem;
use std::time::{SystemTime, UNIX_EPOCH};
fn cmd_available(name: &str) -> bool {
which::which(name).is_ok()
@@ -1267,7 +1265,7 @@ nodes:
eprintln!("skipping: bash or python3 not available");
return;
}
let _guard = TestConfigDirGuard::new();
let _guard = TestConfigDirGuard::new("graph-executor-tests");
materialize_probe_agent(0.0);
let ws = TestWorkspace::new();
ws.write_script("dispatcher.sh", "#!/bin/bash\necho '{}'\n");
@@ -1681,7 +1679,7 @@ nodes:
eprintln!("skipping: python3 not available");
return;
}
let _guard = TestConfigDirGuard::new();
let _guard = TestConfigDirGuard::new("graph-executor-tests");
materialize_probe_agent(0.0);
let ws = TestWorkspace::new();
@@ -2364,47 +2362,7 @@ nodes:
assert_eq!(events[0].event, "job_completed");
}
struct TestConfigDirGuard {
key: String,
previous: Option<std::ffi::OsString>,
path: PathBuf,
}
impl TestConfigDirGuard {
fn new() -> Self {
let key = get_env_name("config_dir");
let previous = env::var_os(&key);
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = env::temp_dir().join(format!("coyote-graph-executor-tests-{unique}"));
fs::create_dir_all(&path).unwrap();
unsafe {
env::set_var(&key, &path);
}
Self {
key,
previous,
path,
}
}
}
impl Drop for TestConfigDirGuard {
fn drop(&mut self) {
if let Some(previous) = &self.previous {
unsafe {
env::set_var(&self.key, previous);
}
} else {
unsafe {
env::remove_var(&self.key);
}
}
let _ = fs::remove_dir_all(&self.path);
}
}
use crate::testing::TestConfigDirGuard;
const PROBE_AGENT: &str = "timeout-probe";
@@ -2654,7 +2612,7 @@ nodes:
eprintln!("skipping: bash or python3 not available");
return;
}
let _guard = TestConfigDirGuard::new();
let _guard = TestConfigDirGuard::new("graph-executor-tests");
materialize_probe_agent(5.0);
let ws = TestWorkspace::new();
// The abort must land after the worker tasks exist (their retirement