Repositories / jai.git

jai.git

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

Branch

add --init option

Author
David Mazieres <dm@uun.org>
Date
2026-03-18 23:13:45 -0700
Commit
00ff4e7f63b57b59592d1d0c28d0a9c3919b831c
default_conf.cc
index 31b62fb..1936811 100644
--- a/default_conf.cc
+++ b/default_conf.cc
@@ -8,9 +8,9 @@ const std::string jai_defaults =
 #     conf .defaults
 #
 # You can override settings in this file by editing it directly or by
-# adding configuration directives to default.conf or other <name>.conf
-# files after the `conf .defaults` line.  (Later lines override
-# previous ones in configuration files.)
+# appending configuration directives to default.conf or other
+# <name>.conf files after the `conf .defaults` line.  (Later lines
+# override previous ones in configuration files.)
 #
 # If you delete this file, jai will re-create it the next time it
 # runs.  You can also see the default contents of this file by running
@@ -125,5 +125,11 @@ unsetenv *_TOKEN
 )";
 
 extern const std::string default_conf =
-  R"(conf .defaults
+  R"(# The following line includes sensible defaults from the file
+# .defaults.  You can override these defaults by appending
+# configuration options to this file.  See the .defaults file or the
+# jai(1) man page for details.
+
+conf .defaults
+
 )";
jai.1.md
index c8cb99e..5c4ff88 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -8,12 +8,13 @@ jai - Jail an AI agent
 
 # SYNOPSIS
 
+`jai` `--init` \
 `jai` [*option*]...  [*cmd* [*arg*]...] \
 `jai` `-u`
 
 # DESCRIPTION
 
-`jai` is a super lightweight sandbox for AI agents requiring almost no
+`jai` is a super-lightweight sandbox for AI agents requiring almost no
 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 at all when you are
@@ -54,13 +55,13 @@ option.
 
 If you use casual mode and forget to export some directory that you
 updated in the jail, you will find changed files in
-`$HOME/.jai/default.changes`.  You can destroy the sandbox with `jai
--u`, move the changed files back into your home directory, and re-run
-`jai` with the appropriate `-d` flag.
+`$HOME/.jai/default.changes`.  You can destroy the jail with `jai -u`,
+move the changed files back into your home directory, and re-run `jai`
+with the appropriate `-d` flag.
 
-jai allows the use of multiple sandboxed home directories.  To use a
-home directory other than the default, just give it a name with the
-`-n` option and it will be created on demand.  When you specify a home
+jai allows the use of multiple jailed home directories.  To use a home
+directory other than the default, just give it a name with the `-n`
+option and it will be created on demand.  When you specify a home
 directory with `-n`, strict mode becomes the default (unless there is
 no unprivileged `jai` user on your system, in which case jai falls
 back to bare mode).  It is possible to have multiple home overlays by
@@ -108,6 +109,10 @@ environment before running the command.
 
 # OPTIONS
 
+`--init`
+: Create default configuration files and exit.  You should run this
+  first, before activating any jails.
+
 `-C` *file*, `--conf `*file*
 : Specifies the configuration file to read.  If *file* does not
   contain a `/`, the file is relative to `$HOME/.jai`.
@@ -143,22 +148,22 @@ environment before running the command.
 : Set jai's execution mode.  In casual mode, the user's home directory
   is made available as an overlay mount.  Casual mode protects against
   destruction of files outside of granted directories, but does not
-  protect confidentiality:  sandboxed code can read most files
-  accessible to the user.  You can hide specific files with the
-  `--mask` option or by deleting them under `/run/jai/$USER/*.home`,
-  but because casual mode makes everything readable by default, it
-  cannot protect all sensitive files.
+  protect confidentiality:  jailed code can read most files accessible
+  to the user.  You can hide specific files with the `--mask` option
+  or by deleting them under `/run/jai/$USER/*.home`, but because
+  casual mode makes everything readable by default, it cannot protect
+  all sensitive files.
 
     In strict mode, the user's home directory is replaced by an empty
-  directory (`$HOME/.jai/`*name*`.home`), and sandboxed code runs with
-  a different user id, `jai`.  Id-mapped mounts are used to map `jai`
-  to the invoking user in granted directories.  Strict mode is the
-  default when you name a sandbox (see `--name`), but not for the
-  default sandbox.
+  directory (`$HOME/.jai/`*name*`.home`), and jailed code runs with a
+  different user id, `jai`.  Id-mapped mounts are used to map `jai` to
+  the invoking user in granted directories.  Strict mode is the
+  default when you name a jail (see `--name`), but not for the default
+  sandbox.
 
     Bare mode uses an empty directory like strict mode, but runs with
   the invoking user's credentials.  It is inferior to strict mode, but
-  can be used for NFS-mounted home directories, since NFS does not
+  can be used for NFS-mounted home directories since NFS does not
   support id-mapped mounts.
 
 `-n` *name*, `--name` *name*
jai.cc
index fc7ea49..db1d272 100644
--- a/jai.cc
+++ b/jai.cc
@@ -85,7 +85,7 @@ struct Config {
   }
 
   int home();
-  int home_jai();
+  int home_jai(bool create = false);
   int storage();
   int run_jai();
   int run_jai_user();
@@ -205,7 +205,7 @@ GECOS field is not "{}")",
   }
 
   const char *jcd = getenv("JAI_CONFIG_DIR");
-  homejaipath_ = homejaipath_ / (jcd ? jcd : ".jai");
+  homejaipath_ = homepath_ / (jcd ? jcd : ".jai");
 
   // Paranoia about ptrace, because we will drop privileges to access
   // the file system as the user.
@@ -245,10 +245,23 @@ Config::check_user(int fd, std::string p, bool untrusted_ok)
 }
 
 int
-Config::home_jai()
+Config::home_jai(bool create)
 {
-  if (!home_jai_fd_)
-    home_jai_fd_ = ensure_udir(home(), homejaipath_);
+  if (!home_jai_fd_) {
+    if (create)
+      home_jai_fd_ = ensure_udir(home(), homejaipath_);
+    else if (Fd fd = openat(home(), homejaipath_.c_str(),
+                            O_RDONLY | O_DIRECTORY | O_CLOEXEC)) {
+      check_user(*fd);
+      home_jai_fd_ = std::move(fd);
+    }
+    else if (errno == ENOENT) {
+      err("{} does not exist; run {} --init to create it",
+          fdpath(home(), homejaipath_), prog.filename().string());
+    }
+    else
+      syserr("{}", fdpath(home(), homejaipath_));
+  }
   return *home_jai_fd_;
 }
 
@@ -601,10 +614,10 @@ Config::make_mnt_ns()
       err("{} should be empty in jail", kRunRoot);
     Fd source = clone_tree(*empty);
     xmnt_setattr(*source, mount_attr{
-                             .attr_set = MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID |
-                                         MOUNT_ATTR_NODEV,
-                             .propagation = MS_PRIVATE,
-                         });
+                              .attr_set = MOUNT_ATTR_RDONLY |
+                                          MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV,
+                              .propagation = MS_PRIVATE,
+                          });
     xmnt_move(*source, *target);
   }
 
@@ -1018,10 +1031,14 @@ do_main(int argc, char **argv)
   bool opt_u{};
   std::vector<path> opt_d;
   path opt_C = "";
+  bool opt_init{};
 
   auto opts = conf.opt_parser();
   // A few options not available in config files
   (*opts)("-u", [&] { opt_u = true; }, "Unmount sandboxed file systems");
+  (*opts)(
+      "--init", [&] { opt_init = true; },
+      "Create initial configuration files and exit");
   // Override inline conf to make CLI idempotent
   (*opts)(
       "-C", "--conf", [&](path p) { opt_C = p; },
@@ -1050,6 +1067,15 @@ The default is CMD.conf if it exists, otherwise default.conf)",
   if (!conf.mask_files_.empty())
     conf.mask_warn_ = true;
 
+  ensure_file(conf.home_jai(opt_init), ".defaults", jai_defaults, 0600);
+  ensure_file(conf.home_jai(), "default.conf", default_conf, 0600);
+
+  if (opt_init) {
+    std::println("You can edit the configuration in {}/default.conf",
+                 conf.homejaipath_.string());
+    exit(0);
+  }
+
   if (opt_u) {
     if (!conf.grant_cwd_ || !conf.grant_directories_.empty() || !cmd.empty()) {
       std::println(stderr, "-u is not compatible with -d, -D, or a command");
@@ -1060,9 +1086,6 @@ The default is CMD.conf if it exists, otherwise default.conf)",
     return;
   }
 
-  ensure_file(conf.home_jai(), ".defaults", jai_defaults, 0600);
-  ensure_file(conf.home_jai(), "default.conf", default_conf, 0600);
-
   if (!opt_C.empty()) {
     if (!conf.parse_config_file(opt_C))
       err("{}: no such configuration file", opt_C.string());