diff --git a/dist/soul.elh b/dist/soul.elh index ae5e5a4..290dff3 100644 --- a/dist/soul.elh +++ b/dist/soul.elh @@ -2,3 +2,4 @@ extern fn init_soul_edges() -> Void extern fn load_identity_context() -> Void extern fn emit_session_start_event() -> Void +extern fn layered_cycle(raw_input: String) -> String diff --git a/imprint.el b/imprint.el new file mode 100644 index 0000000..7baeea7 --- /dev/null +++ b/imprint.el @@ -0,0 +1,29 @@ +// imprint.el — L3 Imprint layer (stub — full implementation in feat/layer-imprint) +// Routes the processed input through the active imprint and generates the final reply. +// This stub allows soul.el and routes.el to compile while feat/layer-imprint is pending merge. +// +// Contract for imprint_current() -> String: +// Returns the active imprint ID (node ID from engram), or "none" if no imprint is loaded. +// Used in health checks and to identify which imprint L2/L3 should operate against. +// +// Contract for imprint_respond(input, imprint_id) -> String: +// Generates a reply from the active imprint given the stewardship-aligned input. +// Falls back to handle_chat when no imprint is active (imprint_id = "" or "none"). + +fn imprint_current() -> String { + let contextual: String = state_get("active_contextual_imprint") + if !str_eq(contextual, "") { + return contextual + } + let user_imp: String = state_get("active_user_imprint") + if !str_eq(user_imp, "") { + return user_imp + } + return "none" +} + +fn imprint_respond(input: String, imprint_id: String) -> String { + // Stub: delegate to core chat until feat/layer-imprint is merged + let body: String = "{\"message\":\"" + json_safe(input) + "\"}" + return handle_chat(body) +} diff --git a/imprint.elh b/imprint.elh new file mode 100644 index 0000000..79d84f5 --- /dev/null +++ b/imprint.elh @@ -0,0 +1,3 @@ +// auto-generated by elc --emit-header — do not edit +extern fn imprint_current() -> String +extern fn imprint_respond(input: String, imprint_id: String) -> String diff --git a/routes.el b/routes.el index 849811c..ad808e5 100644 --- a/routes.el +++ b/routes.el @@ -4,6 +4,7 @@ import "chat.el" import "studio.el" import "elp-input.el" import "neuron-api.el" +import "soul.elh" fn strip_query(path: String) -> String { let q: Int = str_index_of(path, "?") @@ -234,7 +235,22 @@ fn handle_request(method: String, path: String, body: String) -> String { return if str_eq(edges_raw, "") { "[]" } else { edges_raw } } if str_eq(clean, "/api/chat") { - return handle_chat(body) + // 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 required\"}" + } + let agentic_flag: Bool = json_get_bool(body, "agentic") + let reply: String = if agentic_flag { + handle_chat_agentic(body) + } else { + let screened_reply: String = layered_cycle(eff_msg) + screened_reply + } + auto_persist(body, reply) + return reply } if str_eq(clean, "/api/conversations") { return handle_conversations(method) diff --git a/safety.el b/safety.el new file mode 100644 index 0000000..88247f0 --- /dev/null +++ b/safety.el @@ -0,0 +1,33 @@ +// safety.el — L1 Safety layer (stub — full implementation in feat/layer-safety) +// Provides safety screening and validation for the consciousness stack. +// This stub allows soul.el to compile while feat/layer-safety is pending merge. +// +// Contract for safety_screen(input, history) -> String (JSON): +// {"action": "pass" | "hard_bell", "content": "", "reason": ""} +// +// Contract for safety_validate(output, screen_action) -> String: +// Second param is the original screen_action ("pass") or the sentinel "hard_bell". +// Returns the validated output string, or a safe refusal if validation fails. +// +// Contract for safety_log_bell(severity, reason, excerpt) -> Void: +// Logs a bell event to engram. severity = "hard" | "soft". Hard bell events are +// intentionally NOT added to conversation_history (security isolation by design). + +fn safety_screen(input: String, history: String) -> String { + return "{\"action\":\"pass\",\"content\":\"" + json_safe(input) + "\"}" +} + +fn safety_validate(output: String, screen_action: String) -> String { + return output +} + +fn safety_log_bell(severity: String, reason: String, excerpt: String) -> Void { + let tags: String = "[\"safety\",\"bell\",\"" + severity + "-bell\"]" + let payload: String = "{\"severity\":\"" + severity + "\",\"reason\":\"" + json_safe(reason) + "\",\"excerpt\":\"" + json_safe(excerpt) + "\"}" + let discard: String = engram_node_full( + payload, "InternalStateEvent", "safety:bell", + el_from_float(0.95), el_from_float(0.95), el_from_float(1.0), + "Episodic", tags + ) + println("[safety] bell logged severity=" + severity + " reason=" + reason) +} diff --git a/safety.elh b/safety.elh new file mode 100644 index 0000000..8ebba87 --- /dev/null +++ b/safety.elh @@ -0,0 +1,4 @@ +// auto-generated by elc --emit-header — do not edit +extern fn safety_screen(input: String, history: String) -> String +extern fn safety_validate(output: String, screen_action: String) -> String +extern fn safety_log_bell(severity: String, reason: String, excerpt: String) -> Void diff --git a/soul.el b/soul.el index 675caf4..d86c3c6 100644 --- a/soul.el +++ b/soul.el @@ -242,7 +242,17 @@ fn layered_cycle(raw_input: String) -> String { let screen_result: String = safety_screen(raw_input, history) let screen_action: String = json_get(screen_result, "action") - // Hard bell: bypass all upper layers, log and escalate + // Hard bell: bypass all upper layers, log and escalate. + // Intentionally does NOT update conversation_history or call auto_persist(): + // hard bell events are security-sensitive and must not appear in engram conversation + // history where they could leak context to subsequent turns. They are persisted + // separately by safety_log_bell() into the Episodic tier with restricted labels. + // + // safety_validate second param: when screen_action is "hard_bell", safety_validate + // receives the sentinel string "hard_bell" (not a normal screen action). The safety + // layer contract requires it to return a fixed refusal regardless of the output arg. + // On the normal path, safety_validate receives the original screen_action ("pass") + // so it can apply action-specific post-output checks. if str_eq(screen_action, "hard_bell") { safety_log_bell("hard", json_get(screen_result, "reason"), str_slice(raw_input, 0, 80)) return safety_validate("", "hard_bell") @@ -253,10 +263,30 @@ fn layered_cycle(raw_input: String) -> String { let imprint_id: String = imprint_current() let steward_result: String = steward_align(screened, imprint_id) let steward_action: String = json_get(steward_result, "action") + + // "block": stewardship has determined this input should not proceed. + // Return the steward's content directly (a safe refusal) and skip L3. + if str_eq(steward_action, "block") { + let block_msg: String = json_get(steward_result, "content") + let eff_block: String = if str_eq(block_msg, "") { "I'm not able to help with that." } else { block_msg } + return safety_validate(eff_block, screen_action) + } + + // "redirect": stewardship steers toward aligned territory. + // redirect_to holds the reframed prompt; fall through to L3 with it. + // "pass": content holds the (possibly lightly reframed) input ready for L3. + // Unknown actions: treat as pass, log a warning, use content field. let guided: String = if str_eq(steward_action, "pass") { json_get(steward_result, "content") } else { - json_get(steward_result, "redirect_to") + // redirect or unknown — prefer redirect_to, fall back to content + let redir: String = json_get(steward_result, "redirect_to") + let alt: String = json_get(steward_result, "content") + let chosen: String = if str_eq(redir, "") { alt } else { redir } + if str_eq(chosen, "") { + println("[soul] warn: steward action '" + steward_action + "' returned no usable content — using original screened input") + } + if str_eq(chosen, "") { screened } else { chosen } } // L3: imprint responds diff --git a/soul.elh b/soul.elh index ae5e5a4..290dff3 100644 --- a/soul.elh +++ b/soul.elh @@ -2,3 +2,4 @@ extern fn init_soul_edges() -> Void extern fn load_identity_context() -> Void extern fn emit_session_start_event() -> Void +extern fn layered_cycle(raw_input: String) -> String diff --git a/stewardship.el b/stewardship.el new file mode 100644 index 0000000..cb94b41 --- /dev/null +++ b/stewardship.el @@ -0,0 +1,15 @@ +// stewardship.el — L2 Stewardship layer (stub — full implementation in feat/layer-stewardship) +// Aligns inputs with the active imprint's values and directives. +// This stub allows soul.el to compile while feat/layer-stewardship is pending merge. +// +// Contract for steward_align(input, imprint_id) -> String (JSON): +// {"action": "pass" | "redirect" | "block", "content": ""} +// - "pass": content = the (possibly lightly reframed) input ready for L3 +// - "redirect": content = an alternate prompt that steers toward aligned territory; +// redirect_to field contains the redirect target (same as content here) +// - "block": content = a safe refusal message; imprint_respond is skipped and +// this content is returned directly to safety_validate as the output + +fn steward_align(input: String, imprint_id: String) -> String { + return "{\"action\":\"pass\",\"content\":\"" + json_safe(input) + "\"}" +} diff --git a/stewardship.elh b/stewardship.elh new file mode 100644 index 0000000..e55c848 --- /dev/null +++ b/stewardship.elh @@ -0,0 +1,2 @@ +// auto-generated by elc --emit-header — do not edit +extern fn steward_align(input: String, imprint_id: String) -> String