43 std::string
s =
p.string();
45 out.reserve(
s.size() + 2);
69 if (
pipe ==
nullptr) {
72 std::array<char, 4096>
buf{};
73 while (
fgets(
buf.data(),
static_cast<int>(
buf.size()),
pipe) !=
nullptr) {
86 const char*
home = std::getenv(
"HOME");
87 if ((
home ==
nullptr) ||
home[0] ==
'\0') {
89 home = (
pw !=
nullptr) ?
pw->pw_dir :
"/tmp";
91 return std::filesystem::path(
home) /
".entropic" /
"sandbox";
101 std::random_device
rd;
102 std::uniform_int_distribution<uint32_t>
dist;
105 return std::to_string(
static_cast<long>(
getpid())) +
"-" +
hex;
120 if (
kill(
static_cast<pid_t>(
pid), 0) == 0) {
return true; }
133 auto dash = name.find(
'-');
135 name.size() -
dash - 1 == 8;
142 }
catch (
const std::exception&) {
157 return std::filesystem::exists(
dir /
".git",
ec);
171 : project_dir_(
std::filesystem::
absolute(project_dir)),
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; }
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() {
259 if (!std::filesystem::exists(root,
ec)) {
return; }
260 for (
const auto& entry :
262 if (!entry.is_directory()) {
continue; }
263 std::string name = entry.path().filename().string();
264 if (name == session_id_) {
continue; }
269 std::filesystem::remove_all(entry.path(),
rec);
270 logger->info(
"Pruned stale session sandbox: {} (pid {} gone)",
271 entry.path().string(),
pid);
288bool SandboxManager::ensure_base_snapshot() {
289 std::lock_guard<std::mutex>
guard(base_mutex_);
290 if (base_ready_) {
return true; }
292 std::filesystem::create_directories(base_dir_,
ec);
293 bool ok = !
ec && snapshot_tree(project_dir_, base_dir_);
295 logger->error(
"Failed to materialize base snapshot at {}: {}",
301 logger->info(
"Base snapshot ready at {}", base_dir_.string());
314 const std::filesystem::path& source,
315 const std::filesystem::path& target) {
317 " && git ls-files --cached --others --exclude-standard";
320 logger->error(
"git ls-files failed in {}", source.string());
323 std::istringstream
iss(out);
325 while (std::getline(
iss,
rel)) {
326 if (
rel.empty()) {
continue; }
330 std::filesystem::create_directories(
dst.parent_path(),
ec);
331 std::filesystem::copy_file(
333 std::filesystem::copy_options::overwrite_existing,
ec);
335 logger->warn(
"copy {} -> {} failed: {}",
336 src.string(),
dst.string(),
ec.message());
351 const std::filesystem::path& source,
352 const std::filesystem::path& target) {
354 std::filesystem::copy(
356 std::filesystem::copy_options::recursive |
357 std::filesystem::copy_options::overwrite_existing |
358 std::filesystem::copy_options::copy_symlinks,
361 logger->error(
"plain copy {} -> {} failed: {}",
362 source.string(), target.string(),
ec.message());
376bool SandboxManager::snapshot_tree(
377 const std::filesystem::path& source,
378 const std::filesystem::path& target) {
379 if (!path_in_session_base(target)) {
380 logger->error(
"BLOCKED snapshot to path outside session base: {}",
399 const std::string& delegation_id,
401 auto target = session_base_ / (
"d-" + delegation_id);
407 bool path_ok = path_in_session_base(target);
408 bool base_ok = ensure_base_snapshot();
411 logger->error(
"BLOCKED sandbox path outside session base: {}",
418 std::filesystem::remove_all(target,
ec);
423 logger->info(
"Created sandbox: id={} path={} chained_from={}",
424 delegation_id, target.string(),
426 : std::string{
"(base)"});
440 const std::filesystem::path& base,
441 const std::filesystem::path&
head,
442 std::string& patch) {
443 std::string
cmd =
"git diff --no-index --binary --no-color "
447 logger->error(
"git diff failed to spawn");
452 logger->error(
"git diff exit={} output snippet='{}'",
467 if (!
p.empty() &&
p.back() !=
'/') {
p +=
'/'; }
469 if (
line.rfind(
p, 0) == 0) {
471 }
else if (!
p.empty() &&
p.front() ==
'/'
472 &&
line.rfind(
p.substr(1), 0) == 0) {
475 if (
n > 0) {
line.erase(0,
n); }
487 const std::filesystem::path& base,
488 const std::filesystem::path&
head) {
489 std::vector<std::filesystem::path>
changed;
490 std::string
cmd =
"git diff --no-index --name-only "
494 std::istringstream
iss(out);
496 auto base_s = base.string();
498 while (std::getline(
iss,
line)) {
499 if (
line.empty()) {
continue; }
516 if (!path_in_session_base(info.path)) {
517 logger->error(
"finalize_sandbox: path not in session base: {}",
523 res.head_dir = info.path;
528 logger->info(
"Finalized sandbox {}: {} files changed, "
530 info.delegation_id,
res.files_touched.size(),
542 safe_remove(info.path);
543 logger->info(
"Discarded sandbox {}: {}",
544 info.delegation_id, info.path.string());
561 const std::string& delegation_id,
562 const std::string& patch) {
569 logger->error(
"Refusing write_pending_patch: {} not in session base",
578 write_ok = std::fwrite(patch.data(), 1, patch.size(),
f)
583 logger->error(
"write_pending_patch failed for {} (mkdir={} open={} "
608 return session_base_;
619 auto n = id_seq_.fetch_add(1, std::memory_order_relaxed) + 1;
620 return prefix +
"_" + std::to_string(
n);
637 std::string session_key,
641 user_data_(user_data),
642 session_key_(
std::
move(session_key)),
644 if (swap_fn_ !=
nullptr) {
646 logger->info(
"Swapped tool dir to sandbox: {} (session='{}')",
657 if (swap_fn_ !=
nullptr) {
658 swap_fn_(session_key_, original_path_,
false, user_data_);
659 logger->info(
"Restored tool dir to: {} (session='{}')",
660 original_path_.string(), session_key_);
std::string output
Combined stdout+stderr (partial on timeout)
int exit_code
Shell status; 128+N if killed by signal N.
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::string next_delegation_id(const std::string &prefix)
Mint a sandbox id that no sibling delegation can collide with.
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, std::string session_key, const std::filesystem::path &sandbox_path, const std::filesystem::path &original_path)
Construct and swap directories.
void(*)(const std::string &session_key, const std::filesystem::path &path, bool entering, 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.
@ optional
optional: every escape asks the host's path approver
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 base_dir
Snapshot the diff is against.