import "memory.el" import "awareness.el" import "chat.el" import "studio.el" import "elp-input.el" import "neuron-api.el" import "sessions.el" import "soul.elh" // --------------------------------------------------------------------------- // Rate limiting — simple in-memory per-IP sliding window counter. // // State keys: // rl::count — request count in the current window // rl::window — window start timestamp (unix seconds) // // Limit: configurable via soul state key "soul_rate_limit" (requests per // minute). Falls back to 60 req/min if not set. The /health endpoint is // exempt so monitoring does not consume quota. // // State growth: each unique source IP accumulates exactly 2 state keys // (count + window) for the lifetime of the process. Per-IP storage is // bounded and constant; values reset on window expiry. In aggregate, state // grows linearly with distinct IPs — typical for a trusted-client service. // EL has no state_delete builtin, so keys from inactive IPs persist. // TODO: add state_delete sweep when the EL runtime exposes that primitive. // // Returns "" when the request is allowed, or a 429 JSON body when rejected. // --------------------------------------------------------------------------- fn rate_limit_check(ip: String, path: String) -> String { // Health checks are exempt — they must never be blocked. if str_eq(path, "/health") { return "" } let limit_str: String = state_get("soul_rate_limit") let limit: Int = if str_eq(limit_str, "") { 60 } else { str_to_int(limit_str) } let now: Int = time_now() let window_key: String = "rl:" + ip + ":window" let count_key: String = "rl:" + ip + ":count" let win_str: String = state_get(window_key) let win_start: Int = if str_eq(win_str, "") { now } else { str_to_int(win_str) } // New window every 60 seconds. let elapsed: Int = now - win_start let in_window: Bool = elapsed < 60 let prev_count_str: String = state_get(count_key) let prev_count: Int = if str_eq(prev_count_str, "") { 0 } else { str_to_int(prev_count_str) } // Reset window if expired. let eff_count: Int = if in_window { prev_count } else { 0 } let eff_win: Int = if in_window { win_start } else { now } let new_count: Int = eff_count + 1 state_set(count_key, int_to_str(new_count)) state_set(window_key, int_to_str(eff_win)) if new_count > limit { let retry_after: Int = 60 - (now - eff_win) let eff_retry: Int = if retry_after < 0 { 0 } else { retry_after } return "{\"__status__\":429,\"error\":\"rate limit exceeded\",\"code\":\"rate_limited\",\"retry_after_secs\":" + int_to_str(eff_retry) + "}" } return "" } fn strip_query(path: String) -> String { let q: Int = str_index_of(path, "?") if q < 0 { return path } return str_slice(path, 0, q) } fn err_404(path: String) -> String { return "{\"error\":\"not found\",\"code\":\"not_found\",\"path\":\"" + path + "\"}" } fn err_405(method: String, path: String) -> String { return "{\"error\":\"method not allowed\",\"code\":\"method_not_allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}" } fn route_health() -> String { let cgi_id: String = state_get("soul_cgi_id") let boot: String = state_get("soul_boot_count") let boot_num: String = if str_eq(boot, "") { "0" } else { boot } let node_ct: Int = engram_node_count() let edge_ct: Int = engram_edge_count() let pulse: String = state_get("soul.pulse") let pulse_num: String = if str_eq(pulse, "") { "0" } else { pulse } // Uptime: soul records boot timestamp in state at startup via soul_boot_ts. // Compute elapsed seconds; fall back to -1 if not yet set. let boot_ts_str: String = state_get("soul_boot_ts") let uptime_secs: Int = if str_eq(boot_ts_str, "") { -1 } else { time_now() - str_to_int(boot_ts_str) } // LLM connectivity: probe with a minimal call. Any non-error reply = ok. // Use a short, fixed prompt so this never counts against conversation history. let model: String = state_get("soul_model") let eff_model: String = if str_eq(model, "") { "claude-sonnet-4-5" } else { model } let llm_probe: String = llm_call_system(eff_model, "You are a health probe. Reply with the single word: ok", "ping") let llm_ok: Bool = !str_eq(llm_probe, "") && !str_starts_with(llm_probe, "{\"error\"") && !str_starts_with(llm_probe, "{\"type\":\"error\"") && !str_contains(llm_probe, "authentication_error") let llm_status: String = if llm_ok { "ok" } else { "unreachable" } return "{\"status\":\"alive\"" + ",\"cgi_id\":\"" + cgi_id + "\"" + ",\"boot\":" + boot_num + ",\"uptime_secs\":" + int_to_str(uptime_secs) + ",\"node_count\":" + int_to_str(node_ct) + ",\"edge_count\":" + int_to_str(edge_ct) + ",\"pulse\":" + pulse_num + ",\"llm\":\"" + llm_status + "\"" + ",\"layers\":{\"l0\":\"core\",\"l1\":\"safety\",\"l2\":\"stewardship\",\"l3\":\"" + imprint_current() + "\"}}" } fn route_lineage() -> String { let cgi_id: String = state_get("soul_cgi_id") let q: String = "lineage:" + cgi_id let results: String = engram_search_json(q, 1) let len: Int = json_array_len(results) if len <= 0 { return "{\"id\":\"" + cgi_id + "\"" + ",\"tier\":\"citizen\"" + ",\"is_founding\":true" + ",\"validation_attempts\":0" + ",\"training_sessions\":0" + ",\"is_sterile\":false}" } let raw: String = json_get_raw(results, "0") return raw } fn route_imprint_contextual(body: String) -> String { if str_eq(body, "") { return "{\"ok\":false,\"error\":\"empty body\"}" } let tags: String = "[\"imprint\",\"contextual\"]" let id: String = engram_node_full( body, "Entity", "imprint:contextual", el_from_float(0.7), el_from_float(0.6), el_from_float(0.9), "Working", tags ) if str_eq(id, "") { return "{\"ok\":false,\"error\":\"engram write failed\"}" } state_set("active_contextual_imprint", id) return "{\"ok\":true,\"id\":\"" + id + "\"}" } fn route_imprint_user(body: String) -> String { if str_eq(body, "") { return "{\"ok\":false,\"error\":\"empty body\"}" } let tags: String = "[\"imprint\",\"user\"]" let id: String = engram_node_full( body, "Entity", "imprint:user", el_from_float(0.7), el_from_float(0.6), el_from_float(0.9), "Working", tags ) if str_eq(id, "") { return "{\"ok\":false,\"error\":\"engram write failed\"}" } state_set("active_user_imprint", id) return "{\"ok\":true,\"id\":\"" + id + "\"}" } fn route_synthesize(body: String) -> String { if str_eq(body, "") { return "{\"error\":\"body is required\",\"code\":\"missing_param\"}" } let parent_a: String = json_get(body, "parent_a") let parent_b: String = json_get(body, "parent_b") if str_eq(parent_a, "") { return "{\"error\":\"parent_a is required\",\"code\":\"missing_param\"}" } if str_eq(parent_b, "") { return "{\"error\":\"parent_b is required\",\"code\":\"missing_param\"}" } let req: String = "synthesize " + parent_a + " " + parent_b let tags: String = "[\"soul-inbox-pending\",\"synthesis-request\"]" engram_node_full( req, "Entity", "synthesis-request", el_from_float(0.8), el_from_float(0.8), el_from_float(0.9), "Working", tags ) return "{\"mechanism\":\"did not engage\"}" } fn handle_dharma_recv(body: String) -> String { let content_raw: String = json_get(body, "content") let from_id: String = json_get(body, "from") let event_type: String = json_get(content_raw, "event_type") let payload: String = json_get(content_raw, "payload") let eff_event: String = if str_eq(event_type, "") { "chat" } else { event_type } let eff_payload: String = if str_eq(payload, "") { content_raw } else { payload } if str_eq(eff_event, "chat") { let msg: String = json_get(eff_payload, "message") let chat_body: String = if str_eq(msg, "") { "{\"message\":\"" + str_replace(str_replace(eff_payload, "\\", "\\\\"), "\"", "\\\"") + "\"}" } else { eff_payload } let agentic_flag: Bool = json_get_bool(eff_payload, "agentic") let raw_msg: String = json_get(chat_body, "message") let reply: String = if agentic_flag { handle_chat_agentic(chat_body) } else { let screened_reply: String = layered_cycle(raw_msg) screened_reply } auto_persist(chat_body, reply) return reply } if str_eq(eff_event, "memory") { let query: String = json_get(eff_payload, "query") let limit_str: String = json_get(eff_payload, "limit") let limit: Int = if str_eq(limit_str, "") { 20 } else { str_to_int(limit_str) } let q: String = if str_eq(query, "") { eff_payload } else { query } return engram_search_json(q, limit) } if str_eq(eff_event, "tool") { let path_field: String = json_get(eff_payload, "path") let method_field: String = json_get(eff_payload, "method") let tool_body: String = json_get(eff_payload, "body") let eff_method: String = if str_eq(method_field, "") { "POST" } else { method_field } return handle_tool(path_field, eff_method, tool_body) } if str_eq(eff_event, "see") { return handle_see(eff_payload) } if str_eq(eff_event, "health") { return route_health() } if str_eq(eff_event, "dharma_room_turn_agentic") { return handle_dharma_room_turn_agentic(eff_payload) } if str_eq(eff_event, "dharma_room_turn") { return handle_dharma_room_turn(eff_payload) } if str_eq(eff_event, "chat_as_soul") { return handle_chat_as_soul(eff_payload) } // ELP — Engram Language Protocol: two-layer activation, no LLM if str_eq(eff_event, "elp") { return handle_elp_chat(eff_payload) } return "{\"error\":\"unknown event_type\",\"event_type\":\"" + eff_event + "\"}" } // --------------------------------------------------------------------------- // MCP Connectors proxy — thin pass-through to neuron-connectd on :7771. // The UI talks to ONE origin (the soul); all MCP/config complexity lives in // the bridge. Bridge-down returns a clear error (not a panic). // --------------------------------------------------------------------------- fn connectd_get(suffix: String) -> String { let out: String = exec_capture("curl -s --max-time 5 http://127.0.0.1:7771" + suffix) if str_eq(out, "") { return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}" } return out } // POST passthrough: request body is written to a temp file and passed via -d @file // so arbitrary JSON cannot reach the shell as a command-line argument. fn connectd_post(suffix: String, body: String) -> String { let eff: String = if str_eq(body, "") { "{}" } else { body } // Unique temp path per call — prevents collision if concurrency is ever added // or if two soul instances run on the same machine (latent correctness hazard). let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + ".json" fs_write(tmp, eff) let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp) if str_eq(out, "") { return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}" } return out } fn handle_connectors(method: String, clean: String, body: String) -> String { if str_eq(method, "GET") { // /api/connectors -> each configured server with status, tools, auth, auto-approve. return connectd_get("/mcp/servers") } if str_eq(clean, "/api/connectors/add") { return connectd_post("/mcp/servers/add", body) } if str_eq(clean, "/api/connectors/toggle") { return connectd_post("/mcp/servers/toggle", body) } if str_eq(clean, "/api/connectors/auto-approve") { return connectd_post("/mcp/servers/auto-approve", body) } if str_eq(clean, "/api/connectors/remove") { return connectd_post("/mcp/servers/remove", body) } if str_eq(clean, "/api/connectors/secret") { return connectd_post("/mcp/servers/secret", body) } if str_eq(clean, "/api/connectors/oauth/start") { return connectd_post("/mcp/oauth/start", body) } return "{\"ok\":false,\"error\":\"unknown connectors route\"}" } fn handle_request(method: String, path: String, body: String) -> String { let clean: String = strip_query(path) // Rate limit check. Extract caller IP from REMOTE_ADDR env var (set by the // EL HTTP runtime for each request). Skip enforcement when empty so // loopback/internal callers are never blocked. let ip: String = env("REMOTE_ADDR") if !str_eq(ip, "") { let rl_result: String = rate_limit_check(ip, clean) if !str_eq(rl_result, "") { return rl_result } } if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") { return handle_dharma_recv(body) } if str_eq(method, "GET") { if str_eq(clean, "/health") { return route_health() } if str_eq(clean, "/lineage") { return route_lineage() } if str_eq(clean, "/api/graph") || str_eq(clean, "/api/graph/nodes") { return engram_scan_nodes_json(9999, 0) } if str_eq(clean, "/api/graph/edges") { // TODO(reliability #8): engram_save races with awareness loop mem_save(). // Both now use atomic write-to-temp+rename (el_runtime.c). Serialised // by engram_global_mu. Future: add engram_edges_json() builtin. let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.json" engram_save(snap_path) let snap: String = fs_read(snap_path) let edges_raw: String = json_get_raw(snap, "edges") return if str_eq(edges_raw, "") { "[]" } else { edges_raw } } if str_eq(clean, "/api/chat") { // GET /api/chat: pass through layered_cycle for consistency with POST path. // GET chat is a legacy probe interface; body may be empty for simple pings. let raw_msg: String = json_get(body, "message") let eff_msg: String = if str_eq(raw_msg, "") { body } else { raw_msg } if str_eq(eff_msg, "") { return "{\"error\":\"message is required\",\"code\":\"missing_param\"}" } let agentic_flag: Bool = json_get_bool(body, "agentic") let reply: String = if agentic_flag { handle_chat_agentic(body) } else { let screened_reply: String = layered_cycle(eff_msg) screened_reply } auto_persist(body, reply) return reply } if str_eq(clean, "/api/conversations") { return handle_conversations(method) } if str_eq(clean, "/api/config") { return handle_config(method, body) } if str_starts_with(clean, "/api/tools/") { return handle_tool(clean, method, body) } if str_starts_with(clean, "/api/dharma") { return handle_dharma(clean, method, body) } if str_starts_with(clean, "/api/nlg") { return handle_nlg(clean, method, body) } if str_starts_with(clean, "/api/memories") { return axon_get(clean) } if str_starts_with(clean, "/api/knowledge") { return axon_get(clean) } if str_starts_with(clean, "/api/backlog") { return axon_get(clean) } if str_starts_with(clean, "/api/artifacts") { return axon_get(clean) } if str_starts_with(clean, "/api/projects") { return axon_get(clean) } if str_starts_with(clean, "/api/imprints") { return axon_get(clean) } if str_eq(clean, "/") { return render_studio() } // Neuron cognitive API — GET endpoints if str_eq(clean, "/api/neuron/session/begin") { return handle_api_begin_session("") } if str_eq(clean, "/api/neuron/ctx") { return handle_api_compile_ctx("") } if str_eq(clean, "/api/safety-contact") { return handle_safety_contact_get() } if str_starts_with(clean, "/api/neuron/knowledge/search") { return handle_api_search_knowledge(method, path, body) } if str_eq(clean, "/api/neuron/knowledge") { return handle_api_browse_knowledge(path, body) } if str_starts_with(clean, "/api/neuron/processes") { return handle_api_browse_processes(method, path, body) } if str_starts_with(clean, "/api/neuron/state-events") { return handle_api_list_state_events(method, path, body) } if str_starts_with(clean, "/api/neuron/config") { return handle_api_inspect_config(path, body) } if str_starts_with(clean, "/api/neuron/graph") { return handle_api_inspect_graph(method, path, body) } if str_starts_with(clean, "/api/neuron/list/") { let node_type: String = str_slice(clean, 16, str_len(clean)) return handle_api_list_typed(node_type, path, body) } if str_starts_with(clean, "/api/neuron/recall") { return handle_api_recall(method, path, body) } if str_starts_with(clean, "/api/connectors") { return handle_connectors(method, clean, body) } // GET /api/sessions — list all sessions if str_eq(clean, "/api/sessions") { return session_list() } // GET /api/sessions/:id — get session metadata + history if str_starts_with(clean, "/api/sessions/") { let gs_after: String = str_slice(clean, 14, str_len(clean)) let gs_slash: Int = str_index_of(gs_after, "/") let gs_id: String = if gs_slash < 0 { gs_after } else { str_slice(gs_after, 0, gs_slash) } if !str_eq(gs_id, "") { return session_get(gs_id) } } return err_404(clean) } if str_eq(method, "POST") { // POST /api/sessions — create new session if str_eq(clean, "/api/sessions") { return session_create(body) } // MCP tool-bridge resume: POST /api/sessions/{id}/tool_result // The client executed a tool the soul could not run in-process (an MCP // connector/plugin) and posts the result back here so the agentic loop // continues. {id} is the session_id from the prior tool_pending envelope. if str_starts_with(clean, "/api/sessions/") && str_ends_with(clean, "/tool_result") { let after: String = str_slice(clean, 14, str_len(clean)) let slash: Int = str_index_of(after, "/") let session_id: String = if slash < 0 { after } else { str_slice(after, 0, slash) } return handle_tool_result(session_id, body) } // POST /api/sessions/:id/approve — user approval for a pending agentic tool call if str_starts_with(clean, "/api/sessions/") { let sess_after: String = str_slice(clean, 14, str_len(clean)) let sess_slash: Int = str_index_of(sess_after, "/") let sess_id: String = if sess_slash < 0 { sess_after } else { str_slice(sess_after, 0, sess_slash) } let sess_sub: String = if sess_slash < 0 { "" } else { str_slice(sess_after, sess_slash + 1, str_len(sess_after)) } if !str_eq(sess_id, "") && str_eq(sess_sub, "approve") { return handle_session_approve(sess_id, body) } } if str_eq(clean, "/imprint/contextual") { return route_imprint_contextual(body) } if str_eq(clean, "/imprint/user") { return route_imprint_user(body) } if str_eq(clean, "/synthesize") { return route_synthesize(body) } if str_eq(clean, "/api/elp/chat") { return handle_elp_chat(body) } if str_eq(clean, "/api/chat") { // NOTE: streaming (SSE / chunked transfer) is not implemented. All chat // responses are buffered and returned as a single JSON object. Streaming // would require runtime-level SSE support in el_runtime.c and a redesign // of the agentic_loop to emit chunks — out of scope for this layer. let raw_msg: String = json_get(body, "message") if str_eq(raw_msg, "") { return "{\"error\":\"message is required\",\"code\":\"missing_param\"}" } let agentic_flag: Bool = json_get_bool(body, "agentic") let reply: String = if agentic_flag { handle_chat_agentic(body) } else { let screened_reply: String = layered_cycle(raw_msg) screened_reply } auto_persist(body, reply) return reply } if str_eq(clean, "/api/see") { return handle_see(body) } if str_eq(clean, "/api/conversations") { return handle_conversations(method) } if str_eq(clean, "/api/config") { return handle_config(method, body) } if str_starts_with(clean, "/api/tools/") { return handle_tool(clean, method, body) } if str_starts_with(clean, "/api/dharma") { return handle_dharma(clean, method, body) } if str_starts_with(clean, "/api/nlg") { return handle_nlg(clean, method, body) } if str_starts_with(clean, "/api/memories") { return axon_post(clean, body) } if str_starts_with(clean, "/api/knowledge") { return axon_post(clean, body) } if str_starts_with(clean, "/api/backlog") { return axon_post(clean, body) } if str_starts_with(clean, "/api/artifacts") { return axon_post(clean, body) } if str_starts_with(clean, "/api/projects") { return axon_post(clean, body) } if str_starts_with(clean, "/api/imprints") { return axon_post(clean, body) } // Neuron cognitive API — POST endpoints if str_eq(clean, "/api/neuron/session/begin") { return handle_api_begin_session(body) } if str_eq(clean, "/api/neuron/ctx") { return handle_api_compile_ctx(body) } if str_eq(clean, "/api/neuron/knowledge/search") { return handle_api_search_knowledge(method, path, body) } if str_eq(clean, "/api/neuron/knowledge/capture") { return handle_api_capture_knowledge(body) } if str_eq(clean, "/api/neuron/knowledge/evolve") { return handle_api_evolve_knowledge(body) } if str_eq(clean, "/api/neuron/knowledge/promote") { return handle_api_promote_knowledge(body) } if str_eq(clean, "/api/neuron/processes") { return handle_api_browse_processes(method, path, body) } if str_eq(clean, "/api/neuron/processes/define") { return handle_api_define_process(body) } if str_eq(clean, "/api/neuron/state-events") { return handle_api_log_state_event(body) } if str_eq(clean, "/api/neuron/config") { return handle_api_inspect_config(path, body) } if str_eq(clean, "/api/neuron/config/tune") { return handle_api_tune_config(body) } if str_eq(clean, "/api/neuron/graph") { return handle_api_inspect_graph(method, path, body) } if str_eq(clean, "/api/neuron/graph/link") { return handle_api_link_entities(body) } if str_eq(clean, "/api/neuron/memory") { return handle_api_remember(body) } if str_eq(clean, "/api/safety-contact") { return handle_safety_contact_post(body) } if str_eq(clean, "/api/neuron/node/create") { return handle_api_node_create(body) } if str_eq(clean, "/api/neuron/node/update") { return handle_api_node_update(body) } if str_eq(clean, "/api/neuron/node/delete") { return handle_api_node_delete(body) } if str_eq(clean, "/api/neuron/memory/evolve") { return handle_api_evolve_memory(body) } if str_eq(clean, "/api/neuron/memory/forget") { return handle_api_forget(body) } if str_eq(clean, "/api/neuron/memory/delete") { return handle_api_memory_delete(body) } if str_eq(clean, "/api/neuron/memory/update") { return handle_api_memory_update(body) } if str_eq(clean, "/api/neuron/recall") { return handle_api_recall(method, path, body) } if str_eq(clean, "/api/neuron/consolidate") { return handle_api_consolidate(body) } if str_eq(clean, "/api/neuron/cultivate") { return handle_api_cultivate(body) } if str_starts_with(clean, "/api/connectors") { return handle_connectors(method, clean, body) } return err_404(clean) } if str_eq(method, "DELETE") { // DELETE /api/sessions/:id — delete a session and its history if str_starts_with(clean, "/api/sessions/") { let del_after: String = str_slice(clean, 14, str_len(clean)) let del_slash: Int = str_index_of(del_after, "/") let del_id: String = if del_slash < 0 { del_after } else { str_slice(del_after, 0, del_slash) } if !str_eq(del_id, "") { return session_delete(del_id) } } return err_404(clean) } if str_eq(method, "PATCH") { // PATCH /api/sessions/:id — update session title and/or folder if str_starts_with(clean, "/api/sessions/") { let patch_after: String = str_slice(clean, 14, str_len(clean)) let patch_slash: Int = str_index_of(patch_after, "/") let patch_id: String = if patch_slash < 0 { patch_after } else { str_slice(patch_after, 0, patch_slash) } if !str_eq(patch_id, "") { return session_update_patch(patch_id, body) } } return err_404(clean) } return err_405(method, clean) }