Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfa540c066 | |||
| 13241aae25 | |||
| 2c346ee2b8 | |||
| fa5de69358 | |||
| a54770d606 |
@@ -527,27 +527,9 @@ 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()
|
||||
@@ -611,7 +593,6 @@ fn awareness_run() -> Void {
|
||||
}
|
||||
|
||||
sleep_ms(tick_ms)
|
||||
el_arena_pop(tick_mark)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn idle_count() -> Int
|
||||
extern fn idle_inc() -> Int
|
||||
extern fn idle_reset() -> Void
|
||||
|
||||
@@ -1680,8 +1680,16 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
if !path_within_root(path, root) {
|
||||
return json_safe("denied: path is outside the agent workspace root")
|
||||
}
|
||||
fs_write(resolve_in_root(path, root), content)
|
||||
return json_safe("{\"ok\":true}")
|
||||
// BUG-6 fix (2026-07-17): never claim ok without disk truth. fs_write's result was
|
||||
// never checked, so a failed write reported ok — the exact false-receipt failure
|
||||
// the run guards exist to kill. Verify the file landed and return the RESOLVED
|
||||
// path so callers and the model can only narrate what is really on disk.
|
||||
let dest: String = resolve_in_root(path, root)
|
||||
fs_write(dest, content)
|
||||
if !fs_exists(dest) {
|
||||
return json_safe("{\"error\":\"write failed - nothing landed at " + dest + "\"}")
|
||||
}
|
||||
return json_safe("{\"ok\":true,\"path\":\"" + dest + "\"}")
|
||||
}
|
||||
if str_eq(tool_name, "web_get") {
|
||||
let url: String = json_get(tool_input, "url")
|
||||
@@ -1935,8 +1943,24 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
// no root (or cleared the field), and we must not overwrite a server-configured root
|
||||
// from NEURON_AGENT_ROOT with an empty string, which would silently un-scope the agent.
|
||||
let ws_root: String = json_get(body, "agent_workspace_root")
|
||||
// BUG-LEAK fix (2026-07-16): the root used to live ONLY in the shared key, so any
|
||||
// request that omitted it INHERITED the previous session's folder (proven: a rootless
|
||||
// curl session wrote into another session's run folder). Now each session keeps its
|
||||
// own copy, and every request RE-ASSERTS its own root (possibly empty) into the shared
|
||||
// key the tool guards read — no session can ever act under another session's root.
|
||||
// Empty state still falls through to env NEURON_AGENT_ROOT inside
|
||||
// agent_workspace_root(), so a server-configured root survives unchanged.
|
||||
// LIMITATION (for review): assumes serialized request handling; true per-call scoping
|
||||
// means threading session_id through dispatch_tool/classify — deeper change, Will's call.
|
||||
let sess_for_root: String = json_get(body, "session_id")
|
||||
if !str_eq(ws_root, "") {
|
||||
if !str_eq(sess_for_root, "") {
|
||||
state_set("agent_workspace_root_" + sess_for_root, ws_root)
|
||||
}
|
||||
state_set("agent_workspace_root", ws_root)
|
||||
} else {
|
||||
let own_root: String = if str_eq(sess_for_root, "") { "" } else { state_get("agent_workspace_root_" + sess_for_root) }
|
||||
state_set("agent_workspace_root", own_root)
|
||||
}
|
||||
|
||||
// L1 safety screen — agentic path must pass the same gate as layered_cycle.
|
||||
@@ -2066,6 +2090,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 }
|
||||
// PAUSE-CONTRACT fix (2026-07-16): honor the client's require_approval field — the
|
||||
// Phase 1c contract ("the soul pauses on EVERY tool; the client's tier gate decides
|
||||
// what actually prompts") was never implemented engine-side, which made the client's
|
||||
// Ask autonomy silently inert for builtin sub-escalate tools. Persisted per session
|
||||
// (set/reset on every request) so the /approve resume path keeps the same behavior
|
||||
// for the rest of the run. Absent/false = behavior identical to before this fix.
|
||||
let req_ask_all: String = json_get(body, "require_approval")
|
||||
state_set("require_approval_" + session_id, if str_eq(req_ask_all, "true") { "true" } else { "" })
|
||||
// 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")
|
||||
@@ -2126,6 +2158,12 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: String, messages_in: String, h: Map, tools_log_in: String) -> String {
|
||||
let api_url: String = "https://api.anthropic.com/v1/messages"
|
||||
|
||||
// PAUSE-CONTRACT fix (2026-07-16): when the client asked to approve every action
|
||||
// (require_approval on the request, persisted per session), EVERY tool turn bridges —
|
||||
// the client's tier gate decides what actually prompts vs auto-continues. Read from
|
||||
// session state so the /approve resume re-entry keeps the same behavior mid-run.
|
||||
let ask_all: Bool = !str_eq(session_id, "") && str_eq(state_get("require_approval_" + session_id), "true")
|
||||
|
||||
let messages: String = messages_in
|
||||
let final_text: String = ""
|
||||
let tools_log: String = tools_log_in
|
||||
@@ -2212,7 +2250,10 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
|
||||
// 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))
|
||||
// PAUSE-CONTRACT fix (2026-07-16): ask_all bridges EVERYTHING — stricter only.
|
||||
// Escalate keeps its unconditional bridge; "always allow" shortcuts never apply
|
||||
// under ask_all (the client owns its own standing grants at its tier gate).
|
||||
let needs_bridge: Bool = is_tool_turn && (ask_all || 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 { "" }
|
||||
@@ -2352,6 +2393,10 @@ fn agentic_resume(session_id: String, tool_use_id: String, content: String) -> S
|
||||
if str_eq(blob, "") {
|
||||
return "{\"error\":\"unknown session_id\",\"reply\":\"\"}"
|
||||
}
|
||||
// BUG-LEAK fix (2026-07-16): re-assert THIS session's own workspace root before the
|
||||
// loop continues — a resume must never run under whatever root the last unrelated
|
||||
// request happened to leave in the shared key.
|
||||
state_set("agent_workspace_root", state_get("agent_workspace_root_" + session_id))
|
||||
|
||||
let model: String = json_get(blob, "model")
|
||||
let safe_sys: String = json_get(blob, "safe_sys")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn chat_default_model() -> String
|
||||
extern fn engram_numeric_valid(s: String) -> Bool
|
||||
extern fn parse_float_x100(s: String) -> Int
|
||||
@@ -28,6 +28,7 @@ extern fn clean_llm_response(s: String) -> String
|
||||
extern fn conv_history_persist(hist: String) -> Void
|
||||
extern fn conv_history_load() -> String
|
||||
extern fn session_preload_bullets(nodes: String, max_bullets: Int, snip_len: Int) -> String
|
||||
extern fn affective_context_prefix() -> String
|
||||
extern fn handle_chat(body: String) -> String
|
||||
extern fn handle_see(body: String) -> String
|
||||
extern fn studio_tools_json() -> String
|
||||
@@ -46,6 +47,10 @@ extern fn call_neuron_mcp(tool_name: String, args: String) -> String
|
||||
extern fn agent_workspace_root() -> String
|
||||
extern fn path_within_root(path: String, root: String) -> Bool
|
||||
extern fn resolve_in_root(path: String, root: String) -> String
|
||||
extern fn run_command_is_readonly(cmd: String) -> Bool
|
||||
extern fn cmd_abs_escape_at(cmd: String, root: String, needle: String) -> Bool
|
||||
extern fn run_command_guard(cmd: String, root: String) -> String
|
||||
extern fn classify_tool_risk(tool_name: String, tool_input: String) -> String
|
||||
extern fn dispatch_tool(tool_name: String, tool_input: String) -> String
|
||||
extern fn is_builtin_tool(tool_name: String) -> Bool
|
||||
extern fn next_bridge_id() -> String
|
||||
|
||||
+23003
-28254
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
# Narrated runs — engine notes for Will (2026-07-13)
|
||||
|
||||
Source half: commit aa67f86 on feat/agent-phase1-soul (run-progress ledger,
|
||||
`/api/run-progress/<sid>` route, narration on the pause envelope, config display
|
||||
default). E2E-verified via the compiled test bed on Tim's clean profile.
|
||||
|
||||
Compiled-form-only fixes (in `neuron-container-build/soul-narrated-runs-20260713.patch`,
|
||||
applies ON TOP of `soul-webfix-20260711.patch` — these need porting to chat.el when the
|
||||
webfix itself is ported):
|
||||
|
||||
1. **pause_turn + tool_use interleave**: a pause_turn response can ALSO carry a client
|
||||
tool_use; resuming verbatim leaves it unpaired → Anthropic 400 "tool_use ids were
|
||||
found without tool_result". Fix: tool-bearing pause rounds are tool turns
|
||||
(dispatch + pair); verbatim resume only when the round has no client tool.
|
||||
2. **Agentic toolset scope**: agentic_tools_all() fed EVERY connector/MCP tool (Notion,
|
||||
code-execution…) into the loop. Code-execution flips the API into programmatic
|
||||
tool calling, whose pairing protocol the single-tool manual loop does not speak —
|
||||
source of the dangling-pair 400s AND the bash_code_execution workspace-dodge.
|
||||
Fix: handle_chat_agentic declares builtins + ONE server web_search only.
|
||||
Connector tools return when the loop gains real multi-tool/programmatic support.
|
||||
3. **disable_parallel_tool_use: true** on agentic requests — the loop captures only the
|
||||
first tool_use per round; Opus-class models parallel-call. Enforce the invariant.
|
||||
4. **web_search server-tool default variant → web_search_20250305 (GA)**. The 20260209
|
||||
variant couples to code-execution ⇒ programmatic mode (see #2, and the June note:
|
||||
"inert unless code-execution attached").
|
||||
5. **Homegrown web_search removed** from the tool catalog (server-side is the one tool).
|
||||
|
||||
Known engine debts this work surfaced (not fixed):
|
||||
|
||||
- **Poisoned session history**: a failed run persists the malformed assistant turn; every
|
||||
later turn in that session replays it and 400s. Needs history sanitation on load.
|
||||
- **Huge-history invalid-escape 400** (~346KB request) — likely the same poisoned blob.
|
||||
- **macOS note**: replacing a binary in place invalidates its ad-hoc signature (instant
|
||||
silent SIGKILL, looks like exit 0). `rm + cp + codesign -f -s -` is the swap ritual.
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn imprint_current() -> String
|
||||
extern fn imprint_load(imprint_id: String) -> String
|
||||
extern fn imprint_respond(input: String, imprint_id: String) -> String
|
||||
|
||||
@@ -109,8 +109,8 @@ fn mem_consolidate() -> String {
|
||||
}
|
||||
|
||||
fn mem_save(path: String) -> Void {
|
||||
let save_result: Bool = engram_save(path)
|
||||
if !save_result {
|
||||
let save_result: String = engram_save(path)
|
||||
if str_eq(save_result, "") {
|
||||
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
@@ -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: Bool = engram_save(snap)
|
||||
if !save_result {
|
||||
let save_result: String = engram_save(snap)
|
||||
if str_eq(save_result, "") {
|
||||
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn is_protected_node(id: String) -> Bool
|
||||
extern fn api_err_protected(id: String) -> String
|
||||
extern fn api_json_escape(s: String) -> String
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
}
|
||||
@@ -75,7 +74,6 @@ 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 {
|
||||
@@ -84,17 +82,14 @@ 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")
|
||||
@@ -135,7 +130,6 @@ 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
|
||||
@@ -153,7 +147,6 @@ fn route_lineage() -> String {
|
||||
return raw
|
||||
}
|
||||
|
||||
@manager
|
||||
fn route_imprint_contextual(body: String) -> String {
|
||||
if str_eq(body, "") {
|
||||
return "{\"ok\":false,\"error\":\"empty body\"}"
|
||||
@@ -176,7 +169,6 @@ 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\"}"
|
||||
@@ -199,7 +191,6 @@ 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\"}"
|
||||
@@ -227,7 +218,6 @@ 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")
|
||||
@@ -310,7 +300,6 @@ 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, "") {
|
||||
@@ -321,7 +310,6 @@ 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
|
||||
@@ -335,529 +323,38 @@ fn connectd_post(suffix: String, body: String) -> String {
|
||||
return out
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// @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\"}"
|
||||
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")
|
||||
}
|
||||
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/add") {
|
||||
return connectd_post("/mcp/servers/add", 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/toggle") {
|
||||
return connectd_post("/mcp/servers/toggle", 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/auto-approve") {
|
||||
return connectd_post("/mcp/servers/auto-approve", 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/remove") {
|
||||
return connectd_post("/mcp/servers/remove", 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/secret") {
|
||||
return connectd_post("/mcp/servers/secret", 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)
|
||||
if str_eq(clean, "/api/connectors/oauth/start") {
|
||||
return connectd_post("/mcp/oauth/start", 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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
|
||||
@@ -872,16 +369,358 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// 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, "POST") && str_eq(clean, "/dharma/recv") {
|
||||
return handle_dharma_recv(body)
|
||||
}
|
||||
|
||||
// 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") {
|
||||
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/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 {
|
||||
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
@@ -1,5 +1,4 @@
|
||||
// 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
|
||||
@@ -12,4 +11,5 @@ 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-10
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn soft_bell_threshold() -> Int
|
||||
extern fn hard_bell_threshold() -> Int
|
||||
extern fn safety_score_crisis(input: String) -> Int
|
||||
@@ -13,12 +13,3 @@ extern fn safety_self_harm_phrases() -> String
|
||||
extern fn safety_abuse_phrases() -> String
|
||||
extern fn safety_general_hard_phrases() -> String
|
||||
extern fn safety_soft_phrases() -> String
|
||||
extern fn safety_detect_positive_level(message: String) -> String
|
||||
extern fn safety_detect_bell_level(message: String) -> String
|
||||
extern fn safety_classify_hard_bell(message: String) -> String
|
||||
extern fn safety_soft_directive() -> String
|
||||
extern fn safety_hard_directive(hard_type: String) -> String
|
||||
extern fn safety_augment_system(system: String, user_msg: String) -> String
|
||||
extern fn safety_contact_path() -> String
|
||||
extern fn handle_safety_contact_get() -> String
|
||||
extern fn handle_safety_contact_post(body: String) -> String
|
||||
|
||||
+14
-3
@@ -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)
|
||||
@@ -677,6 +677,11 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
// path for all sessions created through handle_chat_agentic / agentic_loop.
|
||||
let bridge_blob: String = state_get("mcp_bridge:" + session_id)
|
||||
if !str_eq(bridge_blob, "") {
|
||||
// BUG-LEAK fix (2026-07-16): the approved tool executes below via dispatch_tool,
|
||||
// whose path/command guards read the shared workspace-root key. Re-assert THIS
|
||||
// session's own root first — an approval must never execute under whatever root
|
||||
// the last unrelated request left behind.
|
||||
state_set("agent_workspace_root", state_get("agent_workspace_root_" + session_id))
|
||||
// For "always": record tool_name in the always-allow list before resuming.
|
||||
// The tool_name is not stored in the bridge blob (only tool_use_id is).
|
||||
// Accept it from the body so the client can pass it along.
|
||||
@@ -708,7 +713,13 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
// For builtin tools with no client-provided content: fall back to
|
||||
// dispatch_tool so those tools still execute correctly.
|
||||
let client_content: String = json_get(body, "content")
|
||||
let use_client_content: Bool = !str_eq(client_content, "")
|
||||
// BUG-6 fix (2026-07-17): the naive json_get scanner matches "content" ANYWHERE
|
||||
// in the body — including INSIDE tool_input — so every approved write_file (whose
|
||||
// input always carries a content field) was mistaken for client-executed, never
|
||||
// dispatched, and narrated as done: a false receipt with no file on disk. Builtin
|
||||
// tools now ALWAYS dispatch server-side; client content is only honored for
|
||||
// non-builtin (MCP/client-executed) tools. Stricter only.
|
||||
let use_client_content: Bool = !str_eq(client_content, "") && !is_builtin_tool(approve_tool_name)
|
||||
let use_dispatch: Bool = is_builtin_tool(approve_tool_name) && !use_client_content
|
||||
let raw_input: String = json_get_raw(body, "tool_input")
|
||||
let eff_input: String = if str_eq(raw_input, "") { "{}" } else { raw_input }
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// 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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user