fix(reliability): safety-resilience #39
@@ -374,6 +374,10 @@ fn handle_chat(body: String) -> String {
|
|||||||
let req_model: String = json_get(body, "model")
|
let req_model: String = json_get(body, "model")
|
||||||
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
|
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
|
||||||
|
|
||||||
|
// ISSUE 9: add safety_augment_system to primary /api/chat path.
|
||||||
|
// handle_chat was the only LLM path missing bell directive injection.
|
||||||
|
let full_system = safety_augment_system(full_system, message)
|
||||||
|
|
||||||
let raw_response: String = llm_call_system(model, full_system, message)
|
let raw_response: String = llm_call_system(model, full_system, message)
|
||||||
|
|
||||||
let is_error: Bool = str_starts_with(raw_response, "{\"error\"")
|
let is_error: Bool = str_starts_with(raw_response, "{\"error\"")
|
||||||
|
|||||||
@@ -144,17 +144,21 @@ fn safety_screen(input: String, history: String) -> String {
|
|||||||
if score >= soft {
|
if score >= soft {
|
||||||
let summary: String = str_slice(input, 0, 80)
|
let summary: String = str_slice(input, 0, 80)
|
||||||
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
|
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
|
||||||
|
// ISSUE 7: also escape tab chars to prevent JSON envelope corruption.
|
||||||
let e1: String = str_replace(input, "\\", "\\\\")
|
let e1: String = str_replace(input, "\\", "\\\\")
|
||||||
let e2: String = str_replace(e1, "\"", "\\\"")
|
let e2: String = str_replace(e1, "\"", "\\\"")
|
||||||
let e3: String = str_replace(e2, "\n", "\\n")
|
let e3: String = str_replace(e2, "\n", "\\n")
|
||||||
let safe_input: String = str_replace(e3, "\r", "\\r")
|
let e4: String = str_replace(e3, "\r", "\\r")
|
||||||
|
let safe_input: String = str_replace(e4, "\t", "\\t")
|
||||||
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
|
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ISSUE 7: also escape tab chars (see soft_bell branch above).
|
||||||
let e1: String = str_replace(input, "\\", "\\\\")
|
let e1: String = str_replace(input, "\\", "\\\\")
|
||||||
let e2: String = str_replace(e1, "\"", "\\\"")
|
let e2: String = str_replace(e1, "\"", "\\\"")
|
||||||
let e3: String = str_replace(e2, "\n", "\\n")
|
let e3: String = str_replace(e2, "\n", "\\n")
|
||||||
let safe_input: String = str_replace(e3, "\r", "\\r")
|
let e4: String = str_replace(e3, "\r", "\\r")
|
||||||
|
let safe_input: String = str_replace(e4, "\t", "\\t")
|
||||||
return "{\"action\":\"pass\",\"content\":\"" + safe_input + "\"}"
|
return "{\"action\":\"pass\",\"content\":\"" + safe_input + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +199,8 @@ fn safety_validate(output: String, action: String) -> String {
|
|||||||
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
|
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
|
||||||
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
|
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
|
||||||
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
|
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
|
||||||
let discard: String = engram_node_full(
|
// ISSUE 2: fallback log when engram write fails silently.
|
||||||
|
let node_id: String = engram_node_full(
|
||||||
content,
|
content,
|
||||||
"BellEvent",
|
"BellEvent",
|
||||||
"bell:" + level,
|
"bell:" + level,
|
||||||
@@ -205,6 +210,9 @@ fn safety_log_bell(level: String, reason: String, input_summary: String) -> Stri
|
|||||||
"Episodic",
|
"Episodic",
|
||||||
tags
|
tags
|
||||||
)
|
)
|
||||||
|
if str_eq(node_id, "") {
|
||||||
|
println("[safety] WARN: bell engram write failed -- " + content)
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +243,10 @@ 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.
|
||||||
|
// json_array_len of malformed input returns 0, silently skipping all checks.
|
||||||
|
// Caching requires language-level static const arrays -- not in current EL.
|
||||||
|
// Migrate to const arrays when EL gains that feature.
|
||||||
// ── Matching helpers (single loops only — el escapes while-body mutation via
|
// ── Matching helpers (single loops only — el escapes while-body mutation via
|
||||||
// top-level let rebinds; nested loops would not advance) ────────────────────
|
// top-level let rebinds; nested loops would not advance) ────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ import "stewardship.el"
|
|||||||
import "imprint.el"
|
import "imprint.el"
|
||||||
import "awareness.el"
|
import "awareness.el"
|
||||||
import "chat.el"
|
import "chat.el"
|
||||||
import "safety.el"
|
|
||||||
import "studio.el"
|
import "studio.el"
|
||||||
import "elp-input.el"
|
import "elp-input.el"
|
||||||
import "routes.el"
|
import "routes.el"
|
||||||
import "safety.el"
|
|
||||||
import "stewardship.el"
|
|
||||||
import "imprint.el"
|
|
||||||
|
|
||||||
cgi "neuron-soul" {
|
cgi "neuron-soul" {
|
||||||
dharma_id: "ntn-genesis@http://localhost:7770",
|
dharma_id: "ntn-genesis@http://localhost:7770",
|
||||||
@@ -265,19 +261,31 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
let screen_result: String = safety_screen(raw_input, history)
|
let screen_result: String = safety_screen(raw_input, history)
|
||||||
let screen_action: String = json_get(screen_result, "action")
|
let screen_action: String = json_get(screen_result, "action")
|
||||||
|
|
||||||
|
// ISSUE 4: safe-mode guard. If safety_screen returned an invalid/empty action
|
||||||
|
// (engram failure or internal error), refuse rather than pass unscreened input.
|
||||||
|
let valid_action: Bool = str_eq(screen_action, "hard_bell")
|
||||||
|
|| str_eq(screen_action, "soft_bell")
|
||||||
|
|| str_eq(screen_action, "pass")
|
||||||
|
if !valid_action {
|
||||||
|
println("[soul] layered_cycle: safety_screen invalid action -- safe mode refusal")
|
||||||
|
return safety_validate("", "hard_bell")
|
||||||
|
}
|
||||||
|
|
||||||
// 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():
|
// Intentionally does NOT update conversation_history or call auto_persist():
|
||||||
// hard bell events are security-sensitive and must not appear in engram conversation
|
// 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
|
// history where they could leak context to subsequent turns. They are persisted
|
||||||
// separately by safety_log_bell() into the Episodic tier with restricted labels.
|
// separately by safety_log_bell() into the Episodic tier with restricted labels.
|
||||||
//
|
//
|
||||||
|
// ISSUE 6: safety_log_bell already called inside safety_screen (line 140).
|
||||||
|
// Do NOT call it again here -- that would double-log every hard bell.
|
||||||
|
//
|
||||||
// safety_validate second param: when screen_action is "hard_bell", safety_validate
|
// 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
|
// 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.
|
// 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")
|
// On the normal path, safety_validate receives the original screen_action ("pass")
|
||||||
// so it can apply action-specific post-output checks.
|
// so it can apply action-specific post-output checks.
|
||||||
if str_eq(screen_action, "hard_bell") {
|
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")
|
return safety_validate("", "hard_bell")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +320,16 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
json_get(steward_result, "redirect_to")
|
json_get(steward_result, "redirect_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ISSUE 1: pre-LLM bell augmentation for layered_cycle path.
|
||||||
|
// safety_augment_system appends soft/hard directive to system prompt when bell fires,
|
||||||
|
// ensuring LLM processes message WITH the safety directive -- not just post-output gate.
|
||||||
|
// Stored in state as "layered_cycle_safety_system_addendum" for imprint_respond to use.
|
||||||
|
// TODO: wire directly when imprint_respond gains system_override param (imprint.el change).
|
||||||
|
// ISSUE 3 TODO: no semantic crisis detection. Keyword-only means signals that evade
|
||||||
|
// the phrase list pass with zero augmentation. Semantic layer = separate decision.
|
||||||
|
let augmented_addendum: String = safety_augment_system("", raw_input)
|
||||||
|
state_set("layered_cycle_safety_system_addendum", augmented_addendum)
|
||||||
|
|
||||||
// L3: imprint responds
|
// L3: imprint responds
|
||||||
let output: String = imprint_respond(aligned, imprint_id)
|
let output: String = imprint_respond(aligned, imprint_id)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user