v0.3.2: shell safety, :system :eval approval, skill sandbox
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 3s
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 3s
1. Shell actuator: remove double bash -c wrapping (format ~s produces S-expression-safe strings, not shell-safe). Now passes cmd directly to (timeout N bash -c cmd) via run-program arg list. 2. Dispatcher: extend high-impact approval gate to :system :eval. Previously only :shell, :tool "shell", and :emacs :eval triggered HITL. Now :system :eval also requires Flight Plan approval. 3. Skill sandbox: before promoting a skill from its jailed package to :passepartout, scan for restricted symbol references (uiop:run-program, uiop:shell, uiop:run-shell-command). Block promotion on violation. New skill-entry status :sandbox-blocked for blocked skills. Test: 91 pass, 0 fail across 13 suites.
This commit is contained in:
@@ -247,21 +247,38 @@ declarations so embedded test code evaluates in the correct package."
|
|||||||
(log-message "LOADER: Loading .lisp skill '~a' in package ~a" skill-base-name (package-name *package*))
|
(log-message "LOADER: Loading .lisp skill '~a' in package ~a" skill-base-name (package-name *package*))
|
||||||
(with-input-from-string (s content)
|
(with-input-from-string (s content)
|
||||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||||
do (handler-case (eval form)
|
do (handler-case (eval form)
|
||||||
(error (c) (log-message "LOADER WARNING in '~a': ~a" skill-base-name c))))))
|
(error (c) (log-message "LOADER WARNING in '~a': ~a" skill-base-name c))))))
|
||||||
(let ((target-pkg (find-package :passepartout))
|
(let* ((jailed-pkg (find-package pkg-name))
|
||||||
(exported 0)
|
(restricted '("RUN-PROGRAM" "SHELL" "RUN-SHELL-COMMAND"))
|
||||||
(seen (make-hash-table :test 'equal)))
|
(violation (loop for r in restricted
|
||||||
(do-symbols (sym (find-package pkg-name))
|
for sym = (find-symbol r :uiop)
|
||||||
(when (and (eq (symbol-package sym) (find-package pkg-name))
|
when (and sym (fboundp sym)
|
||||||
(or (fboundp sym) (boundp sym))
|
(loop for skill-sym being the symbols of jailed-pkg
|
||||||
(not (gethash (symbol-name sym) seen)))
|
when (and (fboundp skill-sym)
|
||||||
(setf (gethash (symbol-name sym) seen) t)
|
(eq (symbol-function skill-sym)
|
||||||
(incf exported)
|
(symbol-function sym)))
|
||||||
(let ((existing (find-symbol (symbol-name sym) target-pkg)))
|
return skill-sym))
|
||||||
(when existing (unintern existing target-pkg)))
|
collect (format nil "~a" sym))))
|
||||||
(import sym target-pkg)
|
(when violation
|
||||||
(ignore-errors (export sym target-pkg))))
|
(log-message "LOADER SANDBOX: Skill '~a' blocked — references restricted symbol(s): ~{~a~^, ~}"
|
||||||
|
skill-base-name violation)
|
||||||
|
(setf (skill-entry-status entry) :sandbox-blocked)
|
||||||
|
(return-from load-skill-from-lisp nil))
|
||||||
|
(log-message "LOADER SANDBOX: Skill '~a' passed sandbox check" skill-base-name))
|
||||||
|
(let ((target-pkg (find-package :passepartout))
|
||||||
|
(exported 0)
|
||||||
|
(seen (make-hash-table :test 'equal)))
|
||||||
|
(do-symbols (sym (find-package pkg-name))
|
||||||
|
(when (and (eq (symbol-package sym) (find-package pkg-name))
|
||||||
|
(or (fboundp sym) (boundp sym))
|
||||||
|
(not (gethash (symbol-name sym) seen)))
|
||||||
|
(setf (gethash (symbol-name sym) seen) t)
|
||||||
|
(incf exported)
|
||||||
|
(let ((existing (find-symbol (symbol-name sym) target-pkg)))
|
||||||
|
(when existing (unintern existing target-pkg)))
|
||||||
|
(import sym target-pkg)
|
||||||
|
(ignore-errors (export sym target-pkg))))
|
||||||
(log-message "LOADER: Exported ~a symbols from ~a to :PASSEPARTOUT"
|
(log-message "LOADER: Exported ~a symbols from ~a to :PASSEPARTOUT"
|
||||||
exported (package-name (find-package pkg-name))))
|
exported (package-name (find-package pkg-name))))
|
||||||
(setf (skill-entry-status entry) :ready))
|
(setf (skill-entry-status entry) :ready))
|
||||||
|
|||||||
@@ -278,7 +278,8 @@ privacy tags, privacy text, shell safety, network exfil, high-impact approval."
|
|||||||
;; Vector 8: High-impact action approval
|
;; Vector 8: High-impact action approval
|
||||||
((or (member target '(:shell))
|
((or (member target '(:shell))
|
||||||
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
||||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval)))
|
(and (eq target :emacs) (eq (proto-get payload :action) :eval))
|
||||||
|
(and (eq target :system) (eq (proto-get payload :action) :eval)))
|
||||||
(log-message "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
|
(log-message "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
|
||||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||||
(t action))))
|
(t action))))
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
(defun actuator-shell-execute (action context)
|
(defun actuator-shell-execute (action context)
|
||||||
"Executes a bash command with timeout (via timeout(1)) and output limit."
|
"Executes a shell command via the OS timeout binary with output limit."
|
||||||
(declare (ignore context))
|
(declare (ignore context))
|
||||||
(let* ((payload (getf action :payload))
|
(let* ((payload (getf action :payload))
|
||||||
(cmd (getf payload :cmd))
|
(cmd (getf payload :cmd))
|
||||||
(timeout-sym (find-symbol "*BOUNCER-SHELL-TIMEOUT*" :passepartout))
|
(timeout-sym (find-symbol "*BOUNCER-SHELL-TIMEOUT*" :passepartout))
|
||||||
(timeout (or (getf payload :timeout) (if timeout-sym (symbol-value timeout-sym) 30)))
|
(timeout (or (getf payload :timeout) (if timeout-sym (symbol-value timeout-sym) 30)))
|
||||||
(max-sym (find-symbol "*BOUNCER-SHELL-MAX-OUTPUT*" :passepartout))
|
(max-sym (find-symbol "*BOUNCER-SHELL-MAX-OUTPUT*" :passepartout))
|
||||||
(max-output (or (getf payload :max-output) (if max-sym (symbol-value max-sym) 100000)))
|
(max-output (or (getf payload :max-output) (if max-sym (symbol-value max-sym) 100000))))
|
||||||
(wrapped-cmd (format nil "timeout ~a bash -c ~s" timeout cmd)))
|
|
||||||
(log-message "ACT [Shell]: ~a (timeout: ~as)" cmd timeout)
|
(log-message "ACT [Shell]: ~a (timeout: ~as)" cmd timeout)
|
||||||
(multiple-value-bind (out err code)
|
(multiple-value-bind (out err code)
|
||||||
(uiop:run-program (list "bash" "-c" wrapped-cmd)
|
(uiop:run-program (list "timeout" (format nil "~a" timeout) "bash" "-c" cmd)
|
||||||
:output :string :error-output :string
|
:output :string :error-output :string
|
||||||
:ignore-error-status t)
|
:ignore-error-status t)
|
||||||
(cond
|
(cond
|
||||||
|
|||||||
@@ -372,21 +372,38 @@ The same jailed package and symbol export process applies.
|
|||||||
(log-message "LOADER: Loading .lisp skill '~a' in package ~a" skill-base-name (package-name *package*))
|
(log-message "LOADER: Loading .lisp skill '~a' in package ~a" skill-base-name (package-name *package*))
|
||||||
(with-input-from-string (s content)
|
(with-input-from-string (s content)
|
||||||
(loop for form = (read s nil :eof) until (eq form :eof)
|
(loop for form = (read s nil :eof) until (eq form :eof)
|
||||||
do (handler-case (eval form)
|
do (handler-case (eval form)
|
||||||
(error (c) (log-message "LOADER WARNING in '~a': ~a" skill-base-name c))))))
|
(error (c) (log-message "LOADER WARNING in '~a': ~a" skill-base-name c))))))
|
||||||
(let ((target-pkg (find-package :passepartout))
|
(let* ((jailed-pkg (find-package pkg-name))
|
||||||
(exported 0)
|
(restricted '("RUN-PROGRAM" "SHELL" "RUN-SHELL-COMMAND"))
|
||||||
(seen (make-hash-table :test 'equal)))
|
(violation (loop for r in restricted
|
||||||
(do-symbols (sym (find-package pkg-name))
|
for sym = (find-symbol r :uiop)
|
||||||
(when (and (eq (symbol-package sym) (find-package pkg-name))
|
when (and sym (fboundp sym)
|
||||||
(or (fboundp sym) (boundp sym))
|
(loop for skill-sym being the symbols of jailed-pkg
|
||||||
(not (gethash (symbol-name sym) seen)))
|
when (and (fboundp skill-sym)
|
||||||
(setf (gethash (symbol-name sym) seen) t)
|
(eq (symbol-function skill-sym)
|
||||||
(incf exported)
|
(symbol-function sym)))
|
||||||
(let ((existing (find-symbol (symbol-name sym) target-pkg)))
|
return skill-sym))
|
||||||
(when existing (unintern existing target-pkg)))
|
collect (format nil "~a" sym))))
|
||||||
(import sym target-pkg)
|
(when violation
|
||||||
(ignore-errors (export sym target-pkg))))
|
(log-message "LOADER SANDBOX: Skill '~a' blocked — references restricted symbol(s): ~{~a~^, ~}"
|
||||||
|
skill-base-name violation)
|
||||||
|
(setf (skill-entry-status entry) :sandbox-blocked)
|
||||||
|
(return-from load-skill-from-lisp nil))
|
||||||
|
(log-message "LOADER SANDBOX: Skill '~a' passed sandbox check" skill-base-name))
|
||||||
|
(let ((target-pkg (find-package :passepartout))
|
||||||
|
(exported 0)
|
||||||
|
(seen (make-hash-table :test 'equal)))
|
||||||
|
(do-symbols (sym (find-package pkg-name))
|
||||||
|
(when (and (eq (symbol-package sym) (find-package pkg-name))
|
||||||
|
(or (fboundp sym) (boundp sym))
|
||||||
|
(not (gethash (symbol-name sym) seen)))
|
||||||
|
(setf (gethash (symbol-name sym) seen) t)
|
||||||
|
(incf exported)
|
||||||
|
(let ((existing (find-symbol (symbol-name sym) target-pkg)))
|
||||||
|
(when existing (unintern existing target-pkg)))
|
||||||
|
(import sym target-pkg)
|
||||||
|
(ignore-errors (export sym target-pkg))))
|
||||||
(log-message "LOADER: Exported ~a symbols from ~a to :PASSEPARTOUT"
|
(log-message "LOADER: Exported ~a symbols from ~a to :PASSEPARTOUT"
|
||||||
exported (package-name (find-package pkg-name))))
|
exported (package-name (find-package pkg-name))))
|
||||||
(setf (skill-entry-status entry) :ready))
|
(setf (skill-entry-status entry) :ready))
|
||||||
|
|||||||
@@ -426,7 +426,8 @@ privacy tags, privacy text, shell safety, network exfil, high-impact approval."
|
|||||||
;; Vector 8: High-impact action approval
|
;; Vector 8: High-impact action approval
|
||||||
((or (member target '(:shell))
|
((or (member target '(:shell))
|
||||||
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
(and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
|
||||||
(and (eq target :emacs) (eq (proto-get payload :action) :eval)))
|
(and (eq target :emacs) (eq (proto-get payload :action) :eval))
|
||||||
|
(and (eq target :system) (eq (proto-get payload :action) :eval)))
|
||||||
(log-message "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
|
(log-message "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
|
||||||
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
|
||||||
(t action))))
|
(t action))))
|
||||||
|
|||||||
@@ -20,18 +20,17 @@ Because shell execution is the highest-risk operation in the system, the Shell A
|
|||||||
;; REPL-VERIFIED: 2026-05-03T13:00:00
|
;; REPL-VERIFIED: 2026-05-03T13:00:00
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
(defun actuator-shell-execute (action context)
|
(defun actuator-shell-execute (action context)
|
||||||
"Executes a bash command with timeout (via timeout(1)) and output limit."
|
"Executes a shell command via the OS timeout binary with output limit."
|
||||||
(declare (ignore context))
|
(declare (ignore context))
|
||||||
(let* ((payload (getf action :payload))
|
(let* ((payload (getf action :payload))
|
||||||
(cmd (getf payload :cmd))
|
(cmd (getf payload :cmd))
|
||||||
(timeout-sym (find-symbol "*BOUNCER-SHELL-TIMEOUT*" :passepartout))
|
(timeout-sym (find-symbol "*BOUNCER-SHELL-TIMEOUT*" :passepartout))
|
||||||
(timeout (or (getf payload :timeout) (if timeout-sym (symbol-value timeout-sym) 30)))
|
(timeout (or (getf payload :timeout) (if timeout-sym (symbol-value timeout-sym) 30)))
|
||||||
(max-sym (find-symbol "*BOUNCER-SHELL-MAX-OUTPUT*" :passepartout))
|
(max-sym (find-symbol "*BOUNCER-SHELL-MAX-OUTPUT*" :passepartout))
|
||||||
(max-output (or (getf payload :max-output) (if max-sym (symbol-value max-sym) 100000)))
|
(max-output (or (getf payload :max-output) (if max-sym (symbol-value max-sym) 100000))))
|
||||||
(wrapped-cmd (format nil "timeout ~a bash -c ~s" timeout cmd)))
|
|
||||||
(log-message "ACT [Shell]: ~a (timeout: ~as)" cmd timeout)
|
(log-message "ACT [Shell]: ~a (timeout: ~as)" cmd timeout)
|
||||||
(multiple-value-bind (out err code)
|
(multiple-value-bind (out err code)
|
||||||
(uiop:run-program (list "bash" "-c" wrapped-cmd)
|
(uiop:run-program (list "timeout" (format nil "~a" timeout) "bash" "-c" cmd)
|
||||||
:output :string :error-output :string
|
:output :string :error-output :string
|
||||||
:ignore-error-status t)
|
:ignore-error-status t)
|
||||||
(cond
|
(cond
|
||||||
|
|||||||
Reference in New Issue
Block a user