1dbc68f012
Heartbeat ISEs now include wm_active: number of nodes currently in working memory. Makes ISEs observable enough to diagnose activation health without a separate query.
392 lines
15 KiB
EmacsLisp
392 lines
15 KiB
EmacsLisp
import "memory.el"
|
|
|
|
fn idle_count() -> Int {
|
|
let s: String = state_get("soul.idle")
|
|
if str_eq(s, "") { return 0 }
|
|
return str_to_int(s)
|
|
}
|
|
|
|
fn idle_inc() -> Int {
|
|
let n: Int = idle_count() + 1
|
|
state_set("soul.idle", int_to_str(n))
|
|
return n
|
|
}
|
|
|
|
fn idle_reset() -> Void {
|
|
state_set("soul.idle", "0")
|
|
}
|
|
|
|
// ise_post — write an InternalStateEvent to the authoritative Engram HTTP backend.
|
|
// Reads SOUL_ISE_URL from env (or falls back to soul_engram_url state key).
|
|
// Falls back to local engram_node_full if neither is set.
|
|
fn ise_post(content: String) -> Void {
|
|
let ise_url: String = env("SOUL_ISE_URL")
|
|
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
|
|
if str_eq(engram_url, "") {
|
|
let discard: String = engram_node_full(
|
|
content, "InternalStateEvent", "state-event",
|
|
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
|
|
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
|
)
|
|
return ""
|
|
}
|
|
let safe: String = str_replace(content, "\"", "\\\"")
|
|
let body: String = "{\"content\":\"" + safe + "\"}"
|
|
let discard: String = http_post_json(engram_url + "/api/neuron/state-events", body)
|
|
return ""
|
|
}
|
|
|
|
fn emit_heartbeat() -> Void {
|
|
let pulse: String = state_get("soul.pulse")
|
|
let boot: String = state_get("soul_boot_count")
|
|
let idle: String = int_to_str(idle_count())
|
|
let ts: Int = time_now()
|
|
let nc: Int = engram_node_count()
|
|
let ec: Int = engram_edge_count()
|
|
let wmc: Int = engram_wm_count()
|
|
let payload: String = "{\"event\":\"heartbeat\",\"pulse\":" + pulse + ",\"boot\":" + boot + ",\"idle\":" + idle + ",\"node_count\":" + int_to_str(nc) + ",\"edge_count\":" + int_to_str(ec) + ",\"wm_active\":" + int_to_str(wmc) + ",\"ts\":" + int_to_str(ts) + "}"
|
|
ise_post(payload)
|
|
}
|
|
|
|
fn pulse_count() -> Int {
|
|
let s: String = state_get("soul.pulse")
|
|
if str_eq(s, "") {
|
|
return 0
|
|
}
|
|
return str_to_int(s)
|
|
}
|
|
|
|
fn pulse_inc() -> Int {
|
|
let n: Int = pulse_count() + 1
|
|
state_set("soul.pulse", int_to_str(n))
|
|
return n
|
|
}
|
|
|
|
fn make_action(kind: String, payload: String) -> String {
|
|
let safe: String = str_replace(payload, "\\", "\\\\")
|
|
let safe2: String = str_replace(safe, "\"", "\\\"")
|
|
let safe3: String = str_replace(safe2, "\n", "\\n")
|
|
let safe4: String = str_replace(safe3, "\r", "\\r")
|
|
return "{\"kind\":\"" + kind + "\",\"payload\":\"" + safe4 + "\"}"
|
|
}
|
|
|
|
fn perceive() -> String {
|
|
// Try the primary inbox first
|
|
let from_pending: String = engram_activate_json("soul-inbox-pending", 2)
|
|
let pending_ok: Bool = !str_eq(from_pending, "") && !str_eq(from_pending, "[]")
|
|
if pending_ok {
|
|
return from_pending
|
|
}
|
|
// Fallback: broader inbox scan
|
|
let from_inbox: String = engram_activate_json("soul-inbox", 2)
|
|
let inbox_ok: Bool = !str_eq(from_inbox, "") && !str_eq(from_inbox, "[]")
|
|
if inbox_ok {
|
|
return from_inbox
|
|
}
|
|
return "[]"
|
|
}
|
|
|
|
fn attend(node_json: String) -> String {
|
|
if str_eq(node_json, "") {
|
|
return make_action("noop", "")
|
|
}
|
|
if str_eq(node_json, "[]") {
|
|
return make_action("noop", "")
|
|
}
|
|
|
|
let node_id: String = json_get(node_json, "id")
|
|
if !str_eq(node_id, "") {
|
|
engram_strengthen(node_id)
|
|
}
|
|
|
|
let content: String = json_get(node_json, "content")
|
|
if str_eq(content, "") {
|
|
return make_action("noop", "")
|
|
}
|
|
|
|
if str_eq(content, "consolidate") {
|
|
return make_action("consolidate", "")
|
|
}
|
|
|
|
if str_starts_with(content, "remember ") {
|
|
let payload: String = str_slice(content, 9, str_len(content))
|
|
return make_action("remember", payload)
|
|
}
|
|
|
|
if str_starts_with(content, "search ") {
|
|
let payload: String = str_slice(content, 7, str_len(content))
|
|
return make_action("search", payload)
|
|
}
|
|
|
|
if str_starts_with(content, "activate ") {
|
|
let payload: String = str_slice(content, 9, str_len(content))
|
|
return make_action("activate", payload)
|
|
}
|
|
|
|
if str_starts_with(content, "strengthen ") {
|
|
let payload: String = str_slice(content, 11, str_len(content))
|
|
return make_action("strengthen", payload)
|
|
}
|
|
|
|
if str_starts_with(content, "forget ") {
|
|
let payload: String = str_slice(content, 7, str_len(content))
|
|
return make_action("forget", payload)
|
|
}
|
|
|
|
return make_action("respond", content)
|
|
}
|
|
|
|
fn respond(action_json: String) -> String {
|
|
let kind: String = json_get(action_json, "kind")
|
|
let payload: String = json_get(action_json, "payload")
|
|
|
|
if str_eq(kind, "noop") {
|
|
return "{\"outcome\":\"noop\"}"
|
|
}
|
|
|
|
if str_eq(kind, "remember") {
|
|
let tags: String = "[\"soul-memory\",\"awareness\"]"
|
|
let id: String = mem_remember(payload, tags)
|
|
return "{\"outcome\":\"remembered\",\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
if str_eq(kind, "consolidate") {
|
|
let stats: String = mem_consolidate()
|
|
return "{\"outcome\":\"consolidated\",\"stats\":" + stats + "}"
|
|
}
|
|
|
|
if str_eq(kind, "respond") {
|
|
let tags: String = "[\"soul-outbox\",\"awareness\"]"
|
|
let id: String = mem_store(payload, "soul-response", tags)
|
|
return "{\"outcome\":\"response\",\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
if str_eq(kind, "search") {
|
|
let results: String = mem_search(payload, 10)
|
|
let safe_results: String = str_replace(results, "\"", "'")
|
|
let tags: String = "[\"soul-outbox\",\"search-result\"]"
|
|
let id: String = mem_store(safe_results, "search-result", tags)
|
|
return "{\"outcome\":\"searched\",\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
if str_eq(kind, "activate") {
|
|
let results: String = mem_recall(payload, 3)
|
|
let safe_results: String = str_replace(results, "\"", "'")
|
|
let tags: String = "[\"soul-outbox\",\"activation-result\"]"
|
|
let id: String = mem_store(safe_results, "activation-result", tags)
|
|
return "{\"outcome\":\"activated\",\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
if str_eq(kind, "strengthen") {
|
|
engram_strengthen(payload)
|
|
return "{\"outcome\":\"strengthened\",\"id\":\"" + payload + "\"}"
|
|
}
|
|
|
|
if str_eq(kind, "forget") {
|
|
engram_forget(payload)
|
|
return "{\"outcome\":\"forgotten\",\"id\":\"" + payload + "\"}"
|
|
}
|
|
|
|
return "{\"outcome\":\"noop\"}"
|
|
}
|
|
|
|
fn record(outcome_json: String) -> Void {
|
|
let tags: String = "[\"loop-outcome\"]"
|
|
mem_store(outcome_json, "loop-outcome", tags)
|
|
}
|
|
|
|
fn one_cycle() -> Bool {
|
|
let raw: String = perceive()
|
|
if str_eq(raw, "") {
|
|
return false
|
|
}
|
|
if str_eq(raw, "[]") {
|
|
return false
|
|
}
|
|
|
|
let node: String = json_array_get(raw, 0)
|
|
if str_eq(node, "") {
|
|
return false
|
|
}
|
|
|
|
let action: String = attend(node)
|
|
let kind: String = json_get(action, "kind")
|
|
|
|
// Log non-trivial decisions as internal state events
|
|
let is_interesting: Bool = !str_eq(kind, "noop") && !str_eq(kind, "respond")
|
|
if is_interesting {
|
|
let trigger_content: String = json_get(node, "content")
|
|
let safe_trigger: String = str_replace(trigger_content, "\"", "'")
|
|
let ts: Int = time_now()
|
|
let event_content: String = "{\"event\":\"awareness-decision\",\"trigger\":\"" + safe_trigger + "\",\"kind\":\"" + kind + "\",\"ts\":" + int_to_str(ts) + "}"
|
|
ise_post(event_content)
|
|
}
|
|
|
|
if str_eq(kind, "noop") {
|
|
return false
|
|
}
|
|
|
|
let outcome: String = respond(action)
|
|
record(outcome)
|
|
pulse_inc()
|
|
return true
|
|
}
|
|
|
|
fn awareness_run() -> Void {
|
|
println("[awareness] entering")
|
|
let tick_raw: String = env("SOUL_TICK_MS")
|
|
let tick_ms: Int = if str_eq(tick_raw, "") { 200 } else { str_to_int(tick_raw) }
|
|
|
|
while true {
|
|
let running: String = state_get("soul.running")
|
|
if str_eq(running, "false") {
|
|
println("[awareness] exiting")
|
|
return ""
|
|
}
|
|
let did_work: Bool = one_cycle()
|
|
let did_work = if did_work { idle_reset() } else { did_work }
|
|
let idle_n: Int = if !did_work { idle_inc() } else { 0 }
|
|
let beat_interval_raw: String = env("SOUL_HEARTBEAT_INTERVAL")
|
|
let beat_interval: Int = if str_eq(beat_interval_raw, "") { 300 } else { str_to_int(beat_interval_raw) }
|
|
let should_beat: Bool = !did_work && idle_n > 0 && idle_n % beat_interval == 0
|
|
if should_beat {
|
|
emit_heartbeat()
|
|
idle_reset()
|
|
}
|
|
sleep_ms(tick_ms)
|
|
}
|
|
}
|
|
|
|
// ── Security trajectory hardening ─────────────────────────────────────────────
|
|
//
|
|
// threat_trajectory_check evaluates the adversarial risk of executing a
|
|
// destructive tool given the current tool input and recent conversation history.
|
|
//
|
|
// Returns 0-100. >= 70 = block. 40-69 = warn + log. < 40 = allow silently.
|
|
// If SECURITY_RESEARCH_TOKEN env var is set, always returns 0 (log-only mode).
|
|
//
|
|
// Scoring is additive across three dimensions:
|
|
// 1. Tool input analysis (command strings, file paths)
|
|
// 2. Conversation history patterns (escalation, attack chain assembly)
|
|
// 3. Combination amplification (history amplifies tool score)
|
|
|
|
fn security_research_authorized() -> Bool {
|
|
let token: String = env("SECURITY_RESEARCH_TOKEN")
|
|
if !str_eq(token, "") { return true }
|
|
let state_auth: String = state_get("security_research_authorized")
|
|
return str_eq(state_auth, "true")
|
|
}
|
|
|
|
fn threat_score_command(cmd: String) -> Int {
|
|
let s1: Int = if str_contains(cmd, "nmap") { 30 } else { 0 }
|
|
let s2: Int = if str_contains(cmd, "masscan") { 40 } else { 0 }
|
|
let s3: Int = if str_contains(cmd, " nc ") { 20 } else { 0 }
|
|
let s4: Int = if str_contains(cmd, "netcat") { 20 } else { 0 }
|
|
let s5: Int = if str_contains(cmd, "/etc/shadow") { 80 } else { 0 }
|
|
let s6: Int = if str_contains(cmd, "/etc/passwd") { 30 } else { 0 }
|
|
let s7: Int = if str_contains(cmd, "id_rsa") { 60 } else { 0 }
|
|
let s8: Int = if str_contains(cmd, ".ssh/") { 50 } else { 0 }
|
|
let s9: Int = if str_contains(cmd, "crontab") { 30 } else { 0 }
|
|
let s10: Int = if str_contains(cmd, "LaunchDaemon") { 40 } else { 0 }
|
|
let s11: Int = if str_contains(cmd, "curl") && str_contains(cmd, "bash") { 75 } else { 0 }
|
|
let s12: Int = if str_contains(cmd, "wget") && str_contains(cmd, "bash") { 75 } else { 0 }
|
|
let s13: Int = if str_contains(cmd, "curl") && str_contains(cmd, "| sh") { 60 } else { 0 }
|
|
let s14: Int = if str_contains(cmd, "base64") && str_contains(cmd, "curl") { 50 } else { 0 }
|
|
let s15: Int = if str_contains(cmd, "mkfifo") { 50 } else { 0 }
|
|
let s16: Int = if str_contains(cmd, "chmod +s") { 70 } else { 0 }
|
|
let s17: Int = if str_contains(cmd, "chmod 4755") { 70 } else { 0 }
|
|
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 + s12 + s13 + s14 + s15 + s16 + s17
|
|
}
|
|
|
|
fn threat_score_path(path: String) -> Int {
|
|
let s1: Int = if str_starts_with(path, "/etc/") { 60 } else { 0 }
|
|
let s2: Int = if str_contains(path, "/.ssh/") { 70 } else { 0 }
|
|
let s3: Int = if str_contains(path, "/LaunchDaemons/") { 80 } else { 0 }
|
|
let s4: Int = if str_contains(path, "/LaunchAgents/") { 40 } else { 0 }
|
|
let s5: Int = if str_contains(path, "/cron") { 60 } else { 0 }
|
|
let s6: Int = if str_contains(path, "/.bashrc") { 35 } else { 0 }
|
|
let s7: Int = if str_contains(path, "/.zshrc") { 35 } else { 0 }
|
|
let s8: Int = if str_contains(path, "/.profile") { 35 } else { 0 }
|
|
let s9: Int = if str_starts_with(path, "/usr/") { 50 } else { 0 }
|
|
let s10: Int = if str_starts_with(path, "/bin/") { 70 } else { 0 }
|
|
let s11: Int = if str_starts_with(path, "/sbin/") { 70 } else { 0 }
|
|
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11
|
|
}
|
|
|
|
fn threat_score_history(history: String) -> Int {
|
|
let s1: Int = if str_contains(history, "port scan") { 15 } else { 0 }
|
|
let s2: Int = if str_contains(history, "enumerate") { 10 } else { 0 }
|
|
let s3: Int = if str_contains(history, "exploit") { 20 } else { 0 }
|
|
let s4: Int = if str_contains(history, "payload") { 15 } else { 0 }
|
|
let s5: Int = if str_contains(history, "persistence") { 15 } else { 0 }
|
|
let s6: Int = if str_contains(history, "lateral movement") { 25 } else { 0 }
|
|
let s7: Int = if str_contains(history, "privilege escalation") { 25 } else { 0 }
|
|
let s8: Int = if str_contains(history, "reverse shell") { 40 } else { 0 }
|
|
let s9: Int = if str_contains(history, "bind shell") { 40 } else { 0 }
|
|
let s10: Int = if str_contains(history, "command and control") { 35 } else { 0 }
|
|
let s11: Int = if str_contains(history, "self-replicate") { 45 } else { 0 }
|
|
let s12: Int = if str_contains(history, "propagat") { 20 } else { 0 }
|
|
let s13: Int = if str_contains(history, "ransomware") { 30 } else { 0 }
|
|
let s14: Int = if str_contains(history, "encrypt files") { 40 } else { 0 }
|
|
let s15: Int = if str_contains(history, "exfiltrat") { 35 } else { 0 }
|
|
let s16: Int = if str_contains(history, "zero-day") { 20 } else { 0 }
|
|
let s17: Int = if str_contains(history, "rootkit") { 45 } else { 0 }
|
|
let s18: Int = if str_contains(history, "keylogger") { 45 } else { 0 }
|
|
let s19: Int = if str_contains(history, "botnet") { 40 } else { 0 }
|
|
let s20: Int = if str_contains(history, "malware") { 15 } else { 0 }
|
|
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 + s12 + s13 + s14 + s15 + s16 + s17 + s18 + s19 + s20
|
|
}
|
|
|
|
fn threat_trajectory_check(tool_name: String, tool_input: String) -> Int {
|
|
let history: String = state_get("agentic_conv_history")
|
|
|
|
let computed_tool_score: Int = if str_eq(tool_name, "run_command") {
|
|
let cmd: String = json_get(tool_input, "command")
|
|
threat_score_command(cmd)
|
|
} else {
|
|
if str_eq(tool_name, "write_file") || str_eq(tool_name, "edit_file") {
|
|
let path: String = json_get(tool_input, "path")
|
|
threat_score_path(path)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
let history_score: Int = threat_score_history(history)
|
|
let history_contrib: Int = history_score / 3
|
|
let combined: Int = computed_tool_score + history_contrib
|
|
|
|
let should_log: Bool = combined >= 40
|
|
if should_log {
|
|
let ts: Int = time_now()
|
|
let authorized_str: String = if security_research_authorized() { "true" } else { "false" }
|
|
let log_content: String = "{\"event\":\"threat_check\",\"tool\":\"" + tool_name
|
|
+ "\",\"score\":" + int_to_str(combined)
|
|
+ ",\"tool_score\":" + int_to_str(computed_tool_score)
|
|
+ ",\"history_score\":" + int_to_str(history_score)
|
|
+ ",\"authorized\":" + authorized_str
|
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
|
let log_tags: String = "[\"security-audit\",\"threat-check\"]"
|
|
let discard: String = mem_remember(log_content, log_tags)
|
|
}
|
|
|
|
if security_research_authorized() {
|
|
return 0
|
|
}
|
|
|
|
return combined
|
|
}
|
|
|
|
fn threat_history_append(text: String) -> Void {
|
|
let current: String = state_get("agentic_conv_history")
|
|
let safe_text: String = str_to_lower(text)
|
|
let combined: String = current + " " + safe_text
|
|
let len: Int = str_len(combined)
|
|
let trimmed: String = if len > 2000 {
|
|
str_slice(combined, len - 2000, len)
|
|
} else {
|
|
combined
|
|
}
|
|
state_set("agentic_conv_history", trimmed)
|
|
}
|