Repositories / jai.git

jai.git

Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git

Branch

bail if explicitly provided configuration file does not exist.

Author
David Mazieres <dm@uun.org>
Date
2026-03-15 21:32:04 -0700
Commit
d83f07c5a894baf34ab614a9a032dceda2206b41
fs.cc
index 57df449..2f044af 100644
--- a/fs.cc
+++ b/fs.cc
@@ -391,7 +391,7 @@ try_read_file(int dfd, path file)
   Fd fdholder;
   int fd = dfd;
   if (!file.empty()) {
-    fdholder = openat(fd, file.c_str(), O_RDONLY, O_CLOEXEC);
+    fdholder = openat(fd, file.c_str(), O_RDONLY | O_CLOEXEC);
     if (!fdholder)
       return std::unexpected(
           std::system_error(errno, std::system_category(), fdpath(fd, file)));
jai.1.md
index 5e3b637..eade22a 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -8,31 +8,32 @@ jai - Jail an AI agent
 
 # SYNOPSIS
 
-`jai` [`-d` *dir*] [`-h` *name*] [`-D`] *cmd* [*arg* ...] \
-`jai` \
+`jai` [*option*]...  *cmd* [*arg*]... \
+`jai` [`--casual`] [*option*]... \
 `jai` `-u`
 
 # DESCRIPTION
 
 `jai` is a super lightweight sandbox for AI agents requiring almost no
-configuration.  It provides only casual security, so is not a
+configuration.  By default it provides casual security, so is not a
 substitute for using a proper container to confine agents.  However,
-it is a great alternative to using no protection when you are thinking
-of giving an agent full control of your account and all its files.
-Compared to the latter, `jai` can reduce the blast radius should
-things go wrong.
+it is a great alternative to using no protection at all when you are
+thinking of giving an agent full control of your account and all its
+files.  Compared to the latter, `jai` can reduce the blast radius
+should things go wrong.
 
-Before using `jai`, if your home directory is on NFS, make
-`$HOME/.jai` a symbolic link to a directory you own on a local file
-system supporting extended attributes.
+Before using `jai`, if your home directory is on NFS and you want to
+use casual mode, make `$HOME/.jai` a symbolic link to a directory you
+own on a local file system that supports extended attributes.
+Otherwise, you may only be able to use strict mode.
 
 To get started, run `jai` with no arguments.  (If it is not setuid
 root, you will need to run `sudo jai`.)  This will create an overlay
-mount of your home directory in `/run/jai/$USER/sandboxed-home`.
-Change to that directory and delete any sensitive files you don't want
-your agent to have access to.  (Start with deleting a file you don't
-care about, and verify that it only disappears from the sandbox, not
-from your real home directory.)
+mount of your home directory in `/run/jai/$USER/default.home`.  Change
+to that directory and delete any sensitive files you don't want your
+agent to have access to.  (Start with deleting a file you don't care
+about, and verify that it only disappears from the sandbox, not from
+your real home directory.)
 
 Once you are satisfied with the sandbox, go to a project directory you
 own and run `jai $SHELL`.  That will let you explore the sandboxed
@@ -60,12 +61,14 @@ flag.
 
 # OPTIONS
 
-`-d` *dir*
+`-d` *dir*, `--dir=`*dir*
 : Grant full access to directory *dir* and everything below in the
   jail.  You must own the directory.  You can supply this option
   multiple times.
 
-`-D`
+`-C` *file*, `--conf=`*file*
+
+`-D`, `--nocwd`
 : By default, `jai` grants access to the current working directory
   even if it is not specified with `-d`.  This option suppresses that
   behavior.  If you run with `-D` and no `-d` options, your entire
jai.cc
index 73ec912..c1c4957 100644
--- a/jai.cc
+++ b/jai.cc
@@ -107,14 +107,13 @@ struct Config {
 bool
 Config::parse_config_file(path file, Options *opts)
 {
-  auto ld = homepath_ / file;
-  if (auto [_it, ok] = config_loop_detect_.insert(ld); !ok) {
-    warn("{}: configuration loop", file.string());
-    return false;
-  }
+  bool slash = std::ranges::distance(file.begin(), file.end()) > 1;
+  auto ld = (slash ? cwd() : homepath_ / ".jai") / file;
+  if (auto [_it, ok] = config_loop_detect_.insert(ld); !ok)
+    err<Options::Error>("configuration loop");
   Defer _clear{[this, ld = std::move(ld)] { config_loop_detect_.erase(ld); }};
 
-  auto r = try_read_file(home_jai(), file);
+  auto r = try_read_file(slash ? AT_FDCWD : home_jai(), file);
   if (!r) {
     if (r.error().code() == std::errc::no_such_file_or_directory)
       return false;
@@ -189,7 +188,8 @@ Try running "sudo systemd-sysusers".)",
 
   // Paranoia about ptrace, because we will drop privileges to access
   // the file system as the user.
-  prctl(PR_SET_DUMPABLE, 0);
+  if (prctl(PR_SET_DUMPABLE, 0) == -1)
+    syserr("prctl(PR_SET_DUMPABLE, 0)");
 
   old_umask_ = umask(0);
 }
@@ -361,7 +361,7 @@ Fd
 Config::make_idmap_ns()
 {
   pid_t pid{-1};
-  Defer _reap([pid] {
+  Defer _reap([&pid] {
     if (pid > 0) {
       while (waitpid(pid, nullptr, 0) == -1 && errno == EINTR)
         ;
@@ -627,7 +627,7 @@ Config::exec(int nsfd, char **argv)
 
   if (auto pid = xfork()) {
     close(nsfd);
-    int status;
+    int status = -1;
     while (waitpid(pid, &status, 0) == -1 && errno == EINTR)
       ;
     // unmount();
@@ -794,10 +794,13 @@ default: CMD.conf or default.conf if CMD.conf does not exist)",
     return;
   }
 
-  if (opt_C.empty() && !cmd.empty() && conf.name_ok(cmd[0]))
-    opt_C = std::format("{}.conf", cmd[0]);
-  if ((opt_C.empty() || !conf.parse_config_file(opt_C)) &&
-      !conf.parse_config_file("default.conf")) {
+  if (!opt_C.empty()) {
+    if (!conf.parse_config_file(opt_C))
+      err("{}: no such configuration file", opt_C.string());
+  }
+  else if ((cmd.empty() || !conf.name_ok(cmd[0]) ||
+            !conf.parse_config_file(std::format("{}.conf", cmd[0]))) &&
+           !conf.parse_config_file("default.conf")) {
     conf.make_default_conf();
     conf.parse_config_file("default.conf");
   }