Files
passepartout/org/channel-shell.org
Amr Gharbeia b9a4318ef8 reorg: tangle to XDG, remove stale lisp files, fix tui input
- Changed all 50 org file :tangle targets from ../lisp/ to
  ~/.local/share/passepartout/lisp/ (XDG data dir)
- Removed 49 generated .lisp files from project lisp/ directory
- Removed tests/system-integration-tests.lisp (generated)
- Removed lisp/*.fasl (compiled, stale)
- Updated core-manifest.org to tangle .asd to XDG root
- Remapped quicklisp symlink: local-projects/passepartout → XDG

TUI fixes in channel-tui-main.org:
- Removed with-raw-terminal (stty raw breaks fd 0 reads in this SBCL)
- Use cat subprocess + pipe for keyboard input (via :input :interactive)
- Blocking read-char on pipe with with-timeout 0.1s for daemon processing
- Key events queued via drain-queue alongside daemon messages
- Full dialog key routing (Escape, Up/Down, Enter, filters, Backspace)
- SIGWINCH resize handling
- Post-handshake backend-size re-query
- Daemon version in status bar (was v0.5.0 hardcoded)
- Handshake version stored in state, no add-msg
- :daemon-version and :size-queried in state plist
- view-status uses draw-rect for background
- Test section gated with #+passepartout-tests
2026-05-14 12:34:06 -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))))