42std::string shell_quote(
const std::filesystem::path& p) {
43 std::string s = p.string();
45 out.reserve(s.size() + 2);
66int run_capture(
const std::string& cmd, std::string& output) {
68 FILE* pipe = popen(cmd.c_str(),
"r");
69 if (pipe ==
nullptr) {
72 std::array<char, 4096> buf{};
73 while (fgets(buf.data(),
static_cast<int>(buf.size()), pipe) !=
nullptr) {
85std::filesystem::path sandbox_root() {
86 const char* home = std::getenv(
"HOME");
87 if ((home ==
nullptr) || home[0] ==
'\0') {
88 struct passwd* pw = getpwuid(getuid());
89 home = (pw !=
nullptr) ? pw->pw_dir :
"/tmp";
91 return std::filesystem::path(home) /
".entropic" /
"sandbox";
100std::string make_session_id() {
101 std::random_device rd;
102 std::uniform_int_distribution<uint32_t> dist;
104 std::snprintf(hex,
sizeof(hex),
"%08x", dist(rd));
105 return std::to_string(
static_cast<long>(getpid())) +
"-" + hex;
119bool pid_is_alive(
long pid) {
120 if (kill(
static_cast<pid_t
>(pid), 0) == 0) {
return true; }
121 return errno != ESRCH;
132bool parse_session_pid(
const std::string& name,
long& pid) {
133 auto dash = name.find(
'-');
134 bool shape_ok = dash != std::string::npos && dash != 0 &&
135 name.size() - dash - 1 == 8;
136 if (!shape_ok) {
return false; }
140 pid = std::stol(name.substr(0, dash), &consumed);
141 parsed = (consumed == dash);
142 }
catch (
const std::exception&) {
155bool is_git_repo(
const std::filesystem::path& dir) {
157 return std::filesystem::exists(dir /
".git", ec);
171 : project_dir_(std::filesystem::absolute(project_dir)),
172 session_id_(make_session_id()),
173 session_base_(sandbox_root() / session_id_),
174 base_dir_(session_base_ /
"base") {
175 logger->info(
"SandboxManager: project={} session={} base={}",
176 project_dir_.string(), session_id_,
177 session_base_.string());
178 prune_stale_sessions();
180 std::filesystem::create_directories(session_base_, ec);
182 logger->error(
"Failed to create session base {}: {}",
183 session_base_.string(), ec.message());
193 safe_remove(session_base_);
194 logger->info(
"Session sandbox cleanup: {}", session_base_.string());
210bool SandboxManager::path_in_session_base(
211 const std::filesystem::path& p)
const {
212 if (session_base_.empty() || p.empty()) {
return false; }
213 auto norm = std::filesystem::absolute(p).lexically_normal();
214 auto base = session_base_.lexically_normal();
215 auto rel = norm.lexically_relative(base);
216 if (rel.empty()) {
return false; }
217 auto first = rel.begin();
218 return first != rel.end() && first->string() !=
"..";
227void SandboxManager::safe_remove(
const std::filesystem::path& p) {
228 if (p.empty()) {
return; }
229 if (p == session_base_) {
231 std::filesystem::remove_all(p, ec);
233 logger->warn(
"remove_all({}) failed: {}",
234 p.string(), ec.message());
238 if (!path_in_session_base(p)) {
239 logger->error(
"BLOCKED remove outside session base: path='{}' "
241 p.string(), session_base_.string());
245 std::filesystem::remove_all(p, ec);
247 logger->warn(
"remove_all({}) failed: {}", p.string(), ec.message());
256void SandboxManager::prune_stale_sessions() {
257 auto root = sandbox_root();
259 if (!std::filesystem::exists(root, ec)) {
return; }
260 for (
const auto& entry :
261 std::filesystem::directory_iterator(root, ec)) {
262 if (!entry.is_directory()) {
continue; }
263 std::string name = entry.path().filename().string();
264 if (name == session_id_) {
continue; }
266 if (!parse_session_pid(name, pid)) {
continue; }
267 if (pid_is_alive(pid)) {
continue; }
269 std::filesystem::remove_all(entry.path(), rec);
270 logger->info(
"Pruned stale session sandbox: {} (pid {} gone)",
271 entry.path().string(), pid);
281bool SandboxManager::ensure_base_snapshot() {
282 if (base_ready_) {
return true; }
284 std::filesystem::create_directories(base_dir_, ec);
285 bool ok = !ec && snapshot_tree(project_dir_, base_dir_);
287 logger->error(
"Failed to materialize base snapshot at {}: {}",
289 ec ? ec.message() : std::string{
"snapshot_tree failed"});
293 logger->info(
"Base snapshot ready at {}", base_dir_.string());
306 const std::filesystem::path& source,
307 const std::filesystem::path& target) {
308 std::string list_cmd =
"cd " + shell_quote(source) +
309 " && git ls-files --cached --others --exclude-standard";
311 if (run_capture(list_cmd, out) != 0) {
312 logger->error(
"git ls-files failed in {}", source.string());
315 std::istringstream iss(out);
317 while (std::getline(iss, rel)) {
318 if (rel.empty()) {
continue; }
319 auto src = source / rel;
320 auto dst = target / rel;
322 std::filesystem::create_directories(dst.parent_path(), ec);
323 std::filesystem::copy_file(
325 std::filesystem::copy_options::overwrite_existing, ec);
327 logger->warn(
"copy {} -> {} failed: {}",
328 src.string(), dst.string(), ec.message());
343 const std::filesystem::path& source,
344 const std::filesystem::path& target) {
346 std::filesystem::copy(
348 std::filesystem::copy_options::recursive |
349 std::filesystem::copy_options::overwrite_existing |
350 std::filesystem::copy_options::copy_symlinks,
353 logger->error(
"plain copy {} -> {} failed: {}",
354 source.string(), target.string(), ec.message());
368bool SandboxManager::snapshot_tree(
369 const std::filesystem::path& source,
370 const std::filesystem::path& target) {
371 if (!path_in_session_base(target)) {
372 logger->error(
"BLOCKED snapshot to path outside session base: {}",
376 if (is_git_repo(source)) {
391 const std::string& delegation_id,
392 std::optional<SandboxInfo> chain_from) {
393 auto target = session_base_ / (
"d-" + delegation_id);
394 auto source = chain_from.has_value() ? chain_from->path : base_dir_;
395 auto effective_base = chain_from.has_value()
396 ? chain_from->base_dir
399 bool path_ok = path_in_session_base(target);
400 bool base_ok = ensure_base_snapshot();
401 if (!path_ok || !base_ok) {
403 logger->error(
"BLOCKED sandbox path outside session base: {}",
410 std::filesystem::remove_all(target, ec);
415 logger->info(
"Created sandbox: id={} path={} chained_from={}",
416 delegation_id, target.string(),
417 chain_from.has_value() ? chain_from->path.string()
418 : std::string{
"(base)"});
419 return SandboxInfo{target, delegation_id, effective_base};
432 const std::filesystem::path& base,
433 const std::filesystem::path& head,
434 std::string& patch) {
435 std::string cmd =
"git diff --no-index --binary --no-color "
436 + shell_quote(base) +
" " + shell_quote(head);
437 int status = run_capture(cmd, patch);
439 logger->error(
"git diff failed to spawn");
442 int exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
443 if (exit_code != 0 && exit_code != 1) {
444 logger->error(
"git diff exit={} output snippet='{}'",
445 exit_code, patch.substr(0, 256));
459 if (!p.empty() && p.back() !=
'/') { p +=
'/'; }
461 if (line.rfind(p, 0) == 0) {
463 }
else if (!p.empty() && p.front() ==
'/'
464 && line.rfind(p.substr(1), 0) == 0) {
467 if (n > 0) { line.erase(0, n); }
479 const std::filesystem::path& base,
480 const std::filesystem::path& head) {
481 std::vector<std::filesystem::path> changed;
482 std::string cmd =
"git diff --no-index --name-only "
483 + shell_quote(base) +
" " + shell_quote(head);
485 run_capture(cmd, out);
486 std::istringstream iss(out);
488 auto base_s = base.string();
489 auto head_s = head.string();
490 while (std::getline(iss, line)) {
491 if (line.empty()) {
continue; }
494 changed.emplace_back(line);
508 if (!path_in_session_base(info.path)) {
509 logger->error(
"finalize_sandbox: path not in session base: {}",
520 logger->info(
"Finalized sandbox {}: {} files changed, "
534 safe_remove(info.path);
535 logger->info(
"Discarded sandbox {}: {}",
536 info.delegation_id, info.path.string());
553 const std::string& delegation_id,
554 const std::string& patch) {
555 auto pending_dir = session_base_ /
"pending";
556 auto out_path = pending_dir / (delegation_id +
".patch");
558 bool contained = path_in_session_base(pending_dir)
559 && path_in_session_base(out_path);
561 logger->error(
"Refusing write_pending_patch: {} not in session base",
565 std::filesystem::create_directories(pending_dir, ec);
567 FILE* f = dir_ok ? std::fopen(out_path.c_str(),
"wb") :
nullptr;
568 bool write_ok =
false;
570 write_ok = std::fwrite(patch.data(), 1, patch.size(), f)
574 if (!dir_ok || f ==
nullptr || !write_ok) {
575 logger->error(
"write_pending_patch failed for {} (mkdir={} open={} "
576 "write={})", out_path.string(), dir_ok,
577 f !=
nullptr, write_ok);
600 return session_base_;
617 const std::filesystem::path& sandbox_path,
618 const std::filesystem::path& original_path)
620 user_data_(user_data),
621 original_path_(original_path) {
622 if (swap_fn_ !=
nullptr) {
623 swap_fn_(sandbox_path, user_data_);
624 logger->info(
"Swapped tool dir to sandbox: {}",
625 sandbox_path.string());
635 if (swap_fn_ !=
nullptr) {
636 swap_fn_(original_path_, user_data_);
637 logger->info(
"Restored tool dir to: {}", original_path_.string());
const std::filesystem::path & session_base() const
Get this session's sandbox base directory.
void discard_sandbox(const SandboxInfo &info)
Remove a sandbox directory.
SandboxManager(const std::filesystem::path &project_dir)
Construct with the user's project directory.
std::optional< SandboxResult > finalize_sandbox(const SandboxInfo &info)
Produce the final patch artifact for a sandbox.
std::optional< SandboxInfo > create_sandbox(const std::string &delegation_id, std::optional< SandboxInfo > chain_from=std::nullopt)
Create a new delegation sandbox.
~SandboxManager()
Remove this session's sandbox tree.
std::optional< std::filesystem::path > write_pending_patch(const std::string &delegation_id, const std::string &patch)
Write a patch to the session's pending/ directory.
const std::filesystem::path & project_dir() const
Get the project directory this manager snapshots from.
~ScopedSandbox()
Restore the original directory.
ScopedSandbox(SwapDirFn swap_fn, void *user_data, const std::filesystem::path &sandbox_path, const std::filesystem::path &original_path)
Construct and swap directories.
void(*)(const std::filesystem::path &path, void *user_data) SwapDirFn
Callback type for directory swapping.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
@ ok
Tool dispatched, returned non-empty content.
static bool snapshot_git_project(const std::filesystem::path &source, const std::filesystem::path &target)
Copy git-tracked + untracked-not-ignored files from a project.
static std::vector< std::filesystem::path > diff_files(const std::filesystem::path &base, const std::filesystem::path &head)
List files differing between base and head via diff -rq-style logic.
static bool snapshot_plain_copy(const std::filesystem::path &source, const std::filesystem::path &target)
Recursive copy for non-git sources (or sandbox-to-sandbox chains).
static void trim_path_prefix(std::string &line, std::string p)
Strip a directory prefix p from the front of line.
static bool capture_diff(const std::filesystem::path &base, const std::filesystem::path &head, std::string &patch)
Run git diff --no-index between base and head, capture patch.
Filesystem-based sandbox isolation for delegations.
Identifies one delegation's sandbox directory.
Final artifact emitted by a finalized sandbox.
std::filesystem::path head_dir
Final sandbox state.
std::vector< std::filesystem::path > files_touched
Relative paths that changed.
std::string patch
Unified diff text.
std::filesystem::path base_dir
Snapshot the diff is against.