Files
passepartout/org/channel-shell.org
Amr Gharbeia c227877302 v0.8.3: TUI stabilization — box calls, package fixes, sandbox, configure
Bug fixes:
- Fix box() calls: set color-pair before box, pass ACS default chtype integers
- Fix markdown functions: move to passepartout.channel-tui package where
  Croatoan is imported; use add-attributes/remove-attributes instead of
  :bold/:underline kwargs to add-string; call theme-color in gate-trace-lines
  to convert theme keys to Croatoan colors
- Fix sandbox: remove dex:get/dex:post from restricted symbols
  (blocked neuro-provider from loading)
- Export *log-lock* from passepartout (was unbound in jailed skill packages)
- Fix configure: always deploy to XDG, skip cp when source==dest
- Fix bash crash handler format string (~~ escaping)
- Revert test reorder in 28 files (caused package leakage in skill loader)

Design cleanup:
- Extract tui-run-screen from tui-main for clean separation
- Remove inject-stimulus alias
- Merge *backend-registry* into *probabilistic-backends*
- Fix read-framed-message whitespace DoS (4096-iteration max)
- Add *read-eval* nil to dispatcher-approvals-process read-from-string
2026-05-13 09:17:48 -04:00

5.9 KiB

SKILL: Shell Actuator (org-skill-shell-actuator.org)

Overview: The Physical Actuator

The Shell Actuator is the agent's hand in the physical world. Given a shell command, it executes it via bash -c and returns the output. This is how the agent installs packages, reads files, runs scripts, and interacts with any Unix tool.

Because shell execution is the highest-risk operation in the system, the Shell Actuator is protected by multiple safety layers:

  1. The Dispatcher's shell safety gate blocks destructive commands (rm -rf /, dd, mkfs)
  2. The Dispatcher's injection gate blocks backtick and $() patterns
  3. The Dispatcher's network exfil gate blocks connections to unwhitelisted hosts
  4. The actuator enforces a timeout (default 30s) so hanging commands don't freeze the agent
  5. The actuator caps output (default 100KB) so infinite output doesn't exhaust memory
  6. (v0.4.3) When bwrap (Bubblewrap) is available, commands execute inside a Linux namespace sandbox with network and IPC isolation

Contract

  1. (bwrap-available-p): returns T if bwrap is installed and usable, NIL otherwise. Cached at load time via which bwrap.
  2. (bwrap-wrap-command cmd timeout memex-dir): returns a command list suitable for uiop:run-program — wraps cmd in a bwrap sandbox with --unshare-net, --unshare-ipc, --ro-bind for system dirs, and --bind for the memex and /tmp.
  3. (actuator-shell-execute action context): when bwrap is available, wraps the command through the sandbox. When bwrap is unavailable, falls back to the existing timeout bash -c behavior.

Implementation

Shell Execution (actuator-shell-execute)

;; REPL-VERIFIED: 2026-05-03T13:00:00

(in-package :passepartout)

(defvar *bwrap-available* nil
  "Set to T at load time if the bwrap binary is found in PATH.")

(defvar *bwrap-base-args*
  '("--ro-bind" "/usr" "/usr"
    "--ro-bind" "/lib" "/lib"
    "--ro-bind" "/bin" "/bin"
    "--ro-bind" "/etc" "/etc"
    "--bind" "/tmp" "/tmp"
    "--unshare-net"
    "--unshare-ipc")
  "Base bwrap arguments for the sandbox. --bind ~/memex ~/memex is added dynamically.")

(defun bwrap-available-p ()
  "Returns T if bwrap (bubblewrap) is installed and usable."
  *bwrap-available*)

(defun bwrap-wrap-command (cmd timeout memex-dir)
  "Wrap CMD in a bwrap sandbox with network and IPC isolation.
Returns a list suitable for uiop:run-program."
  `("bwrap"
    ,@*bwrap-base-args*
    "--bind" ,memex-dir ,memex-dir
    "timeout" ,(format nil "~a" timeout)
    "bash" "-c" ,cmd))

;; Initialize at load time
(setf *bwrap-available*
      (= 0 (nth-value 2 (uiop:run-program '("which" "bwrap") :output nil :error-output nil :ignore-error-status t))))

(defun actuator-shell-execute (action context)
  "Executes a shell command via the OS timeout binary with output limit.
When bwrap is available, wraps the command in a Linux namespace sandbox."
  (declare (ignore context))
  (let* ((payload (getf action :payload))
         (cmd (getf payload :cmd))
         (timeout-sym (find-symbol "*DISPATCHER-SHELL-TIMEOUT*" :passepartout))
         (timeout (or (getf payload :timeout) (if timeout-sym (symbol-value timeout-sym) 30)))
         (max-sym (find-symbol "*DISPATCHER-SHELL-MAX-OUTPUT*" :passepartout))
         (max-output (or (getf payload :max-output) (if max-sym (symbol-value max-sym) 100000)))
         (memex-dir (or (uiop:getenv "MEMEX_DIR") (namestring (merge-pathnames "memex/" (user-homedir-pathname))))))
    (log-message "ACT [Shell]: ~a (timeout: ~as)~@[ bwrap: enabled~]" cmd timeout (and *bwrap-available* " (bwrap)"))
    (let ((cmdline (if *bwrap-available*
                       (bwrap-wrap-command cmd timeout memex-dir)
                       (list "timeout" (format nil "~a" timeout) "bash" "-c" cmd))))
      (multiple-value-bind (out err code)
          (uiop:run-program cmdline
                            :output :string :error-output :string
                            :ignore-error-status t)
        (cond
          ((= code 124) (format nil "ERROR: Command timed out after ~a seconds" timeout))
          ((> (length out) max-output)
           (format nil "~a~%... (output truncated to ~a chars)" (subseq out 0 max-output) max-output))
          ((= code 0) out)
          (t (format nil "ERROR [~a]: ~a" code err)))))))

Skill Registration

(register-actuator :shell #'actuator-shell-execute)

(defskill :passepartout-channel-shell
  :priority 50
  :trigger (lambda (ctx) (declare (ignore ctx)) nil))

Test Suite

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload :fiveam :silent t))

(defpackage :passepartout-shell-actuator-tests
  (:use :cl :fiveam :passepartout)
  (:export #:shell-actuator-suite))

(in-package :passepartout-shell-actuator-tests)

(def-suite shell-actuator-suite :description "Verification of the Shell Actuator")
(in-suite shell-actuator-suite)

(test test-bwrap-wrap-command
  "Contract 2: bwrap-wrap-command returns properly formatted command list."
  (let ((cmdline (passepartout::bwrap-wrap-command "echo hello" 30 "/home/user/memex")))
    (is (member "bwrap" cmdline :test #'string=))
    (is (member "--unshare-net" cmdline :test #'string=))
    (is (member "--unshare-ipc" cmdline :test #'string=))
    (is (member "echo hello" cmdline :test #'string=))))

(test test-bwrap-available-p-returns-boolean
  "Contract 1: bwrap-available-p returns T or NIL."
  (let ((avail (passepartout::bwrap-available-p)))
    (is (typep avail 'boolean))))

(test test-actuator-shell-execute-echo
  "Contract 3: actuator-shell-execute runs echo and returns output."
  (let* ((action '(:type :REQUEST :target :shell :payload (:cmd "echo hello")))
         (result (passepartout::actuator-shell-execute action nil)))
    (is (stringp result))
    (is (search "hello" result :test #'char-equal))))