diff --git a/engram/src/server.el b/engram/src/server.el index 571ad3c..7389d81 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -10,10 +10,60 @@ // cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c // ./engram // -// Configuration via environment: -// ENGRAM_BIND — host:port (default :8742) -// ENGRAM_API_KEY — bearer auth (optional) -// ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram) +// Configuration is DECLARED, not scattered. See the `program` block below: +// every knob's type and default lives there and nowhere else, is resolved from +// the environment (env wins, declaration is the fallback) and validated before +// any statement of this file runs. Read one with config("NAME") -> String. +// +// The one deliberate exception is ENGRAM_DATA_DIR — see the note in the block. + +// ── Program declaration (cross-cutting concerns) ────────────────────────────── +// +// singleton: two engram processes against one data dir is data loss, not a +// warning. The runtime takes an exclusive flock at startup and a second start +// is refused loudly with the holder's pid. +// +// NOT declared here, on purpose: ENGRAM_DATA_DIR. Its resolution is owned by +// engram_resolve_data_dir() (el_runtime.c), which defaults to $HOME/.neuron/engram +// and fails LOUD rather than silently persisting to an ephemeral directory. +// Declaring a default for it here as well would put the data dir's fallback in +// two places — which is precisely the defect this migration removes (until +// 2026-08-15 the reseed backup path carried its own "/tmp/engram" default that +// disagreed with the resolver, so the pre-destructive safety copy landed in /tmp). +// HOME is likewise not declared: it is a genuine environment read, not a knob. +program "engram" { + singleton: "engram" + + // ── Core server ── + env ENGRAM_BIND: String = ":8742" + // Default "" leaves auth DISABLED (check_auth_ok short-circuits to true on an + // empty key). That is the pre-existing behaviour and is deliberately preserved + // here; making this `required` is the obvious hardening follow-up, but it is a + // behaviour change and out of scope for this migration. + env ENGRAM_API_KEY: String = "" + + // ── Feature flags (bool-ish Strings; the predicate fns below own truthiness) ── + env ENGRAM_STORE: String = "off" + env ENGRAM_WAL: String = "off" + env ENGRAM_AUTOCONNECT: String = "off" + env ENGRAM_ISE_OFFGRAPH: String = "off" + + // ── ISE telemetry ── + env ENGRAM_ISE_RETENTION_MS: Int = "172800000" + + // ── Guide (local Qwen3 via llama-server) ── + env GUIDE_ENABLE: String = "off" + env GUIDE_TIER_FORCE: String = "" + env GUIDE_CACHE_DIR: String = "" + env GUIDE_RAM_GB_4B: Int = "16" + env GUIDE_RAM_GB_1P7B: Int = "8" + env GUIDE_BACKEND: String = "llama-server" + env GUIDE_HOST: String = "127.0.0.1" + env GUIDE_PORT: Int = "8771" + env GUIDE_LLAMA_SERVER_BIN: String = "llama-server" + env GUIDE_NGL: Int = "99" + env GUIDE_CTX: Int = "4096" +} // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -133,7 +183,7 @@ fn route_text_health(method: String, path: String, body: String) -> String { // engram_store_enabled() in el_runtime.c EXACTLY (1 / on / true). Default off → // every persistence path below is byte-for-byte the historical snapshot behavior. fn store_on() -> Bool { - let v: String = env("ENGRAM_STORE") + let v: String = config("ENGRAM_STORE") if str_eq(v, "1") { return true } if str_eq(v, "on") { return true } if str_eq(v, "true") { return true } @@ -162,7 +212,6 @@ fn persist_canonical() -> Int { if store_on() { return engram_store_checkpoint() } - let dir_raw: String = env("ENGRAM_DATA_DIR") let dir: String = engram_resolve_data_dir() // (2026-08-10 self-review) This returned a hardcoded 1, which made every // caller's `let saved: Int = persist_canonical()` a dead variable — six @@ -176,7 +225,7 @@ fn persist_canonical() -> Int { // per-write full-snapshot behavior. When ON, structural mutations append O(1) // WAL records instead of rewriting the whole graph, with threshold compaction. fn wal_on() -> Bool { - str_eq(env("ENGRAM_WAL"), "on") + str_eq(config("ENGRAM_WAL"), "on") } // autoconnect_on — ENGRAM_AUTOCONNECT. Will's rule: "we shouldn't be inserting @@ -184,7 +233,7 @@ fn wal_on() -> Bool { // edge (kNN over embeddings) so no content node enters the graph edgeless. // Default OFF -> byte-identical to prior behavior (node created, no auto edges). fn autoconnect_on() -> Bool { - let v: String = env("ENGRAM_AUTOCONNECT") + let v: String = config("ENGRAM_AUTOCONNECT") if str_eq(v, "1") { return true } if str_eq(v, "on") { return true } if str_eq(v, "true") { return true } @@ -197,7 +246,7 @@ fn autoconnect_on() -> Bool { // separate state-event log tier instead of the node graph. Default OFF -> ISEs // remain graph nodes exactly as before (with 48h prune). fn ise_offgraph_on() -> Bool { - let v: String = env("ENGRAM_ISE_OFFGRAPH") + let v: String = config("ENGRAM_ISE_OFFGRAPH") if str_eq(v, "1") { return true } if str_eq(v, "on") { return true } if str_eq(v, "true") { return true } @@ -358,7 +407,6 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String { // process ever booted with a partial/empty store, the first read request // clobbered the good snapshot. Read routes must never write the canonical path.) fn route_scan_edges(method: String, path: String, body: String) -> String { - let dir_raw: String = env("ENGRAM_DATA_DIR") let dir: String = engram_resolve_data_dir() let snap_path: String = dir + "/.scan-export.json" engram_save(snap_path) @@ -519,7 +567,6 @@ fn route_forget(method: String, path: String, body: String) -> String { fn route_save(method: String, path: String, body: String) -> String { let p_raw: String = json_get_string(body, "path") - let dir_raw: String = env("ENGRAM_DATA_DIR") let dir: String = engram_resolve_data_dir() let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw } // (2026-08-10 self-review) engram_save returns 0 on an empty path and the @@ -603,7 +650,6 @@ fn route_drift(method: String, path: String, body: String) -> String { fn route_load(method: String, path: String, body: String) -> String { let p_raw: String = json_get_string(body, "path") - let dir_raw: String = env("ENGRAM_DATA_DIR") let dir: String = engram_resolve_data_dir() let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw } // (2026-08-10 self-review) This was a stub response over the single most @@ -674,7 +720,6 @@ fn route_embed_backfill(method: String, path: String, body: String) -> String { // (it skips nodes already present by ID). Auth-exempt: same-host internal call. // (2026-06-27 self-review: added this route to fix silent 10-min sync failures) fn route_sync(method: String, path: String, body: String) -> String { - let dir_raw: String = env("ENGRAM_DATA_DIR") let dir: String = engram_resolve_data_dir() // 2026-07-21 self-review: export to a scratch path, never the canonical // snapshot.json — read routes must not be able to clobber the good snapshot. @@ -750,8 +795,12 @@ fn route_reseed_nodes(method: String, path: String, body: String) -> String { if str_eq(p, "") { return err_json("path is required") } if str_eq(fs_read(p), "") { return err_json("file missing or empty") } - let dir_raw: String = env("ENGRAM_DATA_DIR") - let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw } + // (2026-08-15) This site carried its own "/tmp/engram" fallback, which + // DISAGREED with engram_resolve_data_dir() ($HOME/.neuron/engram, fail-loud). + // The consumer is the pre-destructive backup below, so with ENGRAM_DATA_DIR + // unset the safety copy taken before a reseed landed in an ephemeral /tmp + // while the store it was protecting lived elsewhere. One owner, one answer. + let dir: String = engram_resolve_data_dir() let backup: String = dir + "/.reseed-backup.json" let replace_raw: String = json_get_raw(body, "replace") @@ -843,8 +892,7 @@ fn route_emit_ise(method: String, path: String, body: String) -> String { sal, imp, conf, "Episodic", "[\"internal-state\",\"InternalStateEvent\"]" ) - let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS") - let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) } + let ret_ms: Int = str_to_int(config("ENGRAM_ISE_RETENTION_MS")) let pruned: Int = engram_prune_telemetry(ret_ms) "{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}" } @@ -1093,14 +1141,12 @@ fn route_correspondence_beat(method: String, path: String, body: String) -> Stri // turns native thinking ON: the response carries reasoning_content (the thinking) // alongside content (the answer). -fn guide_env_or(key: String, dflt: String) -> String { - let v: String = env(key) - if str_eq(v, "") { return dflt } - return v -} +// (2026-08-15) guide_env_or(key, dflt) lived here. Its whole job was supplying a +// per-call-site default, which is now the program block's job — every GUIDE_* knob +// is declared once at the top of this file and read straight through config(). fn guide_enabled() -> Bool { - let v: String = env("GUIDE_ENABLE") + let v: String = config("GUIDE_ENABLE") if str_eq(v, "1") { return true } if str_eq(v, "on") { return true } if str_eq(v, "true") { return true } @@ -1145,15 +1191,15 @@ fn guide_probe_metal() -> Bool { // ── 2. Tier selection (config-driven thresholds, spec-autoselected) ──────────── fn guide_threshold_4b() -> Int { - return str_to_int(guide_env_or("GUIDE_RAM_GB_4B", "16")) + return str_to_int(config("GUIDE_RAM_GB_4B")) } fn guide_threshold_1p7b() -> Int { - return str_to_int(guide_env_or("GUIDE_RAM_GB_1P7B", "8")) + return str_to_int(config("GUIDE_RAM_GB_1P7B")) } // GUIDE_TIER_FORCE overrides the spec autoselect (used to prove cheaply on 0.6b). fn guide_select_tier(ram_gb: Int) -> String { - let forced: String = env("GUIDE_TIER_FORCE") + let forced: String = config("GUIDE_TIER_FORCE") if !str_eq(forced, "") { return forced } if ram_gb >= guide_threshold_4b() { return "4b" } if ram_gb >= guide_threshold_1p7b() { return "1.7b" } @@ -1173,8 +1219,10 @@ fn guide_file(tier: String) -> String { } fn guide_cache_dir() -> String { - let c: String = env("GUIDE_CACHE_DIR") + let c: String = config("GUIDE_CACHE_DIR") if !str_eq(c, "") { return c } + // HOME stays a raw env() read: it is the ambient environment, not a knob of + // this program, and it is deliberately absent from the program block. let home: String = env("HOME") if !str_eq(home, "") { return home + "/.neuron/guide/models" } return engram_resolve_data_dir() + "/guide-models" @@ -1215,9 +1263,9 @@ fn guide_fetch(tier: String) -> Bool { } // ── 4/5. Backend abstraction + BIND as an engageable interlocutor ────────────── -fn guide_backend() -> String { return guide_env_or("GUIDE_BACKEND", "llama-server") } -fn guide_host() -> String { return guide_env_or("GUIDE_HOST", "127.0.0.1") } -fn guide_port() -> String { return guide_env_or("GUIDE_PORT", "8771") } +fn guide_backend() -> String { return config("GUIDE_BACKEND") } +fn guide_host() -> String { return config("GUIDE_HOST") } +fn guide_port() -> String { return config("GUIDE_PORT") } fn guide_base_url() -> String { return "http://" + guide_host() + ":" + guide_port() } // guide_healthy — is the guide present and answering? llama-server's /health @@ -1235,9 +1283,9 @@ fn guide_healthy() -> Bool { fn guide_load(tier: String) -> Bool { if guide_healthy() { return true } let path: String = guide_model_path(tier) - let bin: String = guide_env_or("GUIDE_LLAMA_SERVER_BIN", "llama-server") - let ngl: String = guide_env_or("GUIDE_NGL", "99") - let ctx: String = guide_env_or("GUIDE_CTX", "4096") + let bin: String = config("GUIDE_LLAMA_SERVER_BIN") + let ngl: String = config("GUIDE_NGL") + let ctx: String = config("GUIDE_CTX") let logf: String = guide_cache_dir() + "/llama-server." + guide_port() + ".log" let cmd: String = bin + " -m '" + path + "' --host " + guide_host() + " --port " + guide_port() + " -c " + ctx + " -ngl " + ngl + " --jinja >> '" + logf + "' 2>&1" let pid: String = exec_bg(cmd) @@ -1633,7 +1681,7 @@ fn route_supersede(method: String, path: String, body: String) -> String { // ── Auth ────────────────────────────────────────────────────────────────────── fn check_auth_ok(method: String, body: String) -> Bool { - let key: String = env("ENGRAM_API_KEY") + let key: String = config("ENGRAM_API_KEY") if str_eq(key, "") { return true } // Read-only methods don't require auth. Until http_serve surfaces // request headers we can't accept a Bearer token cleanly; mutating @@ -1896,8 +1944,7 @@ fn handle_request(method: String, path: String, body: String) -> String { // ── Entry ───────────────────────────────────────────────────────────────────── -let bind_raw: String = env("ENGRAM_BIND") -let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw } +let bind_str: String = config("ENGRAM_BIND") let port: Int = parse_port(bind_str) // On startup, try to load any existing snapshot (best effort).