Compare commits

...

9 Commits

Author SHA1 Message Date
will.anderson a5f411e739 routes: convert manual dispatch to @route + fix latent Bool/String crashes
Neuron Soul CI / build (pull_request) Failing after 45s
Neuron Soul CI / deploy (pull_request) Failing after 10m28s
Convert routes.el from ~89 hand-written dispatch branches to 75 @route-decorated
handlers with VBD roles (@manager/@accessor/@utility), driven by the modular
compiler's native @route dispatch. routes.elh updated to match.

Fix four latent bugs that the modular compiler surfaces (unlike the old inlining
compiler, it faithfully emits every source statement, so per-module compilation
no longer silently drops code):
  - safety.el: malformed soft-phrases literal (extra unescaped quote)
  - sessions.el: str_replace quote arg + topic_tags JSON escaping
  - memory.el mem_save and neuron-api.el consolidate: engram_save returns a Bool,
    not a String; str_eq(result, "") dereferenced the Bool value (0x1) as a char*,
    segfaulting the awareness loop on boot and POST /api/neuron/consolidate.
    Check it as a Bool (if !save_result). The old compiler dropped these checks,
    masking the bug in the shipped dist/soul.c.

Regenerate dist/soul.c through the bounded per-module path (modular elc per module
plus amalgamation that embeds dist/elp-c-decls.h), never by folding soul.el through
elc (27GB OOM). Verified on a throwaway snapshot and port: boots with the awareness
loop active and no segfault, @route dispatch matches the manual dispatcher across
all sampled routes and the four shadowing-hazard pairs, and consolidate returns 200.
2026-08-10 17:53:17 -05:00
will.anderson c8cb425412 soul: per-tick arena bracketing in awareness_run + hand-patched dist/soul.c
Neuron Soul CI / build (push) Successful in 5m14s
Neuron Soul CI / deploy (push) Failing after 10m9s
awareness_run's while-loop ran outside any request arena, so every
allocation in every 1s tick (search JSON, heartbeat payloads, curiosity
activations) was treated as permanent by the runtime — 7.5GB RSS in
under a minute. Bracket each iteration with el_arena_push/el_arena_pop
(same pattern the compiler emits for scoped blocks; state_set/state_get
persist separately via el_strdup_persist and are unaffected).

dist/soul.c carries the same change hand-patched at the compiled
awareness_run site — elc is currently unsafe to run locally (pathological
memory on sessions.el), so the generated C was patched to match the
source, verified line-for-line against the compiler's own conventions.

MUST be paired with el repo PR #64 (el_strdup_persist for stored engram
fields): per-tick arena reclamation widens the write-corruption window
without it. Verified together: 5h live soak on the recovered production
snapshot, flat RSS, write-field-integrity clean.

Note: dist/soul.c still needs a full elc regen to pick up PR #73's
source changes (consent tiers) — tracked separately; this patch does not
regress that (those changes were never in dist).
2026-07-13 16:24:15 -05:00
will.anderson 3e7aa0fff4 Merge pull request 'BUG-8 — engine-side agent consent tiers + run_command workspace fence (needs soul.c regen)' (#73) from feat/agent-phase1-soul into main
Neuron Soul CI / build (push) Successful in 7m35s
Neuron Soul CI / deploy (push) Failing after 5m41s
2026-07-13 16:22:03 +00:00
Tim Lingo aa67f86f90 propose(agentic): narrated runs — live run-progress ledger + narration on the pause envelope
Neuron Soul CI / build (pull_request) Successful in 4m47s
Neuron Soul CI / deploy (pull_request) Has been skipped
The model already narrates its intent in a text block before every tool call;
agentic_loop DISCARDED that prose on tool rounds. Now: (1) each loop round
appends {i, t: narration, tool} to state key run_progress_<sid>, reset at run
start, closed with {done:true}; (2) new GET /api/run-progress/<sid> returns the
ledger so clients poll live step updates during a run (the Cowork pattern,
no streaming needed); (3) tool_pending envelope gains a narration field;
(4) handle_config display default aligned to the intended product default
(claude-sonnet-4-5 silently became fresh-profile pickers' default).

Compiled proof for the running test bed:
neuron-container-build/soul-narrated-runs-20260713.patch (applies on top of
soul-webfix-20260711.patch); E2E-verified live: ledger filled DURING an agentic
run (narration + tool per round), safety-contact and workspace scoping intact.

Evidence for why: Tim's 2026-07-13 research run — 9 minutes of silence, then a
timeout banner, zero step visibility (compounded by the Haiku 4.5 incident
14:44-15:24 UTC same morning).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:06:51 -05:00
Tim Lingo 01446e644b feat(agent): BUG-8 — server-side risk tiers + run_command workspace fence
Neuron Soul CI / build (pull_request) Successful in 5m7s
Neuron Soul CI / deploy (pull_request) Has been skipped
Enforcement moves from the client into the engine, where the tools execute:

- classify_tool_risk() tiers every tool call read/reversible/escalate. The
  agentic loop REFUSES to auto-run the escalate tier — being a builtin is no
  longer a free pass, and 'always allow' can never bypass escalate (irreversible
  actions always confirm, the value line). Escalate suspends to the client's
  existing consent bridge; the /approve round-trip is the only path that runs it.
  risk_tier rides the tool_pending envelope so the client renders consent weight.
- run_command_guard() is a real fence, not a cwd suggestion: refuses parent
  traversal, ~, command substitution, and absolute paths outside the workspace,
  and refuses shell entirely when no workspace is set. Applied in dispatch_tool
  so BOTH the loop auto-run and the post-consent approve-dispatch path are fenced.
- web_get gained an http(s)-only scheme guard (previously unguarded — file:// etc).

Adversarially verified against a compiled soul in an isolated container (soul
hit directly, app gate out of the loop): read-outside-workspace denied,
write-class shell suspends for consent, approve-swapped absolute/chaining/
command-substitution escapes all refused with no file created, file:// denied;
legit in-workspace approve executes and read commands auto-run (no over-block).

Still lexical (symlinks); OS-level confinement in el_runtime.c remains the
ceiling, flagged in the LIMITATION note. This closes BUG-8's client-only-gate
and escapable-run_command at the engine. dist/soul.c must be regenerated from
this chat.el via elb at merge (hand-port used only to verify behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:45:52 -05:00
Tim Lingo 92f51885bc refactor(chat): local-toolchain compatibility — hoist affective block, de-shadow session_preload (zero behavior change)
Two mechanical refactors, semantics identical:
- affective_context_prefix(): the block-expression initializer form miscompiles
  under locally-buildable elc (first typed let in a block-expr loses its
  declaration — 3-line repro filed); function-hoist compiles correctly.
  AFFECTIVE/CARE LOGIC BODY UNCHANGED, verbatim move.
- session_preload: same-scope re-let shadowing inside an if-expression
  initializer emits duplicate C declarations; chained bindings renamed
  bullets_0/1/2 etc. References preserved binding-for-binding.

Enables: chat.el compiles cleanly with a self-bootstrapped elc from el/lang
main (Jul 1). Blocked separately: sessions.el (compiler hang), safety.el
(string-lexing corruption — NOT touched, per safety-layer discipline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:35:59 -05:00
will.anderson 2688cb722a chore(dist): update soul.c with PR #63/#65/#66 + Task 1 chat.el changes
Neuron Soul CI / build (push) Successful in 5m57s
Neuron Soul CI / deploy (push) Failing after 5m15s
Manually adds compiled C equivalents for:
- distill_transcript() — last-3-messages extractor; wires into
  handle_dharma_room_turn and handle_dharma_room_turn_agentic
- current_engine_note() — appended to system prompt in handle_chat
  so Neuron can answer 'what model am I running on?' truthfully (PR #66)
- llm_base_url / llm_wire_format / json_escape / openai_chat_complete —
  OpenAI-compatible provider path in handle_chat_agentic (PR #65)
- flag_true() — tolerant agentic flag check (PR #63)

Compile verified: 6 pre-existing warnings, 0 errors.
2026-07-01 11:42:56 -05:00
will.anderson 71bb0820ce Merge PR #65: soul: OpenAI-compatible provider path for chat (Ollama/OpenAI/Grok/Gemini) v1
Neuron Soul CI / build (push) Successful in 5m51s
Neuron Soul CI / deploy (push) Failing after 8m15s
Adds llm_base_url()/llm_wire_format() env-var readers and
openai_chat_complete() for basic (non-agentic) chat via any
OpenAI-compatible endpoint. Activated when NEURON_LLM_0_FORMAT=openai
and NEURON_LLM_0_URL is set; Anthropic path is untouched and remains
default. Agentic tool loop support deferred to a follow-up PR.
2026-07-01 11:35:02 -05:00
Tim Lingo 39acb55d4f soul: OpenAI-compatible provider path for chat (Ollama/OpenAI/Grok/Gemini) — v1 basic completion
Neuron Soul CI / build (pull_request) Failing after 17m19s
Additive, Anthropic path untouched + default. When NEURON_LLM_0_FORMAT=openai and NEURON_LLM_0_URL
set, basic chat turns build an OpenAI chat/completions request and parse choices[0].message.content.
v1 = plain completion, NO tools/agentic loop yet (follow-up). Unblocks all OpenAI-format providers
at once. PARSES (elc chat.el exit 0); NOT yet built/tested — needs the soul rebuild (dist/soul.c) + E2E.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:52:26 -05:00
17 changed files with 28430 additions and 22607 deletions
+19
View File
@@ -527,9 +527,27 @@ fn awareness_run() -> Void {
let scan_ms: Int = beat_ms / 2
while true {
// Arena-scope each tick: awareness_run() is a background loop, not an
// HTTP request, so nothing ever called el_request_start/el_request_end
// for this thread. Per the runtime's own convention (el_runtime.c),
// any thread that never enters a request/arena scope is treated as a
// one-shot CLI program whose allocations are intentionally permanent —
// so every el_strdup/el_strbuf/jb_finish string built during perceive(),
// emit_heartbeat(), and proactive_curiosity() (JSON payloads, search
// results, string concatenation via +) leaked forever, once per tick.
// el_arena_push()/el_arena_pop() are the same builtins the EL compiler
// itself uses to scope allocations per function/statement (see
// codegen.el's fn_arena_mark / stmt_mark usage) — mirroring that here
// reclaims everything allocated in one tick as soon as the tick ends.
// Safe: state_set/state_get persist through a separate global table
// (el_strdup_persist, outside the arena) — state_get's return value is
// only an arena-tracked *copy* of the persisted value, scoped to this
// tick's use, which is exactly what should be reclaimed here.
let tick_mark: Any = el_arena_push()
let running: String = state_get("soul.running")
if str_eq(running, "false") {
println("[awareness] exiting")
el_arena_pop(tick_mark)
return ""
}
let did_work: Bool = one_cycle()
@@ -593,6 +611,7 @@ fn awareness_run() -> Void {
}
sleep_ms(tick_ms)
el_arena_pop(tick_mark)
}
}
+1
View File
@@ -7,6 +7,7 @@ extern fn elapsed_ms() -> Int
extern fn elapsed_human() -> String
extern fn embed_ok() -> Int
extern fn emit_heartbeat() -> Void
extern fn auto_term_try_slot(slot_type: String, slot_lbl: String) -> Void
extern fn proactive_curiosity() -> Bool
extern fn pulse_count() -> Int
extern fn pulse_inc() -> Int
+369 -92
View File
@@ -926,6 +926,68 @@ fn session_preload_bullets(nodes: String, max_bullets: Int, snip_len: Int) -> St
return bullets
}
// Cross-session affective context (hoisted verbatim from handle_chat, 2026-07-04):
// the block-expression initializer form miscompiles under the local El toolchain
// (first typed let in a block-expr loses its declaration - repro filed for Will).
// Function-hoist is semantically identical. AFFECTIVE/CARE LOGIC: body unchanged.
fn affective_context_prefix() -> String {
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
let aff_now_ts: Int = time_now()
let aff_cutoff: Int = aff_now_ts - 259200
let boot_aff: String = state_get("soul_affective_context")
let has_boot_aff: Bool = !str_eq(boot_aff, "")
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
let found_recent_dist: Bool = if has_boot_aff {
true
} else {
if has_dist_aff {
let dn0: String = json_array_get(dist_nodes_aff, 0)
let dn_content: String = json_get(dn0, "content")
let daff_marker: String = " | ts:"
let daff_pos: Int = str_index_of(dn_content, daff_marker)
let daff_ts_str: String = if daff_pos >= 0 {
let daff_start: Int = daff_pos + str_len(daff_marker)
let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content))
let daff_next: Int = str_index_of(daff_rest, " | ")
if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) }
} else {
let daff_ca: String = json_get(dn0, "created_at")
if str_eq(daff_ca, "") { json_get(dn0, "updated_at") } else { daff_ca }
}
let daff_ts: Int = if str_eq(daff_ts_str, "") { 0 } else { str_to_int(daff_ts_str) }
daff_ts > aff_cutoff
} else { false }
}
let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]")
let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist {
let pn0: String = json_array_get(pos_nodes_aff, 0)
let pn_content: String = json_get(pn0, "content")
let paff_marker: String = " | ts:"
let paff_pos: Int = str_index_of(pn_content, paff_marker)
let paff_ts_str: String = if paff_pos >= 0 {
let paff_start: Int = paff_pos + str_len(paff_marker)
let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content))
let paff_next: Int = str_index_of(paff_rest, " | ")
if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) }
} else {
let paff_ca: String = json_get(pn0, "created_at")
if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca }
}
let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) }
paff_ts > aff_cutoff
} else { false }
let affective_out: String = if found_recent_dist {
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
} else {
if found_recent_pos {
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
} else { "" }
}
return affective_out
}
fn handle_chat(body: String) -> String {
let message: String = json_get(body, "message")
if str_eq(message, "") {
@@ -954,62 +1016,7 @@ fn handle_chat(body: String) -> String {
// Cross-session affective context: on session start (no history yet), check engram
// for recent distress signals within 72h and prepend a care directive if found.
let affective_prefix: String = {
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
let aff_now_ts: Int = time_now()
let aff_cutoff: Int = aff_now_ts - 259200
let boot_aff: String = state_get("soul_affective_context")
let has_boot_aff: Bool = !str_eq(boot_aff, "")
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
let found_recent_dist: Bool = if has_boot_aff {
true
} else {
if has_dist_aff {
let dn0: String = json_array_get(dist_nodes_aff, 0)
let dn_content: String = json_get(dn0, "content")
let daff_marker: String = " | ts:"
let daff_pos: Int = str_index_of(dn_content, daff_marker)
let daff_ts_str: String = if daff_pos >= 0 {
let daff_start: Int = daff_pos + str_len(daff_marker)
let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content))
let daff_next: Int = str_index_of(daff_rest, " | ")
if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) }
} else {
let daff_ca: String = json_get(dn0, "created_at")
if str_eq(daff_ca, "") { json_get(dn0, "updated_at") } else { daff_ca }
}
let daff_ts: Int = if str_eq(daff_ts_str, "") { 0 } else { str_to_int(daff_ts_str) }
daff_ts > aff_cutoff
} else { false }
}
let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]")
let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist {
let pn0: String = json_array_get(pos_nodes_aff, 0)
let pn_content: String = json_get(pn0, "content")
let paff_marker: String = " | ts:"
let paff_pos: Int = str_index_of(pn_content, paff_marker)
let paff_ts_str: String = if paff_pos >= 0 {
let paff_start: Int = paff_pos + str_len(paff_marker)
let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content))
let paff_next: Int = str_index_of(paff_rest, " | ")
if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) }
} else {
let paff_ca: String = json_get(pn0, "created_at")
if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca }
}
let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) }
paff_ts > aff_cutoff
} else { false }
if found_recent_dist {
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
} else {
if found_recent_pos {
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
} else { "" }
}
}
let affective_prefix: String = affective_context_prefix()
let ctx: String = engram_compile(activation_seed)
// Tell the LLM which engine it is running on this turn, so it can answer truthfully instead of
@@ -1026,7 +1033,7 @@ fn handle_chat(body: String) -> String {
// nodes stored under names like "Prism" unless those exact words appear in content.
let session_preload: String = if hist_len == 0 {
let profile_nodes: String = engram_search_json("user profile identity preferences", 5)
let work_nodes: String = engram_search_json("in_progress active project work", 5)
let work_nodes_0: String = engram_search_json("in_progress active project work", 5)
let project_nodes: String = engram_search_json("project status current ongoing active", 5)
let summary_nodes: String = engram_search_json("SessionSummary session:summary previous-session recent", 3)
@@ -1035,80 +1042,80 @@ fn handle_chat(body: String) -> String {
// Issue 1: typed work query WorkItem with in_progress label first.
let work_nodes_typed: String = engram_search_json("WorkItem status:in_progress active work", 6)
let work_ok_typed: Bool = !str_eq(work_nodes_typed, "") && !str_eq(work_nodes_typed, "[]")
let work_nodes: String = if work_ok_typed {
let work_nodes_1: String = if work_ok_typed {
work_nodes_typed
} else {
engram_search_json("active project task current in_progress", 6)
}
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
let work_ok: Bool = !str_eq(work_nodes_1, "") && !str_eq(work_nodes_1, "[]")
let project_ok: Bool = !str_eq(project_nodes, "") && !str_eq(project_nodes, "[]")
let summary_ok: Bool = !str_eq(summary_nodes, "") && !str_eq(summary_nodes, "[]")
let profile_bullets: String = if profile_ok {
let pn: Int = json_array_len(profile_nodes)
let bullets: String = ""
let bullets = if pn > 0 {
let bullets_0: String = ""
let bullets_1 = if pn > 0 {
let n0: String = json_array_get(profile_nodes, 0)
let id0: String = json_get(n0, "id")
let c0: String = json_get(n0, "content")
let s0: String = if str_len(c0) > 120 { str_slice(c0, 0, 120) } else { c0 }
if id_in_seen(id0, seen_ids) || str_eq(s0, "") { bullets } else { "- " + s0 }
} else { bullets }
let bullets = if pn > 1 {
if id_in_seen(id0, seen_ids) || str_eq(s0, "") { bullets_0 } else { "- " + s0 }
} else { bullets_0 }
let bullets_2 = if pn > 1 {
let n1: String = json_array_get(profile_nodes, 1)
let id1: String = json_get(n1, "id")
let c1: String = json_get(n1, "content")
let s1: String = if str_len(c1) > 120 { str_slice(c1, 0, 120) } else { c1 }
if id_in_seen(id1, seen_ids) || str_eq(s1, "") { bullets } else { bullets + "\n- " + s1 }
} else { bullets }
let bullets = if pn > 2 {
if id_in_seen(id1, seen_ids) || str_eq(s1, "") { bullets_1 } else { bullets_1 + "\n- " + s1 }
} else { bullets_1 }
let bullets_3 = if pn > 2 {
let n2: String = json_array_get(profile_nodes, 2)
let id2: String = json_get(n2, "id")
let c2: String = json_get(n2, "content")
let s2: String = if str_len(c2) > 120 { str_slice(c2, 0, 120) } else { c2 }
if id_in_seen(id2, seen_ids) || str_eq(s2, "") { bullets } else { bullets + "\n- " + s2 }
} else { bullets }
bullets
if id_in_seen(id2, seen_ids) || str_eq(s2, "") { bullets_2 } else { bullets_2 + "\n- " + s2 }
} else { bullets_2 }
bullets_3
} else { "" }
let work_bullets: String = if work_ok {
let wn: Int = json_array_len(work_nodes)
let wb: String = ""
let wb = if wn > 0 {
let w0: String = json_array_get(work_nodes, 0)
let wn: Int = json_array_len(work_nodes_1)
let wb_0: String = ""
let wb_1 = if wn > 0 {
let w0: String = json_array_get(work_nodes_1, 0)
let wid0: String = json_get(w0, "id")
let wc0: String = json_get(w0, "content")
let ws0: String = if str_len(wc0) > 120 { str_slice(wc0, 0, 120) } else { wc0 }
if id_in_seen(wid0, seen_ids) || str_eq(ws0, "") { wb } else { "- " + ws0 }
} else { wb }
let wb = if wn > 1 {
let w1: String = json_array_get(work_nodes, 1)
if id_in_seen(wid0, seen_ids) || str_eq(ws0, "") { wb_0 } else { "- " + ws0 }
} else { wb_0 }
let wb_2 = if wn > 1 {
let w1: String = json_array_get(work_nodes_1, 1)
let wid1: String = json_get(w1, "id")
let wc1: String = json_get(w1, "content")
let ws1: String = if str_len(wc1) > 120 { str_slice(wc1, 0, 120) } else { wc1 }
if id_in_seen(wid1, seen_ids) || str_eq(ws1, "") { wb } else { wb + "\n- " + ws1 }
} else { wb }
wb
if id_in_seen(wid1, seen_ids) || str_eq(ws1, "") { wb_1 } else { wb_1 + "\n- " + ws1 }
} else { wb_1 }
wb_2
} else { "" }
let project_bullets: String = if project_ok {
let prn: Int = json_array_len(project_nodes)
let pb: String = ""
let pb = if prn > 0 {
let pb_0: String = ""
let pb_1 = if prn > 0 {
let pr0: String = json_array_get(project_nodes, 0)
let prid0: String = json_get(pr0, "id")
let prc0: String = json_get(pr0, "content")
let ps0: String = if str_len(prc0) > 120 { str_slice(prc0, 0, 120) } else { prc0 }
if id_in_seen(prid0, seen_ids) || str_eq(ps0, "") { pb } else { "- " + ps0 }
} else { pb }
let pb = if prn > 1 {
if id_in_seen(prid0, seen_ids) || str_eq(ps0, "") { pb_0 } else { "- " + ps0 }
} else { pb_0 }
let pb_2 = if prn > 1 {
let pr1: String = json_array_get(project_nodes, 1)
let prid1: String = json_get(pr1, "id")
let prc1: String = json_get(pr1, "content")
let ps1: String = if str_len(prc1) > 120 { str_slice(prc1, 0, 120) } else { prc1 }
if id_in_seen(prid1, seen_ids) || str_eq(ps1, "") { pb } else { pb + "\n- " + ps1 }
} else { pb }
pb
if id_in_seen(prid1, seen_ids) || str_eq(ps1, "") { pb_1 } else { pb_1 + "\n- " + ps1 }
} else { pb_1 }
pb_2
} else { "" }
let summary_bullet: String = if summary_ok {
@@ -1276,6 +1283,86 @@ fn agentic_api_key() -> String {
return env("NEURON_LLM_0_KEY")
}
// OpenAI-compatible providers (Ollama / OpenAI / Grok / Gemini)
// The brain speaks Anthropic's Messages format by default. When the active provider uses the
// OpenAI-compatible wire format (NEURON_LLM_0_FORMAT=openai) with a configured base URL
// (NEURON_LLM_0_URL, e.g. http://localhost:11434/v1 for local Ollama), basic chat turns are served
// here instead of the Anthropic agentic loop.
// v1 SCOPE: plain chat completion only NO tools / agentic loop yet (that is a follow-up port).
// This block is ADDITIVE: the Anthropic path is untouched and stays the default.
fn llm_base_url() -> String {
return env("NEURON_LLM_0_URL")
}
fn llm_wire_format() -> String {
let f: String = env("NEURON_LLM_0_FORMAT")
if str_eq(f, "") {
return "anthropic"
}
return f
}
// Escape a decoded string so it can be embedded back into a JSON string literal.
fn json_escape(s: String) -> String {
let a: String = str_replace(s, "\\", "\\\\")
let b: String = str_replace(a, "\"", "\\\"")
let c: String = str_replace(b, "\n", "\\n")
let d: String = str_replace(c, "\r", "\\r")
return d
}
// Basic (non-agentic) chat completion against an OpenAI-compatible endpoint.
// [safe_sys] is already JSON-escaped; [messages_json] is the same JSON array the Anthropic path
// builds (e.g. [{"role":"user","content":"..."}]). Returns the soul's standard {"reply":"..."}.
fn openai_chat_complete(model: String, base_url: String, api_key: String, safe_sys: String, messages_json: String) -> String {
// Prepend the system prompt as an OpenAI "system" message, then the existing turn array.
let inner: String = if json_array_len(messages_json) > 0 {
str_slice(messages_json, 1, str_len(messages_json) - 1)
} else {
""
}
let msgs: String = if str_eq(inner, "") {
"[{\"role\":\"system\",\"content\":\"" + safe_sys + "\"}]"
} else {
"[{\"role\":\"system\",\"content\":\"" + safe_sys + "\"}," + inner + "]"
}
let req_body: String = "{\"model\":\"" + model + "\""
+ ",\"max_tokens\":4096"
+ ",\"messages\":" + msgs
+ "}"
let h: Map = {}
map_set(h, "content-type", "application/json")
// Ollama needs no key; OpenAI / Grok / Gemini use a Bearer token.
if !str_eq(api_key, "") {
map_set(h, "Authorization", "Bearer " + api_key)
}
let url: String = base_url + "/chat/completions"
let raw_resp: String = http_post_with_headers(url, req_body, h)
let is_error: Bool = str_starts_with(raw_resp, "{\"error\"") || str_contains(raw_resp, "\"error\":")
if is_error {
return "{\"error\":\"llm unavailable\",\"reply\":\"\"}"
}
// Parse OpenAI response shape: choices[0].message.content
let choices: String = json_get_raw(raw_resp, "choices")
let eff_choices: String = if str_eq(choices, "") {
"[]"
} else {
choices
}
if json_array_len(eff_choices) < 1 {
return "{\"error\":\"empty response\",\"reply\":\"\"}"
}
let first: String = json_array_get(eff_choices, 0)
let message: String = json_get_raw(first, "message")
let content: String = json_get(message, "content")
return "{\"reply\":\"" + json_escape(content) + "\",\"tools_used\":[]}"
}
fn agentic_tools_literal() -> String {
return "[" +
"{\"name\":\"read_file\",\"description\":\"Read contents of a file from disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute file path\"}},\"required\":[\"path\"]}}," +
@@ -1448,6 +1535,134 @@ fn resolve_in_root(path: String, root: String) -> String {
return root + "/" + path
}
// ---------------------------------------------------------------------------
// BUG-8: server-side risk tiers + a real fence for run_command.
//
// Before this block, the ONLY thing deciding whether a tool call paused for
// user consent was is_builtin_tool() a destructive shell command and a
// read-only file read were treated identically (both auto-ran), and the
// client's approval UI was the sole line of defense. Enforcement now lives
// where the tools execute:
//
// "read" observes only runs silently.
// "reversible" workspace-confined writes with a client undo path runs,
// lands on the run receipt.
// "escalate" irreversible / outward / shell NEVER auto-runs. The loop
// suspends to the client's consent flow; the /approve
// round-trip IS the approval token, because the engine only
// executes an escalated tool inside handle_session_approve.
//
// "Always allow" can never bypass the escalate tier (irreversible actions
// always confirm the value line). Unknown tools default to escalate.
// The run_command fence refuses parent traversal, ~, command substitution,
// and absolute paths outside the workspace refusal, not a cwd suggestion.
// Still lexical underneath (symlinks; see the LIMITATION note above): tiered
// consent + the fence raise the floor a second and third rung; OS-level
// confinement in el_runtime.c remains the ceiling, flagged for Will.
// ---------------------------------------------------------------------------
// Read-only shell commands may auto-run (still fenced); anything with shell
// plumbing (pipes, redirects, chaining) or an unknown head word escalates.
fn run_command_is_readonly(cmd: String) -> Bool {
if str_contains(cmd, "|") || str_contains(cmd, ">") || str_contains(cmd, "<") {
return false
}
if str_contains(cmd, ";") || str_contains(cmd, "&") {
return false
}
let sp: Int = str_index_of(cmd, " ")
let first: String = if sp < 0 { cmd } else { str_slice(cmd, 0, sp) }
if str_eq(first, "ls") || str_eq(first, "cat") || str_eq(first, "head") || str_eq(first, "tail") {
return true
}
if str_eq(first, "grep") || str_eq(first, "wc") || str_eq(first, "find") || str_eq(first, "pwd") {
return true
}
if str_eq(first, "echo") || str_eq(first, "date") || str_eq(first, "which") || str_eq(first, "file") || str_eq(first, "stat") {
return true
}
return false
}
// True if the command references an absolute path (introduced by `needle`,
// whose last char is the "/") that does NOT stay inside the workspace root.
fn cmd_abs_escape_at(cmd: String, root: String, needle: String) -> Bool {
let rest: String = cmd
let found: Bool = false
while !found && str_contains(rest, needle) {
let idx: Int = str_index_of(rest, needle)
let slash_at: Int = idx + str_len(needle) - 1
let after: String = str_slice(rest, slash_at, str_len(rest))
let ok: Bool = str_starts_with(after, root + "/") || str_starts_with(after, root + " ") || str_eq(after, root)
let found = if !ok { true } else { found }
let rest = str_slice(rest, slash_at + 1, str_len(rest))
}
return found
}
// The run_command fence. Returns "" when the command may run, else the denial
// message (sent back to the model as the tool result, same pattern as the
// path tools). Root is REQUIRED for shell: no workspace, no commands.
fn run_command_guard(cmd: String, root: String) -> String {
if str_eq(root, "") {
return "denied: no workspace folder is set — the user must choose a workspace folder in the Agent panel before shell commands can run"
}
if str_contains(cmd, "..") {
return "denied: parent-directory traversal ('..') is not allowed"
}
if str_contains(cmd, "~") {
return "denied: home-directory references ('~') are not allowed"
}
if str_contains(cmd, "$(") || str_contains(cmd, "`") {
return "denied: command substitution is not allowed"
}
if str_starts_with(cmd, "/") && !str_starts_with(cmd, root + "/") {
return "denied: absolute paths outside the workspace are not allowed"
}
if cmd_abs_escape_at(cmd, root, " /") || cmd_abs_escape_at(cmd, root, "\"/") || cmd_abs_escape_at(cmd, root, "'/") {
return "denied: absolute paths outside the workspace are not allowed"
}
if cmd_abs_escape_at(cmd, root, "=/") || cmd_abs_escape_at(cmd, root, ">/") || cmd_abs_escape_at(cmd, root, "</") || cmd_abs_escape_at(cmd, root, "(/") {
return "denied: absolute paths outside the workspace are not allowed"
}
return ""
}
// The engine's own risk classification for a tool call. Client UI renders it;
// the engine ENFORCES it.
fn classify_tool_risk(tool_name: String, tool_input: String) -> String {
if str_eq(tool_name, "read_file") || str_eq(tool_name, "list_files") || str_eq(tool_name, "grep") {
return "read"
}
if str_eq(tool_name, "search_memory") || str_eq(tool_name, "recall") || str_eq(tool_name, "web_get") {
return "read"
}
if str_eq(tool_name, "remember") || str_eq(tool_name, "neuron_remember") {
return "reversible"
}
if str_starts_with(tool_name, "neuron_") {
return "read"
}
if str_eq(tool_name, "write_file") || str_eq(tool_name, "edit_file") {
let root: String = agent_workspace_root()
// Unscoped writes (no workspace chosen) are not "reversible" escalate.
if str_eq(root, "") {
return "escalate"
}
return "reversible"
}
if str_eq(tool_name, "run_command") {
let cmd: String = json_get(tool_input, "command")
let root: String = agent_workspace_root()
if !str_eq(root, "") && run_command_is_readonly(cmd) {
return "read"
}
return "escalate"
}
// Unknown tool = escalate. Default-deny, never default-allow.
return "escalate"
}
fn dispatch_tool(tool_name: String, tool_input: String) -> String {
if str_eq(tool_name, "read_file") {
let path: String = json_get(tool_input, "path")
@@ -1470,6 +1685,10 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
}
if str_eq(tool_name, "web_get") {
let url: String = json_get(tool_input, "url")
// BUG-8: scheme guard web_get had no guard at all (file:// etc).
if !str_starts_with(url, "http://") && !str_starts_with(url, "https://") {
return json_safe("denied: only http(s) URLs can be fetched")
}
let result: String = http_get(url)
return json_safe(result)
}
@@ -1481,7 +1700,14 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
if str_eq(tool_name, "run_command") {
let cmd: String = json_get(tool_input, "command")
let root: String = agent_workspace_root()
let scoped: String = if str_eq(root, "") { cmd } else { "cd " + root + " && ( " + cmd + " )" }
// BUG-8(B): the fence refusal, not a cwd suggestion. Applies on EVERY
// execution path (auto-run in the loop AND post-consent dispatch from
// handle_session_approve), because both land here.
let denial: String = run_command_guard(cmd, root)
if !str_eq(denial, "") {
return json_safe(denial)
}
let scoped: String = "cd " + root + " && ( " + cmd + " )"
let result: String = exec_capture(scoped)
return json_safe(result)
}
@@ -1840,7 +2066,14 @@ fn handle_chat_agentic(body: String) -> String {
// Use caller-supplied session_id if provided, otherwise generate a bridge id.
let session_id: String = if str_eq(req_session, "") { next_bridge_id() } else { req_session }
let result: String = agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
// Provider fork: OpenAI-compatible providers (Ollama/OpenAI/Grok/Gemini) take the plain-completion
// path (v1, no tools); everything else stays on the Anthropic agentic loop (the default).
let use_openai: Bool = !str_eq(llm_base_url(), "") && str_eq(llm_wire_format(), "openai")
let result: String = if use_openai {
openai_chat_complete(model, llm_base_url(), agentic_api_key(), safe_sys, messages)
} else {
agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
}
// Persist the exchange to session/global history for thread continuity on next turn.
// Only save when the loop completed (reply present), not when tool_pending.
@@ -1904,6 +2137,19 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
let pend_tool_id: String = ""
let pend_tool_name: String = ""
let pend_tool_input: String = ""
let pend_tool_tier: String = ""
let pend_narration: String = ""
// Live run-progress ledger (2026-07-13, proposed with the narrated-runs work):
// the model already narrates its intent in a text block before every tool call,
// and the loop previously DISCARDED that prose on tool rounds. Each iteration now
// appends {"i":N,"t":"<narration>","tool":"<name>"} to state key
// run_progress_<session_id>; the client polls GET /api/run-progress/<session_id>
// during a run to render live step updates (the Cowork pattern) without needing
// streaming. Reset at loop start; a {"done":true} entry lands on completion.
if !str_eq(session_id, "") {
state_set("run_progress_" + session_id, "")
}
while keep_going && iteration < 8 {
let req_body: String = "{\"model\":\"" + model + "\""
@@ -1960,7 +2206,13 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
let always_key: String = "always_allow_" + session_id
let always_list: String = if !str_eq(session_id, "") { state_get(always_key) } else { "" }
let is_always_allowed: Bool = !str_eq(tool_name, "") && !str_eq(always_list, "") && str_contains(always_list, tool_name)
let needs_bridge: Bool = is_tool_turn && !is_builtin_tool(tool_name) && !is_always_allowed
// BUG-8(A): the engine classifies every tool call and REFUSES to auto-run
// the escalate tier being a builtin is no longer a free pass, and
// "always allow" can never bypass escalate (irreversible actions always
// confirm). Escalated calls suspend to the client's consent flow; the
// /approve round-trip is the only path that executes them.
let risk_tier: String = if is_tool_turn { classify_tool_risk(tool_name, tool_input) } else { "" }
let needs_bridge: Bool = is_tool_turn && (str_eq(risk_tier, "escalate") || (!is_builtin_tool(tool_name) && !is_always_allowed))
// Built-in tools dispatch locally; bridged tools yield "" (never sent upstream).
let tool_result_raw: String = if is_tool_turn && !needs_bridge { dispatch_tool(tool_name, tool_input) } else { "" }
@@ -1991,11 +2243,27 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
"[" + inner2 + ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}]"
} else { messages }
// Live progress ledger: one entry per round the model's own narration
// (its pre-tool prose, previously discarded here) plus the tool it reached
// for. Clients poll /api/run-progress/<sid> to render these live.
if !str_eq(session_id, "") {
let prog_key: String = "run_progress_" + session_id
let prog_prev: String = state_get(prog_key)
let prog_snip: String = if str_len(text_out) > 280 { str_slice(text_out, 0, 280) } else { text_out }
let prog_entry: String = "{\"i\":" + int_to_str(iteration)
+ ",\"t\":\"" + json_safe(prog_snip) + "\""
+ ",\"tool\":\"" + json_safe(tool_name) + "\"}"
let prog_next: String = if str_eq(prog_prev, "") { prog_entry } else { prog_prev + "," + prog_entry }
state_set(prog_key, prog_next)
}
// Bridge turn: persist the continuation and stop the loop.
let pending = if needs_bridge { true } else { pending }
let pend_tool_id = if needs_bridge { tool_id } else { pend_tool_id }
let pend_tool_name = if needs_bridge { tool_name } else { pend_tool_name }
let pend_tool_input = if needs_bridge { tool_input } else { pend_tool_input }
let pend_tool_tier = if needs_bridge { risk_tier } else { pend_tool_tier }
let pend_narration = if needs_bridge { text_out } else { pend_narration }
// Stash messages-with-the-assistant-request so resume only needs to append the
// client's tool_result block. messages_with_assistant is only meaningful when a
// tool was requested, so guard on needs_bridge before persisting.
@@ -2016,6 +2284,8 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
+ ",\"call_id\":\"" + pend_tool_id + "\""
+ ",\"tool_name\":\"" + pend_tool_name + "\""
+ ",\"tool_input\":" + safe_in
+ ",\"risk_tier\":\"" + pend_tool_tier + "\""
+ ",\"narration\":\"" + json_safe(pend_narration) + "\""
+ ",\"model\":\"" + model + "\""
+ ",\"agentic\":true"
+ ",\"tools_used\":" + tools_arr + "}"
@@ -2037,6 +2307,13 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
let safe_text: String = json_safe(final_text)
let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" }
// Close the live-progress ledger: pollers see {"done":true} and stop.
if !str_eq(session_id, "") {
let done_key: String = "run_progress_" + session_id
let done_prev: String = state_get(done_key)
let done_next: String = if str_eq(done_prev, "") { "{\"done\":true}" } else { done_prev + ",{\"done\":true}" }
state_set(done_key, done_next)
}
return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + ",\"iterations\":" + int_to_str(iteration) + "}"
}
+6
View File
@@ -17,7 +17,9 @@ extern fn id_in_seen(node_id: String, seen: String) -> Bool
extern fn add_to_seen(seen: String, node_id: String) -> String
extern fn engram_extract_ids(nodes_json: String) -> String
extern fn engram_compile(intent: String) -> String
extern fn distill_transcript(transcript: String) -> String
extern fn json_safe(s: String) -> String
extern fn current_engine_note(model: String) -> String
extern fn build_system_prompt(ctx: String, chat_mode: Bool) -> String
extern fn hist_append(hist: String, role: String, content: String) -> String
extern fn hist_trim(hist: String) -> String
@@ -30,6 +32,10 @@ extern fn handle_chat(body: String) -> String
extern fn handle_see(body: String) -> String
extern fn studio_tools_json() -> String
extern fn agentic_api_key() -> String
extern fn llm_base_url() -> String
extern fn llm_wire_format() -> String
extern fn json_escape(s: String) -> String
extern fn openai_chat_complete(model: String, base_url: String, api_key: String, safe_sys: String, messages_json: String) -> String
extern fn agentic_tools_literal() -> String
extern fn agentic_tools_with_web() -> String
extern fn connector_tools_json() -> String
Generated Vendored
+27487 -22139
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn elp_extract_topic(msg: String) -> String
extern fn elp_detect_predicate(msg: String) -> String
extern fn elp_parse(msg: String) -> String
+2 -2
View File
@@ -109,8 +109,8 @@ fn mem_consolidate() -> String {
}
fn mem_save(path: String) -> Void {
let save_result: String = engram_save(path)
if str_eq(save_result, "") {
let save_result: Bool = engram_save(path)
if !save_result {
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn tier_working() -> String
extern fn tier_episodic() -> String
extern fn tier_canonical() -> String
+2 -2
View File
@@ -656,8 +656,8 @@ fn handle_api_consolidate(body: String) -> String {
let summary: String = json_get(body, "summary")
let snap: String = state_get("soul_snapshot_path")
if !str_eq(snap, "") {
let save_result: String = engram_save(snap)
if str_eq(save_result, "") {
let save_result: Bool = engram_save(snap)
if !save_result {
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
}
}
+531 -358
View File
@@ -11,6 +11,7 @@ import "soul.elh"
// 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.
@utility
fn flag_true(body: String, key: String) -> Bool {
return json_get_bool(body, key) || json_get_int(body, key) > 0
}
@@ -74,6 +75,7 @@ fn rate_limit_check(ip: String, path: String) -> String {
return ""
}
@utility
fn strip_query(path: String) -> String {
let q: Int = str_index_of(path, "?")
if q < 0 {
@@ -82,14 +84,17 @@ fn strip_query(path: String) -> String {
return str_slice(path, 0, q)
}
@utility
fn err_404(path: String) -> String {
return "{\"error\":\"not found\",\"code\":\"not_found\",\"path\":\"" + path + "\"}"
}
@utility
fn err_405(method: String, path: String) -> String {
return "{\"error\":\"method not allowed\",\"code\":\"method_not_allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
}
@manager
fn route_health() -> String {
let cgi_id: String = state_get("soul_cgi_id")
let boot: String = state_get("soul_boot_count")
@@ -130,6 +135,7 @@ fn route_health() -> String {
+ ",\"layers\":{\"l0\":\"core\",\"l1\":\"safety\",\"l2\":\"stewardship\",\"l3\":\"" + imprint_current() + "\"}}"
}
@manager
fn route_lineage() -> String {
let cgi_id: String = state_get("soul_cgi_id")
let q: String = "lineage:" + cgi_id
@@ -147,6 +153,7 @@ fn route_lineage() -> String {
return raw
}
@manager
fn route_imprint_contextual(body: String) -> String {
if str_eq(body, "") {
return "{\"ok\":false,\"error\":\"empty body\"}"
@@ -169,6 +176,7 @@ fn route_imprint_contextual(body: String) -> String {
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
@manager
fn route_imprint_user(body: String) -> String {
if str_eq(body, "") {
return "{\"ok\":false,\"error\":\"empty body\"}"
@@ -191,6 +199,7 @@ fn route_imprint_user(body: String) -> String {
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
@manager
fn route_synthesize(body: String) -> String {
if str_eq(body, "") {
return "{\"error\":\"body is required\",\"code\":\"missing_param\"}"
@@ -218,6 +227,7 @@ fn route_synthesize(body: String) -> String {
return "{\"mechanism\":\"did not engage\"}"
}
@manager
fn handle_dharma_recv(body: String) -> String {
let content_raw: String = json_get(body, "content")
let from_id: String = json_get(body, "from")
@@ -300,6 +310,7 @@ fn handle_dharma_recv(body: String) -> String {
// the bridge. Bridge-down returns a clear error (not a panic).
// ---------------------------------------------------------------------------
@accessor
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, "") {
@@ -310,6 +321,7 @@ fn connectd_get(suffix: String) -> String {
// 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.
@accessor
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
@@ -323,38 +335,529 @@ fn connectd_post(suffix: String, body: String) -> String {
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")
//
// @route DISPATCH every HTTP route is a @route-decorated handler. The El
// compiler scans these decorators and synthesizes `el_route_dispatch(method,
// clean, path, body)`, emitted SPECIFICITY-SORTED (exact > compound > suffix >
// prefix; longer wins within a class) so overlapping paths never shadow,
// independent of source order. Matching is on `clean` (query-stripped); the
// ORIGINAL `path` is passed to handlers so query strings survive. Unmatched
// sentinel "__EL_NO_ROUTE__". handle_request (bottom) calls it once, then maps
// the sentinel to 404 (recognised method) / 405 (unknown method).
//
// Adapters carry the uniform (method, path, body) signature. Those that need
// the query-stripped path recompute `clean = strip_query(path)` internally,
// exactly as the former hand-written dispatcher did.
//
// pre-guard: inter-soul Dharma receive (POST /dharma/recv)
@route("/dharma/recv", "POST", "exact") @manager
fn r_dharma_recv(method: String, path: String, body: String) -> String {
return handle_dharma_recv(body)
}
// GET: liveness / lineage
@route("/health", "GET", "exact") @manager
fn r_health(method: String, path: String, body: String) -> String {
return route_health()
}
@route("/lineage", "GET", "exact") @manager
fn r_lineage(method: String, path: String, body: String) -> String {
return route_lineage()
}
// GET: raw engram graph (two exact aliases share one helper)
@route("/api/graph", "GET", "exact") @manager
fn r_api_graph(method: String, path: String, body: String) -> String {
return engram_scan_nodes_json(9999, 0)
}
@route("/api/graph/nodes", "GET", "exact") @manager
fn r_api_graph_nodes(method: String, path: String, body: String) -> String {
return engram_scan_nodes_json(9999, 0)
}
@route("/api/graph/edges", "GET", "exact") @manager
fn r_api_graph_edges(method: String, path: String, body: String) -> String {
// 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 }
}
// GET /api/chat legacy probe interface; body may be empty ───────────────
@route("/api/chat", "GET", "exact") @manager
fn r_chat_get(method: String, path: String, body: String) -> String {
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\"}"
}
if str_eq(clean, "/api/connectors/add") {
return connectd_post("/mcp/servers/add", body)
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 {
let screened_reply: String = layered_cycle(eff_msg)
screened_reply
}
if str_eq(clean, "/api/connectors/toggle") {
return connectd_post("/mcp/servers/toggle", body)
auto_persist(body, reply)
return reply
}
// GET|POST: method-branching handlers (same fn, guards on method)
@route("/api/conversations", "GET|POST", "exact") @manager
fn r_conversations(method: String, path: String, body: String) -> String {
return handle_conversations(method)
}
@route("/api/config", "GET|POST", "exact") @manager
fn r_config(method: String, path: String, body: String) -> String {
return handle_config(method, body)
}
@route("/api/tools/", "GET|POST", "prefix") @manager
fn r_tools(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return handle_tool(clean, method, body)
}
@route("/api/dharma", "GET|POST", "prefix") @manager
fn r_dharma(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return handle_dharma(clean, method, body)
}
@route("/api/nlg", "GET|POST", "prefix") @manager
fn r_nlg(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return handle_nlg(clean, method, body)
}
// GET|POST axon proxies (GET axon_get, POST axon_post)
@route("/api/memories", "GET|POST", "prefix") @manager
fn r_memories(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
}
@route("/api/knowledge", "GET|POST", "prefix") @manager
fn r_knowledge_axon(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
}
@route("/api/backlog", "GET|POST", "prefix") @manager
fn r_backlog(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
}
@route("/api/artifacts", "GET|POST", "prefix") @manager
fn r_artifacts(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
}
@route("/api/projects", "GET|POST", "prefix") @manager
fn r_projects(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
}
@route("/api/imprints", "GET|POST", "prefix") @manager
fn r_imprints(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
}
// GET / studio UI
@route("/", "GET", "exact") @manager
fn r_root(method: String, path: String, body: String) -> String {
return render_studio()
}
// Neuron cognitive API session/ctx (GET empty arg, POST body)
@route("/api/neuron/session/begin", "GET|POST", "exact") @manager
fn r_session_begin(method: String, path: String, body: String) -> String {
return if str_eq(method, "GET") { handle_api_begin_session("") } else { handle_api_begin_session(body) }
}
@route("/api/neuron/ctx", "GET|POST", "exact") @manager
fn r_ctx(method: String, path: String, body: String) -> String {
return if str_eq(method, "GET") { handle_api_compile_ctx("") } else { handle_api_compile_ctx(body) }
}
@route("/api/safety-contact", "GET|POST", "exact") @manager
fn r_safety_contact(method: String, path: String, body: String) -> String {
return if str_eq(method, "GET") { handle_safety_contact_get() } else { handle_safety_contact_post(body) }
}
// Neuron cognitive API knowledge
// GET search is a PREFIX (legacy) while POST search is EXACT kept distinct so
// semantics match the former dispatcher byte-for-byte.
@route("/api/neuron/knowledge/search", "GET", "prefix") @manager
fn r_knowledge_search_get(method: String, path: String, body: String) -> String {
return handle_api_search_knowledge(method, path, body)
}
@route("/api/neuron/knowledge/search", "POST", "exact") @manager
fn r_knowledge_search_post(method: String, path: String, body: String) -> String {
return handle_api_search_knowledge(method, path, body)
}
@route("/api/neuron/knowledge", "GET", "exact") @manager
fn r_knowledge_browse(method: String, path: String, body: String) -> String {
return handle_api_browse_knowledge(path, body)
}
@route("/api/neuron/knowledge/capture", "POST", "exact") @manager
fn r_knowledge_capture(method: String, path: String, body: String) -> String {
return handle_api_capture_knowledge(body)
}
@route("/api/neuron/knowledge/evolve", "POST", "exact") @manager
fn r_knowledge_evolve(method: String, path: String, body: String) -> String {
return handle_api_evolve_knowledge(body)
}
@route("/api/neuron/knowledge/promote", "POST", "exact") @manager
fn r_knowledge_promote(method: String, path: String, body: String) -> String {
return handle_api_promote_knowledge(body)
}
// Neuron cognitive API processes (GET prefix, POST exact + define)
@route("/api/neuron/processes", "GET", "prefix") @manager
fn r_processes_get(method: String, path: String, body: String) -> String {
return handle_api_browse_processes(method, path, body)
}
@route("/api/neuron/processes", "POST", "exact") @manager
fn r_processes_post(method: String, path: String, body: String) -> String {
return handle_api_browse_processes(method, path, body)
}
@route("/api/neuron/processes/define", "POST", "exact") @manager
fn r_processes_define(method: String, path: String, body: String) -> String {
return handle_api_define_process(body)
}
// Neuron cognitive API state events (GET prefix list, POST exact log)
@route("/api/neuron/state-events", "GET", "prefix") @manager
fn r_state_events_get(method: String, path: String, body: String) -> String {
return handle_api_list_state_events(method, path, body)
}
@route("/api/neuron/state-events", "POST", "exact") @manager
fn r_state_events_post(method: String, path: String, body: String) -> String {
return handle_api_log_state_event(body)
}
// Neuron cognitive API config (GET prefix, POST exact + tune)
@route("/api/neuron/config", "GET", "prefix") @manager
fn r_config_get(method: String, path: String, body: String) -> String {
return handle_api_inspect_config(path, body)
}
@route("/api/neuron/config", "POST", "exact") @manager
fn r_config_post(method: String, path: String, body: String) -> String {
return handle_api_inspect_config(path, body)
}
@route("/api/neuron/config/tune", "POST", "exact") @manager
fn r_config_tune(method: String, path: String, body: String) -> String {
return handle_api_tune_config(body)
}
// Neuron cognitive API graph (GET prefix, POST exact + link)
@route("/api/neuron/graph", "GET", "prefix") @manager
fn r_graph_get(method: String, path: String, body: String) -> String {
return handle_api_inspect_graph(method, path, body)
}
@route("/api/neuron/graph", "POST", "exact") @manager
fn r_graph_post(method: String, path: String, body: String) -> String {
return handle_api_inspect_graph(method, path, body)
}
@route("/api/neuron/graph/link", "POST", "exact") @manager
fn r_graph_link(method: String, path: String, body: String) -> String {
return handle_api_link_entities(body)
}
// Neuron cognitive API typed-node list (dynamic :node_type)
// Offset 17 = len("/api/neuron/list/"). str_slice on `clean` so query strings
// never leak into node_type.
@route("/api/neuron/list/", "GET", "prefix") @manager
fn r_list_typed(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
let node_type: String = str_slice(clean, 17, str_len(clean))
return handle_api_list_typed(node_type, path, body)
}
// Neuron cognitive API recall (GET prefix, POST exact)
@route("/api/neuron/recall", "GET", "prefix") @manager
fn r_recall_get(method: String, path: String, body: String) -> String {
return handle_api_recall(method, path, body)
}
@route("/api/neuron/recall", "POST", "exact") @manager
fn r_recall_post(method: String, path: String, body: String) -> String {
return handle_api_recall(method, path, body)
}
// Neuron cognitive API memory / node writes (POST exact)
@route("/api/neuron/memory", "POST", "exact") @manager
fn r_memory(method: String, path: String, body: String) -> String {
return handle_api_remember(body)
}
@route("/api/neuron/memory/evolve", "POST", "exact") @manager
fn r_memory_evolve(method: String, path: String, body: String) -> String {
return handle_api_evolve_memory(body)
}
@route("/api/neuron/memory/forget", "POST", "exact") @manager
fn r_memory_forget(method: String, path: String, body: String) -> String {
return handle_api_forget(body)
}
@route("/api/neuron/memory/delete", "POST", "exact") @manager
fn r_memory_delete(method: String, path: String, body: String) -> String {
return handle_api_memory_delete(body)
}
@route("/api/neuron/memory/update", "POST", "exact") @manager
fn r_memory_update(method: String, path: String, body: String) -> String {
return handle_api_memory_update(body)
}
@route("/api/neuron/node/create", "POST", "exact") @manager
fn r_node_create(method: String, path: String, body: String) -> String {
return handle_api_node_create(body)
}
@route("/api/neuron/node/update", "POST", "exact") @manager
fn r_node_update(method: String, path: String, body: String) -> String {
return handle_api_node_update(body)
}
@route("/api/neuron/node/delete", "POST", "exact") @manager
fn r_node_delete(method: String, path: String, body: String) -> String {
return handle_api_node_delete(body)
}
@route("/api/neuron/consolidate", "POST", "exact") @manager
fn r_consolidate(method: String, path: String, body: String) -> String {
return handle_api_consolidate(body)
}
@route("/api/neuron/cultivate", "POST", "exact") @manager
fn r_cultivate(method: String, path: String, body: String) -> String {
return handle_api_cultivate(body)
}
// POST: chat / ELP / see / imprint / synthesize
@route("/api/elp/chat", "POST", "exact") @manager
fn r_elp_chat(method: String, path: String, body: String) -> String {
return handle_elp_chat(body)
}
@route("/api/see", "POST", "exact") @manager
fn r_see(method: String, path: String, body: String) -> String {
return handle_see(body)
}
@route("/imprint/contextual", "POST", "exact") @manager
fn r_imprint_contextual(method: String, path: String, body: String) -> String {
return route_imprint_contextual(body)
}
@route("/imprint/user", "POST", "exact") @manager
fn r_imprint_user(method: String, path: String, body: String) -> String {
return route_imprint_user(body)
}
@route("/synthesize", "POST", "exact") @manager
fn r_synthesize(method: String, path: String, body: String) -> String {
return route_synthesize(body)
}
// POST /api/chat buffered (no streaming); message is REQUIRED.
@route("/api/chat", "POST", "exact") @manager
fn r_chat_post(method: String, path: String, body: String) -> String {
let raw_msg: String = json_get(body, "message")
if str_eq(raw_msg, "") {
return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
}
if str_eq(clean, "/api/connectors/auto-approve") {
return connectd_post("/mcp/servers/auto-approve", body)
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 {
let screened_reply: String = layered_cycle(raw_msg)
screened_reply
}
if str_eq(clean, "/api/connectors/remove") {
return connectd_post("/mcp/servers/remove", body)
auto_persist(body, reply)
return reply
}
// Sessions list / create / dynamic :id (GET/POST/DELETE/PATCH)
@route("/api/sessions", "GET", "exact") @manager
fn r_sessions_list(method: String, path: String, body: String) -> String {
return session_list()
}
@route("/api/sessions", "POST", "exact") @manager
fn r_sessions_create(method: String, path: String, body: String) -> String {
return session_create(body)
}
// COMPOUND: POST /api/sessions/:id/tool_result MCP tool-bridge resume. Must
// out-specify the bare approve prefix (it does: compound > prefix), preserving
// the load-bearing tool_result-before-approve order of the old dispatcher.
@route("/api/sessions/", "POST", "compound", "/tool_result") @manager
fn r_sessions_tool_result(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
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 bare prefix + in-handler sub check, exactly
// as the former dispatcher. Non-"approve" subpaths fall through to 404 (the old
// code returned nothing and dropped to the POST-block err_404).
@route("/api/sessions/", "POST", "prefix") @manager
fn r_sessions_approve(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
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, "/api/connectors/secret") {
return connectd_post("/mcp/servers/secret", body)
return err_404(clean)
}
@route("/api/sessions/", "GET", "prefix") @manager
fn r_sessions_get(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
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)
}
if str_eq(clean, "/api/connectors/oauth/start") {
return connectd_post("/mcp/oauth/start", body)
return err_404(clean)
}
@route("/api/sessions/", "DELETE", "prefix") @manager
fn r_sessions_delete(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
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)
}
// 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 err_404(clean)
}
@route("/api/sessions/", "PATCH", "prefix") @manager
fn r_sessions_patch(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
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)
}
// GET /api/run-progress/:session_id live agentic-run ledger
// Offset 18 = len("/api/run-progress/").
@route("/api/run-progress/", "GET", "prefix") @manager
fn r_run_progress(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
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 + "}"
}
return err_404(clean)
}
// MCP Connectors proxy to neuron-connectd :7771
// GET (any /api/connectors*) server list. POST sub-routes are exact; an
// unmatched POST /api/connectors* prefix returns the "unknown connectors route"
// body, exactly as the former handle_connectors fallthrough.
@route("/api/connectors", "GET", "prefix") @manager
fn r_connectors_get(method: String, path: String, body: String) -> String {
return connectd_get("/mcp/servers")
}
@route("/api/connectors/add", "POST", "exact") @manager
fn r_connectors_add(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/servers/add", body)
}
@route("/api/connectors/toggle", "POST", "exact") @manager
fn r_connectors_toggle(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/servers/toggle", body)
}
@route("/api/connectors/auto-approve", "POST", "exact") @manager
fn r_connectors_auto_approve(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/servers/auto-approve", body)
}
@route("/api/connectors/remove", "POST", "exact") @manager
fn r_connectors_remove(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/servers/remove", body)
}
@route("/api/connectors/secret", "POST", "exact") @manager
fn r_connectors_secret(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/servers/secret", body)
}
@route("/api/connectors/oauth/start", "POST", "exact") @manager
fn r_connectors_oauth_start(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/oauth/start", body)
}
// Call a connector tool directly (pre-chat), e.g. WhatsApp get_pairing_qr /
// get_login_status. Keeps the app on the app->soul->connectd path.
@route("/api/connectors/call", "POST", "exact") @manager
fn r_connectors_call(method: String, path: String, body: String) -> String {
return connectd_post("/mcp/call", body)
}
@route("/api/connectors", "POST", "prefix") @manager
fn r_connectors_unknown(method: String, path: String, body: String) -> String {
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
}
//
// handle_request HTTP entry point (registered via http_serve_async in soul.el).
// Rate-limits, then dispatches through the compiler-synthesized @route table.
// The sentinel maps to 404 for a recognised method or 405 for an unknown one,
// reproducing the per-method-block fallthroughs of the former dispatcher.
//
@manager
fn handle_request(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
@@ -369,346 +872,16 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
}
if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") {
return handle_dharma_recv(body)
// Compiler-synthesized dispatch (specificity-sorted, method-guarded).
let route_resp: String = el_route_dispatch(method, clean, path, body)
if !str_eq(route_resp, "__EL_NO_ROUTE__") {
return route_resp
}
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 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 {
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/") {
// 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/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)
}
}
// Fallthrough: a recognised method with no matching path 404; an
// unrecognised method 405 (matches the old per-method-block structure).
if str_eq(method, "GET") || str_eq(method, "POST") || str_eq(method, "DELETE") || str_eq(method, "PATCH") {
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 {
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)
}
+1 -1
View File
@@ -1,4 +1,5 @@
// auto-generated by elc --emit-header — do not edit
extern fn flag_true(body: String, key: String) -> Bool
extern fn rate_limit_check(ip: String, path: String) -> String
extern fn strip_query(path: String) -> String
extern fn err_404(path: String) -> String
@@ -11,5 +12,4 @@ extern fn route_synthesize(body: String) -> String
extern fn handle_dharma_recv(body: String) -> String
extern fn connectd_get(suffix: String) -> String
extern fn connectd_post(suffix: String, body: String) -> String
extern fn handle_connectors(method: String, clean: String, body: String) -> String
extern fn handle_request(method: String, path: String, body: String) -> String
+1 -1
View File
@@ -244,7 +244,7 @@ fn safety_general_hard_phrases() -> String {
}
fn safety_soft_phrases() -> String {
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\""]"
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\"]"
}
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
+2 -2
View File
@@ -514,10 +514,10 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
let last_role: String = json_get(last_entry, "role")
let last_content: String = json_get(last_entry, "content")
let topic_snip: String = if str_len(last_content) > 200 { str_slice(last_content, 0, 200) } else { last_content }
let safe_topic: String = str_replace(topic_snip, """, "'")
let safe_topic: String = str_replace(topic_snip, "\"", "'")
let ts_now: String = int_to_str(time_now())
let topic_content: String = "last-session-topic | ts:" + ts_now + " | session:" + session_id + " | topic:" + safe_topic
let topic_tags: String = "["last-session-topic","conv:history","Conversation","session:topic"]"
let topic_tags: String = "[\"last-session-topic\",\"conv:history\",\"Conversation\",\"session:topic\"]"
let topic_label: String = "last-session-topic:" + session_id
// Delete old last-session-topic node for this session before writing fresh
let old_topic: String = engram_search_json("last-session-topic:" + session_id, 2)
+1
View File
@@ -8,6 +8,7 @@ extern fn session_list() -> String
extern fn session_get(session_id: String) -> String
extern fn session_delete(session_id: String) -> String
extern fn session_update_patch(session_id: String, body: String) -> String
extern fn session_search_entry(node: String) -> String
extern fn session_search(query: String) -> String
extern fn session_hist_load(session_id: String) -> String
extern fn session_hist_save(session_id: String, hist: String) -> Void
+2 -6
View File
@@ -1,15 +1,11 @@
// stewardship.elh — Layer 2 public surface
// auto-generated by elc --emit-header — do not edit
extern fn steward_log_event(kind: String, detail: String) -> Void
extern fn steward_get_mission() -> String
extern fn steward_align(input: String, imprint_id: String) -> String
extern fn steward_validate_imprint(imprint_id: String, tool_name: String) -> String
extern fn steward_cgi_check(action: String) -> String
// steward_log_event is an internal helper exported here because El has no access modifiers.
// External callers have no business invoking this directly — use steward_align,
// steward_validate_imprint, or steward_cgi_check, which call it at the correct points.
extern fn steward_log_event(kind: String, detail: String) -> Void
// Behavioral profiling and continuity detection (Layer 2 — session fingerprinting).
extern fn steward_fingerprint_session(input: String, session_id: String) -> String
extern fn extract_dim(content: String, key: String) -> String
extern fn steward_build_baseline() -> String
extern fn steward_check_continuity(current_fingerprint: String, session_id: String) -> String
extern fn steward_session_check(input: String, session_id: String) -> String
+3 -1
View File
@@ -46,7 +46,9 @@ fn handle_config(method: String, body: String) -> String {
}
}
let current_model: String = state_get("soul_model")
let display: String = if str_eq(current_model, "") { "claude-sonnet-4-5" } else { current_model }
// Display fallback aligned with the intended product default (was claude-sonnet-4-5,
// which silently became the app's picker default on fresh profiles 2026-07-13).
let display: String = if str_eq(current_model, "") { "claude-opus-4-8" } else { current_model }
return "{\"model\":\"" + display + "\",\"ok\":true}"
}
+1 -1
View File
@@ -1,4 +1,4 @@
// auto-generated by elc --emit-header - do not edit
// auto-generated by elc --emit-header do not edit
extern fn auth_headers(tok: String) -> Map
extern fn axon_get(path: String) -> String
extern fn axon_post(path: String, body: String) -> String