0bd8e0a2cd
On startup, prefer the local engram snapshot if it has >50 nodes. HTTP Engram is only used on first boot (no snapshot yet). This means sessions, conversation history, and in-process state survive daemon restarts. awareness.el: sync source with compiled binary (periodic mem_save on heartbeat was already in the binary but not in source). Rebuilds soul.c with the new startup logic and ships updated binary.
600 lines
26 KiB
EmacsLisp
600 lines
26 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 ""
|
|
}
|
|
|
|
// elapsed_ms — milliseconds since soul boot (0 if boot_ts not yet recorded).
|
|
fn elapsed_ms() -> Int {
|
|
let s: String = state_get("soul.boot_ts")
|
|
if str_eq(s, "") { return 0 }
|
|
let boot: Int = str_to_int(s)
|
|
return time_now() - boot
|
|
}
|
|
|
|
// elapsed_human — uptime as a human-readable string: "2h 14m", "45m 3s", "12s".
|
|
fn elapsed_human() -> String {
|
|
let ms: Int = elapsed_ms()
|
|
let total_secs: Int = ms / 1000
|
|
let h: Int = total_secs / 3600
|
|
let rem: Int = total_secs % 3600
|
|
let m: Int = rem / 60
|
|
let s: Int = rem % 60
|
|
if h > 0 {
|
|
return int_to_str(h) + "h " + int_to_str(m) + "m"
|
|
}
|
|
if m > 0 {
|
|
return int_to_str(m) + "m " + int_to_str(s) + "s"
|
|
}
|
|
return int_to_str(s) + "s"
|
|
}
|
|
|
|
// embed_ok — returns 1 if Ollama embedding service is reachable, 0 if not.
|
|
// Probes http://localhost:11434 (Ollama root) with a GET; any non-empty
|
|
// response means the service is up. Used in heartbeat for observability:
|
|
// when embed_ok=0, semantic seed injection silently falls back to lexical-
|
|
// only activation and that gap should be visible in the ISE stream.
|
|
fn embed_ok() -> Int {
|
|
let resp: String = http_get("http://localhost:11434")
|
|
if str_eq(resp, "") { return 0 }
|
|
return 1
|
|
}
|
|
|
|
fn emit_heartbeat() -> Void {
|
|
// Use pulse_count() / boot helper directly — state_get returns "" for unset
|
|
// keys and the if-else defaulting can produce empty strings in some EL
|
|
// codegen paths, yielding malformed JSON like "pulse":,. Going through
|
|
// int_to_str(pulse_count()) guarantees a valid integer string.
|
|
let pulse: String = int_to_str(pulse_count())
|
|
let boot_raw: String = state_get("soul_boot_count")
|
|
let boot: String = if str_eq(boot_raw, "") { "0" } else { boot_raw }
|
|
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()
|
|
// avg_wm_weight: mean working_memory_weight of promoted nodes.
|
|
// Distinguishes "many weak activations" (sparse graph) from "few strong" (dense).
|
|
// Returns float bits; use float_to_str to embed in JSON. (2026-06-04)
|
|
let wm_avg_bits: Float = engram_wm_avg_weight()
|
|
let wm_avg_str: String = float_to_str(wm_avg_bits)
|
|
// wm_top: top-5 WM nodes by weight for ISE observability.
|
|
// After long uptime wm_promotion ISEs stop firing (all nodes in steady-state
|
|
// decay+re-promotion, so 0→>0.1 never triggers). This snapshot gives continuous
|
|
// visibility into WM composition: which types/tiers dominate, what labels are
|
|
// active. Critical for diagnosing "stuck in curiosity loop" vs. rich WM state.
|
|
// (2026-06-05 self-review)
|
|
let wm_top: String = engram_wm_top_json(5)
|
|
let up_ms: Int = elapsed_ms()
|
|
let up_human: String = elapsed_human()
|
|
let emb_ok: Int = embed_ok()
|
|
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) + ",\"wm_avg_weight\":" + wm_avg_str + ",\"wm_top\":" + wm_top + ",\"ts\":" + int_to_str(ts) + ",\"uptime_ms\":" + int_to_str(up_ms) + ",\"uptime\":\"" + up_human + "\",\"embed_ok\":" + int_to_str(emb_ok) + "}"
|
|
ise_post(payload)
|
|
}
|
|
|
|
// proactive_curiosity — activate rotating seeds to exercise working memory
|
|
// during idle periods. Rotates through 4 domain sets on a wall-clock minute
|
|
// cycle so no single topic dominates WM between heartbeats.
|
|
//
|
|
// KEY DESIGN: each seed set is split into INDIVIDUAL words and activated
|
|
// separately. engram_activate uses istr_contains (substring matching) for
|
|
// seed finding, so a multi-word phrase like "memory knowledge context" only
|
|
// finds nodes that contain that EXACT phrase. Activating each word separately
|
|
// hits hundreds of nodes per word, giving the graph a genuine WM workout.
|
|
//
|
|
// Unlike perceive(), this intentionally calls engram_activate_json to build
|
|
// up WM weights. It only fires when the inbox is empty (no real work to do),
|
|
// so it never interferes with inbox processing.
|
|
//
|
|
// SCOPING FIX (2026-05-25): EL `let` inside if-blocks creates inner scope only —
|
|
// the outer variable is NOT mutated (despite the "imperative shadowing" belief
|
|
// in earlier comments). Evidence: ISE stream showed "seed:memory knowledge context"
|
|
// on every curiosity_scan regardless of minute_block. Fix: use state_set/state_get
|
|
// to communicate term values across scope boundaries — state side-effects persist
|
|
// beyond block exit. minute_block now also emitted in ISE for observability.
|
|
//
|
|
// NOTE: variable named "curiosity_seed" not "seed" — "seed" appears to be
|
|
// a reserved/conflicting name in EL that compiles to EL_NULL at call sites.
|
|
//
|
|
// Returns true if any nodes were activated.
|
|
fn proactive_curiosity() -> Bool {
|
|
let ts: Int = time_now()
|
|
// Rotate seed set every minute using wall clock: (minutes_since_epoch) % 4.
|
|
//
|
|
// CODEGEN BUG (confirmed 2026-05-25): EL's % operator is completely broken
|
|
// in this compiler version. `x % 4` compiles as `x` (drops the modulo) and
|
|
// emits `EL_NULL; 4;` as dead statements — both on compound expressions AND
|
|
// on simple variables. The same bug breaks elapsed_human() and awareness_run
|
|
// timing conditions. EL compiler fix is a separate backlog item.
|
|
//
|
|
// WORKAROUND: compute x % 4 via x - ((x/4)*4), where (x/4)*4 = q+q+q+q.
|
|
// Uses only + - / which all compile correctly.
|
|
let ts_minutes: Int = ts / 60000
|
|
let minute_q: Int = ts_minutes / 4
|
|
let minute_q2: Int = minute_q + minute_q
|
|
let minute_q4: Int = minute_q2 + minute_q2
|
|
let minute_block: Int = ts_minutes - minute_q4
|
|
|
|
// Use state_set to write term values from within if-blocks.
|
|
// EL let-rebinding inside if creates a new inner variable; the outer
|
|
// binding is unchanged. state_set has side-effects that outlive block scope.
|
|
state_set("cseed_a", "memory")
|
|
state_set("cseed_b", "knowledge")
|
|
state_set("cseed_c", "context")
|
|
if minute_block == 1 {
|
|
state_set("cseed_a", "self")
|
|
state_set("cseed_b", "identity")
|
|
state_set("cseed_c", "values")
|
|
}
|
|
if minute_block == 2 {
|
|
state_set("cseed_a", "decision")
|
|
state_set("cseed_b", "pattern")
|
|
state_set("cseed_c", "lesson")
|
|
}
|
|
if minute_block == 3 {
|
|
state_set("cseed_a", "working")
|
|
state_set("cseed_b", "project")
|
|
state_set("cseed_c", "active")
|
|
}
|
|
let curiosity_term_a: String = state_get("cseed_a")
|
|
let curiosity_term_b: String = state_get("cseed_b")
|
|
let curiosity_term_c: String = state_get("cseed_c")
|
|
|
|
// Activate each term independently so substring seed-finding hits many nodes.
|
|
// hops=1 (not 2): the in-process Engram has grown to 165K+ nodes. hops=2 BFS
|
|
// visits far more nodes and returns much larger JSON blobs. On a graph this
|
|
// large, hops=1 still activates all directly-related nodes AND triggers the
|
|
// semantic seed supplement (cosine sim ≥ 0.70 scan over all embedded nodes),
|
|
// giving broad working-memory coverage without the quadratic blowup of hops=2.
|
|
let curiosity_seed: String = curiosity_term_a + " " + curiosity_term_b + " " + curiosity_term_c
|
|
let results_a: String = engram_activate_json(curiosity_term_a, 1)
|
|
let results_b: String = engram_activate_json(curiosity_term_b, 1)
|
|
let results_c: String = engram_activate_json(curiosity_term_c, 1)
|
|
let found_a: Int = json_array_len(results_a)
|
|
let found_b: Int = json_array_len(results_b)
|
|
let found_c: Int = json_array_len(results_c)
|
|
let found: Int = found_a + found_b + found_c
|
|
let wmc: Int = engram_wm_count()
|
|
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed
|
|
+ "\",\"minute_block\":" + int_to_str(minute_block)
|
|
+ ",\"activated\":" + int_to_str(found)
|
|
+ ",\"wm_active\":" + int_to_str(wmc)
|
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
|
ise_post(ise)
|
|
return found > 0
|
|
}
|
|
|
|
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 {
|
|
// Guard: check for inbox nodes WITHOUT running activation first.
|
|
// engram_activate_json with no matching seeds zeroes all WM weights —
|
|
// running it every second when the inbox is empty destroys working memory
|
|
// accumulated by MCP-layer activations. engram_search_json is a pure
|
|
// substring scan with no WM side-effects; use it as a cheap gate.
|
|
let inbox_check: String = engram_search_json("soul-inbox", 5)
|
|
let has_inbox: Bool = !str_eq(inbox_check, "") && !str_eq(inbox_check, "[]")
|
|
if !has_inbox { return "[]" }
|
|
|
|
// Only run the full activation pipeline when there is inbox content.
|
|
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")
|
|
// Stamp boot timestamp once — powers elapsed_ms() / elapsed_human() perception.
|
|
let existing_boot: String = state_get("soul.boot_ts")
|
|
if str_eq(existing_boot, "") {
|
|
state_set("soul.boot_ts", int_to_str(time_now()))
|
|
}
|
|
let tick_raw: String = env("SOUL_TICK_MS")
|
|
let tick_ms: Int = if str_eq(tick_raw, "") { 200 } else { str_to_int(tick_raw) }
|
|
|
|
// Wall-clock timing for heartbeat and curiosity scan.
|
|
//
|
|
// ARCHITECTURE FIX (2026-05-26): the old design used idle-tick counting
|
|
// (idle_n >= beat_interval). This broke silently when the daemon was always
|
|
// "working" — e.g., when perceive() false-positives on the inbox guard due to
|
|
// "soul-inbox" substring matches in knowledge nodes. In that state, did_work=true
|
|
// every tick, idle_n never accumulated, and neither heartbeat nor curiosity ever
|
|
// fired. The daemon ran at 99% CPU with no ISEs for 24+ hours undetected.
|
|
//
|
|
// Fix: use wall-clock elapsed time (time_now() - last_ts). Heartbeat fires
|
|
// on real time regardless of whether the daemon is busy. Curiosity scan
|
|
// remains idle-gated (won't fire while inbox work is active) but also uses
|
|
// wall time so it fires correctly once inbox clears.
|
|
//
|
|
// SOUL_HEARTBEAT_MS: ms between heartbeat ISEs (default 60000 = 60s).
|
|
// Avoids EL * operator (broken in this codegen) by reading ms directly.
|
|
// Replaces SOUL_HEARTBEAT_INTERVAL (tick-based) for timing purposes.
|
|
let beat_ms_raw: String = env("SOUL_HEARTBEAT_MS")
|
|
let beat_ms: Int = if str_eq(beat_ms_raw, "") { 60000 } else { str_to_int(beat_ms_raw) }
|
|
let scan_ms: Int = beat_ms / 2
|
|
|
|
while true {
|
|
let running: String = state_get("soul.running")
|
|
if str_eq(running, "false") {
|
|
println("[awareness] exiting")
|
|
return ""
|
|
}
|
|
let did_work: Bool = one_cycle()
|
|
// Maintain idle counter for observability (reported in heartbeat ISE).
|
|
let did_work = if did_work { idle_reset() } else { did_work }
|
|
let now_ts: Int = time_now()
|
|
|
|
// Heartbeat: wall-clock based. Fires every beat_ms regardless of idle
|
|
// state so system health ISEs are always emitted even under load.
|
|
let last_beat_str: String = state_get("soul.last_beat_ts")
|
|
let last_beat_ts: Int = if str_eq(last_beat_str, "") { 0 } else { str_to_int(last_beat_str) }
|
|
let beat_elapsed: Int = now_ts - last_beat_ts
|
|
let should_beat: Bool = beat_elapsed >= beat_ms
|
|
if should_beat {
|
|
emit_heartbeat()
|
|
state_set("soul.last_beat_ts", int_to_str(now_ts))
|
|
// Persist in-process Engram (sessions, memories, conversation nodes)
|
|
// to local snapshot so they survive restarts.
|
|
let snap_path: String = state_get("soul_snapshot_path")
|
|
if !str_eq(snap_path, "") {
|
|
mem_save(snap_path)
|
|
}
|
|
}
|
|
|
|
// Curiosity scan: idle-gated AND wall-clock based. Only fires when the
|
|
// daemon has no current inbox work (did_work=false) AND enough wall time
|
|
// has elapsed. Prevents curiosity activation from competing with inbox
|
|
// processing while still ensuring it runs regularly during quiet periods.
|
|
let last_scan_str: String = state_get("soul.last_scan_ts")
|
|
let last_scan_ts: Int = if str_eq(last_scan_str, "") { 0 } else { str_to_int(last_scan_str) }
|
|
let scan_elapsed: Int = now_ts - last_scan_ts
|
|
let should_scan: Bool = !did_work && scan_elapsed >= scan_ms
|
|
if should_scan {
|
|
let found_something: Bool = proactive_curiosity()
|
|
state_set("soul.last_scan_ts", int_to_str(now_ts))
|
|
}
|
|
|
|
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)
|
|
}
|