fix(skills): complete reconstruction of bouncer skill to resolve catastrophic syntax failures

This commit is contained in:
2026-04-28 18:42:49 -04:00
parent 91c9bba50a
commit 014cd152db

View File

@@ -1,173 +1,69 @@
#+PROPERTY: header-args:lisp :tangle (concat (identity (getenv "INSTALL_DIR")) "/skills/org-skill-bouncer.lisp")" ) #+TITLE: SKILL: Bouncer (org-skill-bouncer.org)
:PROPERTIES: #+AUTHOR: Agent
:ID: bouncer-agent-skill
:CREATED: [2026-04-11 Sat 15:20]
:EDITED: [2026-04-22 Wed 16:00]
:END:
#+DEPENDS_ON: org-skill-credentials-vault
#+TITLE: SKILL: Deterministic Engine Bouncer (Authorization Gate)
#+STARTUP: content
#+FILETAGS: :system:bouncer:authorization:autonomy: #+FILETAGS: :system:bouncer:authorization:autonomy:
#+PROPERTY: header-args:lisp :tangle org-skill-bouncer.lisp
* Overview * Overview
The *Bouncer Skill* is the physical security layer of OpenCortex. It enforces operational security checks on all proposed actions.
The *Bouncer Skill* is the physical security layer of OpenCortex. While the Policy skill enforces constitutional invariants (transparency, autonomy, modularity), the Bouncer enforces operational security checks. * Implementation
Think of Policy as the constitution and Bouncer as the bouncer at the door:
- **Policy** asks: "Is this action aligned with our values?"
- **Bouncer** asks: "Is this action safe to execute?"
** The Flight Plan Pattern
High-risk actions don't simply pass or fail—they can enter the "Flight Plan" approval workflow:
1. Bouncer intercepts a risky action
2. Creates an Org node ("Flight Plan describing the action
3. User manually approves the flight plan in Emacs
4. Bouncer detects approval on next heartbeat
5. Action is re-injected with `approved = t` flag, bypassing the gate
This creates human-in-the-loop oversight for dangerous operations without blocking the system entirely.
** Why a Separate Skill?**
Security and policy are separated for clarity and auditability:
- Policy decisions can be explained (they reference invariants)
- Bouncer decisions are technical (they reference threat vectors)
When something is blocked, the logs clearly show which layer blocked it and why.
* Package Context
** Package Context
#+begin_src lisp #+begin_src lisp
(in-package :opencortex) (in-package :opencortex)
#+end_src #+end_src
* Security Vectors ** Security Configuration
#+begin_src lisp
The Bouncer implements the 5-Vector security model: (defvar *bouncer-network-whitelist*
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com")
| Vector | Threat | Response | "Domains that the Bouncer considers safe for outbound connections.")
|--------|--------|----------| #+end_src
| Secret Exposure | API keys, passwords in output | Hard block |
| Network Exfiltration | Data sent to unauthorized hosts | Approval required |
| Shell Execution | Arbitrary command execution | Approval required |
| File Modification | Writing/deleting files | Soft check |
| Eval Execution | Arbitrary code evaluation | Approval required |
** Secret Exposure Detection
The vault stores sensitive credentials. This check scans action text for vault secrets to prevent accidental exposure.
** Secret Scanning (bouncer-scan-secrets)
#+begin_src lisp #+begin_src lisp
(defun bouncer-scan-secrets (text) (defun bouncer-scan-secrets (text)
"Scans TEXT for known secrets from the vault. "Scans TEXT for known secrets from the vault."
RETURNS: The name of the matched secret, or NIL if text is clean.
This prevents the catastrophic failure mode where the agent
accidentally echoes an API key in its response or log output.
The check uses substring matching (not regex) for reliability.
Only secrets longer than 5 characters are checked to avoid
false positives on common words."
(when (and text (stringp text)) (when (and text (stringp text))
(let ((found-secret nil)) (let ((found-secret nil))
(maphash (lambda (key val) (maphash (lambda (key val)
;; Only check secrets of meaningful length
(when (and val (stringp val) (> (length val) 5)) (when (and val (stringp val) (> (length val) 5))
;; Search for secret value in action text
(when (search val text) (when (search val text)
(setf found-secret key)))) (setf found-secret key))))
*vault-memory*)
opencortex::*vault-memory*)
found-secret))) found-secret)))
#+end_src #+end_src
** Network Exfiltration Detection ** Network Check (bouncer-check-network-exfil)
Detects when shell commands try to send data to untrusted network destinations.
#+begin_src lisp #+begin_src lisp
(defvar *bouncer-network-whitelist*
'("api.telegram.org" "matrix.org" "googleapis.com" "openai.com" "anthropic.com
"Domains that the Bouncer considers safe for outbound connections.
This whitelist should be minimalonly services explicitly configured
as gateways. All other outbound connections require approval.
(defun bouncer-check-network-exfil (cmd) (defun bouncer-check-network-exfil (cmd)
"Detects if CMD attempts to contact an unwhitelisted external host. "Detects if CMD attempts to contact an unwhitelisted external host."
Returns T if the command targets an unknown external host.
Returns NIL if the command is clean or only contacts whitelisted hosts.
The check looks for HTTP/HTTPS/FTP URLs and extracts the domain.
If the domain isn't in *bouncer-network-whitelist*, it's flagged."
(when (and cmd (stringp cmd)) (when (and cmd (stringp cmd))
(multiple-value-bind (match regs)
;; Look for URL patterns in the command (cl-ppcre:scan-to-strings "(http|https|ftp)://([\\w\\.-]+)" cmd)
(when (cl-ppcre:scan "(http|https|ftp)://([\\w\\.-]+)" cmd) (declare (ignore match))
(when regs
(multiple-value-bind (match regs)
(cl-ppcre:scan-to-strings "(http|https|ftp)://([\\w\\.-]+)" cmd)
(declare (ignore match))
(let ((domain (aref regs 1))) (let ((domain (aref regs 1)))
;; Check if domain is whitelisted
(not (some (lambda (safe) (search safe domain)) (not (some (lambda (safe) (search safe domain))
*bouncer-network-whitelist*))))))) *bouncer-network-whitelist*)))))))
#+end_src #+end_src
* Runtime Guard ** Main Security Gate (bouncer-check)
** bouncer-check: Main Security Gate
#+begin_src lisp #+begin_src lisp
(defun bouncer-check (action context) (defun bouncer-check (action context)
"The 5-Vector security gate for high-risk actions. "The 5-Vector security gate for high-risk actions."
Evaluates an action against all security vectors and either:
- Returns the action unchanged (pass)
- Returns a blocking LOG event (hard block)
- Returns an approval-required EVENT (soft block)
Vector evaluation order:
1. Already approved actions pass immediately
2. Secret exposure → hard block
3. Network exfiltration → approval required
4. High-impact targets → approval required
The context parameter is not used directly but provided for
consistency with the skill gate signature."
(declare (ignore context)) (declare (ignore context))
(let* ((target (proto-get action :target))
(let* ((target (getf action :target)) (payload (proto-get action :payload))
(payload (getf action :payload)) (text (or (proto-get payload :text) (proto-get action :text)))
(text (or (getf payload :text) (getf action :text))) (cmd (or (proto-get payload :cmd)
;; Extract cmd from direct shell or tool-mediated shell call (when (and (eq target :tool) (equal (proto-get payload :tool) "shell"))
(cmd (or (getf payload :cmd) (proto-get (proto-get payload :args) :cmd))))
(when (and (eq target :tool) (approved (proto-get action :approved)))
(equal (getf payload :tool) "shell)
(getf (getf payload :args) :cmd))))
(approved (getf action :approved)))
(cond (cond
(approved action)
;; Vector 0: Already approved actions pass through
(approved
action)
;; Vector 1: Secret Exposure (Hard Block)
;; If any vault secret is found in the action text, block immediately
((and text (bouncer-scan-secrets text)) ((and text (bouncer-scan-secrets text))
(let ((secret-name (bouncer-scan-secrets text))) (let ((secret-name (bouncer-scan-secrets text)))
(harness-log "SECURITY VIOLATION: Blocked potential leak of secret '~a'" secret-name) (harness-log "SECURITY VIOLATION: Blocked potential leak of secret '~a'" secret-name)
@@ -175,208 +71,75 @@ Detects when shell commands try to send data to untrusted network destinations.
:payload (list :level :error :payload (list :level :error
:text (format nil "Action blocked: Potential exposure of '~a'" secret-name))))) :text (format nil "Action blocked: Potential exposure of '~a'" secret-name)))))
;; Vector 2: Network Exfiltration (Soft Block)
;; Shell commands targeting unknown hosts require approval
((and (or (eq target :shell) ((and (or (eq target :shell)
(and (eq target :tool) (and (eq target :tool) (equal (proto-get payload :tool) "shell")))
(equal (getf payload :tool) "shell))
(bouncer-check-network-exfil cmd)) (bouncer-check-network-exfil cmd))
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval.")
(list :type :EVENT :payload (list :sensor :approval-required :action action)))
(harness-log "SECURITY WARNING: External network call detected. Queuing for approval.
(list :type :EVENT
:payload (list :sensor :approval-required
:action action)))
;; Vector 3: High-Impact Targets (Soft Block)
;; Shell execution, file repair, and eval require approval
((or (member target '(:shell)) ((or (member target '(:shell))
(and (eq target :tool) (and (eq target :tool) (member (proto-get payload :tool) '("shell" "repair-file") :test #'string=))
(member (getf payload :tool) '("shell" "repair-file :test #'string=)) (and (eq target :emacs) (eq (proto-get payload :action) :eval)))
(and (eq target :emacs) (harness-log "SECURITY: High-impact action requires approval: ~a" (or (proto-get payload :tool) target))
(eq (getf payload :action) :eval))) (list :type :EVENT :payload (list :sensor :approval-required :action action)))
(harness-log "SECURITY: High-impact action requires approval: ~a" (t action))))
(or (getf payload :tool) target))
(list :type :EVENT
:payload (list :sensor :approval-required
:action action)))
;; Vector 4: Default pass
(t
action))))
#+end_src #+end_src
* Flight Plan Workflow ** Approval Processing (bouncer-process-approvals)
** Processing Approvals
When a flight plan is approved in Emacs, the Bouncer detects it and re-injects the action.
#+begin_src lisp #+begin_src lisp
(defun bouncer-process-approvals () (defun bouncer-process-approvals ()
"Scans the object store for APPROVED flight plans and re-injects them. "Scans for APPROVED flight plans and re-injects them."
(let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED"))
This function is called on every heartbeat, allowing the agent to
check for approvals without blocking the main signal pipeline.
Flight Plan format:
- Has TAGS including \"FLIGHT_PLAN\"
- Has TODO set to \"APPROVED\"
- Has ACTION containing the serialized action plist
When an approved flight plan is found:
1. Deserialize the action from the ACTION attribute
2. Mark the action as :approved = t (bypasses security gate)
3. Re-inject into the signal pipeline
4. Mark the flight plan as DONE
Returns T if any flight plans were processed."
(let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED)
(found-any nil)) (found-any nil))
(dolist (node approved-nodes) (dolist (node approved-nodes)
(let* ((attrs (org-object-attributes node))
(let* ((tags (getf (org-object-attributes node) :TAGS)) (tags (getf attrs :TAGS))
(action-str (getf (org-object-attributes node) :ACTION))) (action-str (getf attrs :ACTION)))
(when (and (member "FLIGHT_PLAN" tags :test #'string-equal) action-str)
;; Only process flight plans (not other APPROVED items) (harness-log "BOUNCER: Found approved flight plan '~a'. Re-injecting..." (org-object-id node))
(when (and (member "FLIGHT_PLAN" tags :test #'string-equal)
action-str)
(harness-log "BOUNCER: Found approved flight plan '~a'. Re-injecting..."
(org-object-id node))
(let ((action (ignore-errors (read-from-string action-str)))) (let ((action (ignore-errors (read-from-string action-str))))
(when action (when action
;; Mark as approved to bypass the security gate on re-injection
(setf (getf action :approved) t) (setf (getf action :approved) t)
;; Re-inject the action into the signal pipeline
(inject-stimulus action) (inject-stimulus action)
(setf (getf (org-object-attributes node) :TODO) "DONE")
;; Mark the flight plan as done
(setf (getf (org-object-attributes node) :TODO) "DONE
(setq found-any t)))))) (setq found-any t))))))
found-any)) found-any))
#+end_src #+end_src
** Creating Flight Plans ** Flight Plan Creation (bouncer-create-flight-plan)
When the Bouncer intercepts a high-risk action, it creates a flight plan node for manual approval.
#+begin_src lisp #+begin_src lisp
(defun bouncer-create-flight-plan (blocked-action) (defun bouncer-create-flight-plan (blocked-action)
"Creates an Org node representing a pending flight plan for manual approval. "Creates a Flight Plan node for manual approval."
(let ((id (org-id-new)))
BLOCKED-ACTION is the action plist that was intercepted. (harness-log "BOUNCER: Creating flight plan node '~a'..." id)
(list :type :REQUEST :target :emacs
The flight plan node contains: :payload (list :action :insert-node :id id
- A title describing the action :attributes (list :TITLE "Flight Plan: High-Risk Action"
- TODO set to PLAN (awaiting approval) :TODO "PLAN" :TAGS '("FLIGHT_PLAN")
- TAGS including FLIGHT_PLAN :ACTION (format nil "~s" blocked-action))))))
- ACTION attribute containing the serialized action
The user reviews the flight plan and changes TODO to APPROVED.
On the next heartbeat, bouncer-process-approvals will detect
the approval and re-inject the action.
Returns the generated org-id for the flight plan."
(let ((id (org-id-new)))
(harness-log "BOUNCER: Creating flight plan node '~a'..." id)
;; Inject a node creation request
(list :type :REQUEST
:target :emacs
:payload (list :action :insert-node
:id id
:attributes (list
:TITLE "Flight Plan: High-Risk Action"
:TODO "PLAN"
:TAGS '("FLIGHT_PLAN
:ACTION (format nil "~s" blocked-action))))))
#+end_src #+end_src
* Skill Gate ** Gate Logic (bouncer-deterministic-gate)
** Main Gate Function
#+begin_src lisp #+begin_src lisp
(defun bouncer-deterministic-gate (action context) (defun bouncer-deterministic-gate (action context)
"Main deterministic gate for the Bouncer skill. "Main deterministic gate for the Bouncer skill."
Handles three types of signals:
1. :approval-required - Create a flight plan for the blocked action
2. :heartbeat - Process any pending approvals
3. otherwise - Run security check on the action
The trigger is always true (bouncer evaluates all actions)
because security cannot be selective."
(let* ((payload (getf context :payload)) (let* ((payload (getf context :payload))
(sensor (getf payload :sensor))) (sensor (getf payload :sensor)))
(case sensor (case sensor
;; Signal type 1: Action was blocked, create flight plan
(:approval-required (:approval-required
(let* ((blocked-action (getf payload :action))) (bouncer-create-flight-plan (getf payload :action)))
(bouncer-create-flight-plan blocked-action)))
;; Signal type 2: Heartbeat, check for approvals
(:heartbeat (:heartbeat
(bouncer-process-approvals) (bouncer-process-approvals)
;; After processing approvals, still run the security check (if action (bouncer-check action context) action))
(if action
(bouncer-check action context)
action))
;; Signal type 3: Normal action, run security check
(otherwise (otherwise
(if action (if action (bouncer-check action context) action)))))
(bouncer-check action context)
action)))))
#+end_src #+end_src
** Skill Registration ** Skill Registration
#+begin_src lisp #+begin_src lisp
(defskill :skill-bouncer (defskill :skill-bouncer
:priority 150 :priority 150
:trigger (lambda (ctx) (declare (ignore ctx)) t) :trigger (lambda (ctx) (declare (ignore ctx)) t)
:probabilistic nil
:deterministic #'bouncer-deterministic-gate) :deterministic #'bouncer-deterministic-gate)
#+end_src #+end_src
* Quick Reference
** Security Vectors Summary
| Vector | Check | Response |
|--------|-------|----------|
| Secret Exposure | `bouncer-scan-secrets` | Hard block |
| Network Exfil | `bouncer-check-network-exfil` | Approval required |
| Shell Execution | target = :shell or tool = "shell" | Approval required |
| Eval Execution | target = :emacs and action = :eval | Approval required |
| File Repair | tool = "repair-file" | Approval required |
** Flight Plan Lifecycle
1. High-risk action intercepted → `:approval-required` signal
2. Flight plan node created in Emacs with `TODO: PLAN`
3. User reviews and sets `TODO: APPROVED`
4. Next heartbeat detects approval
5. Action re-injected with `approved = t`
6. Security gate bypassed, action executes
7. Flight plan marked `TODO: DONE`
* See Also
- [[file:org-skill-credentials-vault.org][Credentials Vault]] - Where secrets are stored
- [[file:org-skill-policy.org][Policy Skill]] - Constitutional constraints
- [[file:../harness/act.org][Act Stage]] - Where gates are invoked