bef36a5e3a
`runStructuralAudit` has been an advertised MCP tool with nothing behind it: the
dispatcher GET'd /session/begin and returned that unrelated session digest under
an audit tool's name. Meanwhile the failure the audit would have caught ran
silently for about three weeks — the soul reporting 103,089 nodes while the
engram, which OWNS persistence, held ~79,900, a crash discarding the difference,
and every boot reporting green throughout, because nothing in the system ever
compared the two sides.
WHAT THE PATENT SPECIFIES, AND HOW IT SHAPED THIS
CGI provisional, 05-detailed-description.md, "Stage 1: Structural audit 430".
Two clauses did the design work. First the four things the module evaluates:
the density and typed distribution of causal edges; value/execution-record
consistency; the richness and connectivity of the self-model; and wonder-
manifest authenticity. Second, and decisively: it "produces a coherence
assessment 432 — NOT A BINARY SCORE but an annotated characterization of the
graph's structural properties."
So every finding carries its numbers AND a plain-language note saying what
they mean and how they were obtained. There is no pass/fail and no composite
health figure, and `"score":null` is emitted explicitly so a reader cannot
mistake its absence for an omission.
WHAT IS IN STAGE 1 (four findings)
owner_runtime_divergence — the motivating case. Runtime counts vs the owner's
own GET /api/stats, the delta, and the trend against the previous audit, so
a second call answers "is the gap growing?" rather than restating it.
self_model_connectivity — the three identity pillars plus the self root:
present, content length, one-hop degree. This RETIRES the Claude-side vitals
identity block, which lived outside the system it was checking and went on
reporting green while the memory-philosophy pillar was absent from the live
graph. Asking the running soul is the designed mechanism; a shell probe was
the fourth patch on the same hole.
typed_edge_distribution — exact counts against the claim-10 vocabulary, plus
density, plus a separate count of LOWERCASE near-misses ("causes" vs
"Causes"): "the vocabulary is unused" and "the vocabulary is misspelled by
the write paths" are different defects with different fixes.
orphans_and_dangling_edges — the tool's own long-standing promise.
WHAT IS DEFERRED, AND WHY IT IS DATA RATHER THAN A COMMENT
Value/execution-record consistency and wonder-manifest authenticity ship as a
`deferred` array that MEASURES the populations they would need (Prediction and
WonderQuestion nodes) and reports those counts as the reason. Both are ~0 today
— WonderQuestion because of a known write/read node-type mismatch. Asserting
value coherence or a pull-weight correlation on an empty population would be a
fabricated result, which is worse than a stated gap.
MEASUREMENT HONESTY: EXACT WHERE CHEAP, SAMPLED WHERE NOT, ALWAYS LABELLED
Counts, edge typing and self-model connectivity are exact. Orphan and dangling
rates are sampled, because engram_find_node_index is a linear scan — an
exhaustive dangling check is O(nodes x edges), ~2.2e9 string compares at today's
scale. Samples are UNIFORM across the whole population (str_index_of_all gives
every edge offset in one pass, so any index is O(1); json_array_get would have
been O(n^2)), never head-of-list, and each figure ships with its own sampled /
population / exhaustive fields. ?edge_sample= and ?node_sample= at population
size run either check exhaustively. The real fix is an id index in the runtime.
ONE BUG THIS FOUND IN ITSELF, CAUGHT IN TEST
http_get does not return "" when the owner is unreachable — it returns a JSON
error object. Testing only for "" made a DEAD owner read as reachable with
node_count 0, so the audit reported 100% divergence and named it data loss.
Reachability is now proved by the presence of the node_count field, and the
owner's raw reply is attached. A confident wrong answer is exactly what this
route exists to stop.
Edge findings need relation labels and the runtime has no edge-enumeration
builtin, so they use the same scratch export GET /api/graph/edges already uses
(engram_save to TMPDIR, never the owner's canonical file — #117). That is a
large write on a large graph, so this is a manual route, not a timer; ?edges=0
skips it.
neuron-api.el:900-1273 handler + helpers
routes.el:567,752 GET and POST /api/neuron/audit/structural
mcp-wrapper/src/main.el:113,682 tool description + dispatch off /session/begin
dist/soul.c regenerated (1255 bodies)
Rung: E2E-VERIFIED. Soul built from this branch (gen-soul-amalgam + cc-brain,
921,192 bytes, 16 warnings, 0 errors), booted on throwaway ports 7893/7896/7897
with throwaway HOMEs against a stub owner on 7894. Three scenarios pass: owner
reachable (runtime 62 vs owner 42, delta 20 / 32.2%, trend flat on the second
call; 12/20 edges claim-10 typed, 3 lowercase near-misses; 50/62 orphans, 3/20
dangling — every figure matches the fixture by construction), owner unreachable
(reported as a finding with the raw reply, not a crash), and file mode (owner
"none", divergence undefined). Reached end-to-end through the MCP tool via a
locally built wrapper. verify-soul-contract.sh: GATE PASS, 27/27 routes +
immutability. No process left running; live :7770 and :8742 untouched (GET only).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
834 lines
36 KiB
EmacsLisp
834 lines
36 KiB
EmacsLisp
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"
|
|
|
|
// flag_true — tolerant flag test: accepts both boolean `true` (Kotlin UI) and
|
|
// integer 1 (el-src UI). json_get_bool only recognises literal `true`, so
|
|
// without this wrapper an "agentic":1 request would silently route to the
|
|
// non-agentic path.
|
|
fn flag_true(body: String, key: String) -> Bool {
|
|
return json_get_bool(body, key) || json_get_int(body, key) > 0
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// plain_chat_envelope — the JSON response contract for a non-agentic ("Tools: Off")
|
|
// chat turn. Every /api/chat dispatch that calls layered_cycle goes through here, so
|
|
// the three call sites cannot drift apart.
|
|
//
|
|
// WHY THE ENVELOPE IS BUILT HERE AND NOT INSIDE layered_cycle:
|
|
// layered_cycle returns the user-facing text AFTER safety_validate has acted on it.
|
|
// Keeping the JSON out of the cycle means the output gate always sees raw model text
|
|
// and never an escaped blob — there is nothing to unwrap and re-wrap on the crisis
|
|
// path, which is exactly the failure mode that made wiring handle_chat unsafe.
|
|
// Escaping is the last thing that happens, strictly after the gate.
|
|
//
|
|
// FIELDS: `reply` and `response` carry the same validated text. Both are required by
|
|
// live clients — the desktop app reads `reply` first (DaemonClient.parseChatResponse),
|
|
// while the CLI tools and the Telegram gateway read `response` (the gateway reads only
|
|
// `response`). Emitting one would break the other.
|
|
//
|
|
// EMPTY MEANS FAILURE, NOT AN EMPTY ANSWER: a hard bell returns the fixed crisis
|
|
// message and a soft bell is padded to non-empty by safety_validate, so the only way
|
|
// an empty string leaves the cycle is a failed model call. It is reported as an error
|
|
// rather than dressed up as a successful blank reply.
|
|
// ---------------------------------------------------------------------------
|
|
fn plain_chat_envelope(validated: String, model: String) -> String {
|
|
if str_eq(validated, "") {
|
|
return "{\"error\":\"llm unavailable\",\"reply\":\"\",\"response\":\"\",\"agentic\":false,\"tools_used\":[]}"
|
|
}
|
|
let safe: String = json_safe(validated)
|
|
return "{\"reply\":\"" + safe + "\""
|
|
+ ",\"response\":\"" + safe + "\""
|
|
+ ",\"model\":\"" + json_safe(model) + "\""
|
|
+ ",\"agentic\":false"
|
|
+ ",\"tools_used\":[]}"
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rate limiting — simple in-memory per-IP sliding window counter.
|
|
//
|
|
// State keys:
|
|
// rl:<ip>:count — request count in the current window
|
|
// rl:<ip>: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 = wt_node(
|
|
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 = wt_node(
|
|
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\"]"
|
|
wt_node(
|
|
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 req_mode: String = json_get(chat_body, "mode")
|
|
let reply: String = if str_eq(req_mode, "plan") {
|
|
handle_chat_plan(chat_body)
|
|
} else if agentic_flag {
|
|
handle_chat_agentic(chat_body)
|
|
} else {
|
|
// Non-agentic ("Tools: Off"): the full L1→L2→L3→L1 cycle, which now generates
|
|
// at L3 instead of echoing. Envelope built outside the cycle — see
|
|
// plain_chat_envelope.
|
|
// FIX B/E1 (2026-08-05): the cycle is told which conversation it is in, and
|
|
// whether this generation is conversation at all. Same two arguments at all
|
|
// three dispatch sites.
|
|
let screened_reply: String = layered_cycle(raw_msg, json_get(chat_body, "session_id"), is_utility_request(chat_body, json_get(chat_body, "session_id")))
|
|
plain_chat_envelope(screened_reply, chat_default_model())
|
|
}
|
|
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)
|
|
}
|
|
// Call a connector tool directly (pre-chat), e.g. WhatsApp get_pairing_qr / get_login_status for
|
|
// the pairing UI. Body: {"name":"mcp__<server>__<tool>","input":{...}}. Keeps the app on the
|
|
// app->soul->connectd path (the UI never hits connectd directly) and works for remote/hosted apps.
|
|
if str_eq(clean, "/api/connectors/call") {
|
|
return connectd_post("/mcp/call", body)
|
|
}
|
|
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
|
|
}
|
|
|
|
// handle_request — the soul's HTTP entry point.
|
|
//
|
|
// NOTE ON THE NAME (neuron#117): the el runtime resolves this handler by NAME
|
|
// via dlsym(RTLD_DEFAULT, "handle_request") — that is why the Linux build must
|
|
// link -rdynamic. So the dispatcher body moved to route_dispatch and the name
|
|
// `handle_request` stays put as a thin wrapper. Do not rename it back.
|
|
//
|
|
// The wrapper exists to give the write-through boundary a guaranteed flush
|
|
// point. route_dispatch returns from ~60 places; a per-branch flush would be
|
|
// forgotten on the 61st. Draining here means EVERY request that staged a write
|
|
// pushes it before the connection closes, whatever route produced it, including
|
|
// routes added later that know nothing about persistence.
|
|
//
|
|
// wt_drain is a no-op (no HTTP, no cost) when nothing is staged and when the
|
|
// soul is not in HTTP-engram mode, so this is free on read traffic.
|
|
fn handle_request(method: String, path: String, body: String) -> String {
|
|
let resp: String = route_dispatch(method, path, body)
|
|
let flushed: Int = wt_drain()
|
|
return resp
|
|
}
|
|
|
|
fn route_dispatch(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
|
|
// ACTIVITY STAMP (2026-07-30 self-review): every inbound HTTP request —
|
|
// MCP wrapper calls, chat, API — marks real external activity. Before
|
|
// this, "idle" was only reset by rare inbox synthesis-requests, so the
|
|
// heartbeat idle field tracked uptime exactly (idle == pulse on every
|
|
// beat) and carried zero information. The awareness heartbeat now
|
|
// reports idle_ms = wall-clock ms since this stamp.
|
|
state_set("soul.last_activity_ts", int_to_str(time_now()))
|
|
|
|
// 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") {
|
|
// FIXED (neuron#117): this GET used to engram_save() straight over
|
|
// ~/.neuron/engram/snapshot.json — a READ route, in a process that is
|
|
// NOT the persistence owner, overwriting the owner's canonical file
|
|
// on every call. It broke soul.el:571-573 ("the soul must NEVER write
|
|
// to the local snapshot") and it is the same defect class Will removed
|
|
// from the engram itself in el `dc39a61` ("stop read routes clobbering
|
|
// canonical snapshot"), where route_scan_edges/route_sync were moved
|
|
// to scratch paths for exactly this reason. It was also the race the
|
|
// old TODO(reliability #8) admitted to.
|
|
//
|
|
// Export to a scratch path instead. Same response, no canonical write.
|
|
// The soul's own snapshot writes are otherwise already gated behind
|
|
// state key "soul_snapshot_path", which is set ONLY in the genesis
|
|
// file-mode branch (soul.el: is_genesis && safe_to_seed, and
|
|
// safe_to_seed is unconditionally false when ENGRAM_URL is set) — so
|
|
// after this change the soul writes nothing at all in HTTP mode.
|
|
// Future: add an engram_edges_json() builtin and drop the file round
|
|
// trip entirely.
|
|
let scratch_dir: String = env("TMPDIR")
|
|
let scratch_base: String = if str_eq(scratch_dir, "") { "/tmp" } else { scratch_dir }
|
|
let snap_path: String = scratch_base + "/soul-edges-export-" + state_get("soul_cgi_id") + ".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 req_mode: String = json_get(body, "mode")
|
|
let reply: String = if str_eq(req_mode, "plan") {
|
|
handle_chat_plan(body)
|
|
} else if agentic_flag {
|
|
handle_chat_agentic(body)
|
|
} else {
|
|
// Non-agentic ("Tools: Off") — same cycle and same envelope as POST.
|
|
// FIX B/E1: same threading. A GET probe usually carries no session_id, which
|
|
// resolves to the anonymous window — the documented behaviour for this door.
|
|
let screened_reply: String = layered_cycle(eff_msg, json_get(body, "session_id"), is_utility_request(body, json_get(body, "session_id")))
|
|
plain_chat_envelope(screened_reply, chat_default_model())
|
|
}
|
|
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)
|
|
}
|
|
// Stage 1 structural audit (CGI provisional, "Structural audit 430").
|
|
// GET because it is a read of the graph's own structure; the query string
|
|
// carries the sample caps (?edge_sample=, ?node_sample=, ?edges=0), so
|
|
// str_starts_with rather than str_eq.
|
|
if str_starts_with(clean, "/api/neuron/audit/structural") {
|
|
return handle_api_structural_audit(method, path, body)
|
|
}
|
|
if str_starts_with(clean, "/api/neuron/list/") {
|
|
// Offset 17 = len("/api/neuron/list/"). Was 16, which left a leading "/" on node_type
|
|
// ("/BacklogItem"), so engram_scan_nodes_by_type_json matched nothing → list/<type>
|
|
// returned [] for EVERY type (broke backlog/typed-node listing app- and tool-wide).
|
|
let node_type: String = str_slice(clean, 17, 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/run-progress/:session_id — live agentic-run ledger (2026-07-13,
|
|
// narrated-runs). agentic_loop appends one {"i","t","tool"} entry per round
|
|
// (the model's own pre-tool narration); a {"done":true} entry closes the run.
|
|
// Clients poll this during a run to render live step updates without streaming.
|
|
if str_starts_with(clean, "/api/run-progress/") {
|
|
let rp_id: String = str_slice(clean, 18, str_len(clean))
|
|
if !str_eq(rp_id, "") {
|
|
let rp_raw: String = state_get("run_progress_" + rp_id)
|
|
let rp_arr: String = if str_eq(rp_raw, "") { "[]" } else { "[" + rp_raw + "]" }
|
|
return "{\"progress\":" + rp_arr + "}"
|
|
}
|
|
}
|
|
// 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 req_mode: String = json_get(body, "mode")
|
|
let reply: String = if str_eq(req_mode, "plan") {
|
|
handle_chat_plan(body)
|
|
} else if agentic_flag {
|
|
handle_chat_agentic(body)
|
|
} else {
|
|
// Non-agentic ("Tools: Off") — the app's DEFAULT mode (AgentMode.NEVER).
|
|
// Full L1→L2→L3→L1 cycle with real generation at L3; envelope built
|
|
// outside the cycle so safety_validate always sees raw text.
|
|
// FIX B/E1: same threading. This is the app's main plain-chat door, so this
|
|
// is the site that ends the blank stare in practice.
|
|
let screened_reply: String = layered_cycle(raw_msg, json_get(body, "session_id"), is_utility_request(body, json_get(body, "session_id")))
|
|
plain_chat_envelope(screened_reply, chat_default_model())
|
|
}
|
|
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)
|
|
}
|
|
// POST accepted too: same handler, so a JSON-RPC-shaped caller that only
|
|
// speaks POST reaches the identical audit. Options still come from the
|
|
// query string — the handler reads no body fields.
|
|
if str_eq(clean, "/api/neuron/audit/structural") {
|
|
return handle_api_structural_audit(method, path, 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)
|
|
}
|