ci: make official engram build store-enabled (publish + link engram_store.{c,h}) #95

Merged
will.anderson merged 27 commits from engram-tiered-storage into dev 2026-08-12 20:23:37 +00:00
2 changed files with 271 additions and 13 deletions
Showing only changes of commit 6838e5cbff - Show all commits
+270 -13
View File
@@ -1553,8 +1553,13 @@ static void* http_worker(void* arg) {
int fd = a->fd;
#endif
free(a);
char *method = NULL, *path = NULL, *body = NULL;
if (http_read_request(fd, &method, &path, &body, NULL) == 0) {
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0
&& !el_http_request_authorized(method, path, hdr_block)) {
/* Loopback hardening: EL_HTTP_AUTH_KEY is set and this request lacks the
* matching X-Neuron-Auth header refuse before it reaches any handler. */
el_http_send_401(fd);
} else if (method != NULL) {
http_handler_fn h = http_lookup_active();
char* response = NULL;
/* HEAD: dispatch as GET so existing handlers respond with the same
@@ -1582,7 +1587,7 @@ static void* http_worker(void* arg) {
_tl_http_head_only = 0;
free(response);
}
free(method); free(path); free(body);
free(method); free(path); free(body); free(hdr_block);
el_closesocket(fd);
/* release a slot */
pthread_mutex_lock(&_http_conn_mu);
@@ -1592,6 +1597,108 @@ static void* http_worker(void* arg) {
return NULL;
}
/* ── loopback lock + local API-key auth (shipped desktop hardening) ────────
* Both controls are OFF by default (their env vars unset), so dev, self-host,
* and server builds behave exactly as before. The shipped macOS launcher
* neuron-daemons.sh sets them so a customer's soul is neither reachable from
* other machines on the LAN nor callable by other local users/processes
* without the per-install key held in the login Keychain:
*
* EL_HTTP_BIND_HOST=127.0.0.1 -> bind loopback only (el_http_apply_bind_addr)
* EL_HTTP_AUTH_KEY=<per-install> -> require "X-Neuron-Auth: <key>" per request
*/
/* Set the listen address on the dual-stack (AF_INET6, V6ONLY=0) socket. Default
* is in6addr_any (all interfaces) unchanged. When EL_HTTP_BIND_HOST names a
* loopback ("127.0.0.1", "localhost", "loopback", or "::1") we bind the IPv4-
* mapped IPv6 loopback ::ffff:127.0.0.1: on a V6ONLY=0 socket this accepts IPv4
* 127.0.0.1 clients (the desktop app connects there) while refusing every
* off-machine address. */
static void el_http_apply_bind_addr(struct sockaddr_in6* addr) {
const char* h = getenv("EL_HTTP_BIND_HOST");
int loopback = h && *h && (strcmp(h, "127.0.0.1") == 0
|| strcmp(h, "localhost") == 0
|| strcmp(h, "loopback") == 0
|| strcmp(h, "::1") == 0);
if (loopback) {
memset(&addr->sin6_addr, 0, sizeof(addr->sin6_addr));
addr->sin6_addr.s6_addr[10] = 0xff; /* ::ffff:127.0.0.1 */
addr->sin6_addr.s6_addr[11] = 0xff;
addr->sin6_addr.s6_addr[12] = 127;
addr->sin6_addr.s6_addr[15] = 1;
} else {
addr->sin6_addr = in6addr_any;
}
}
/* Human-readable description of the active bind host, for the listen log line. */
static const char* el_http_bind_desc(void) {
const char* h = getenv("EL_HTTP_BIND_HOST");
if (h && *h && (strcmp(h, "127.0.0.1") == 0 || strcmp(h, "localhost") == 0
|| strcmp(h, "loopback") == 0 || strcmp(h, "::1") == 0)) {
return "127.0.0.1 (loopback)";
}
return "[::] (dual-stack)";
}
/* Case-insensitive compare of the first n bytes of a and b. */
static int el_ci_eq_n(const char* a, const char* b, size_t n) {
for (size_t i = 0; i < n; i++) {
unsigned char ca = (unsigned char)a[i], cb = (unsigned char)b[i];
if (tolower(ca) != tolower(cb)) return 0;
}
return 1;
}
/* Return 1 iff the raw header block carries a header named `name` (case-
* insensitive) whose trimmed value equals `want` exactly. */
static int el_http_header_equals(const char* hdr_block, const char* name,
const char* want) {
if (!hdr_block || !name || !want) return 0;
size_t nlen = strlen(name), wlen = strlen(want);
const char* p = hdr_block;
while (*p) {
const char* line_end = strstr(p, "\r\n");
const char* end = line_end ? line_end : p + strlen(p);
const char* colon = memchr(p, ':', (size_t)(end - p));
if (colon && (size_t)(colon - p) == nlen && el_ci_eq_n(p, name, nlen)) {
const char* v = colon + 1;
while (v < end && (*v == ' ' || *v == '\t')) v++;
size_t vlen = (size_t)(end - v);
while (vlen > 0 && (v[vlen - 1] == ' ' || v[vlen - 1] == '\t')) vlen--;
if (vlen == wlen && memcmp(v, want, wlen) == 0) return 1;
}
if (!line_end) break;
p = line_end + 2;
}
return 0;
}
/* Authorize an inbound request. Enforcement is active only when EL_HTTP_AUTH_KEY
* is set; otherwise every request is allowed (dev default). GET/HEAD /health*
* are always allowed so launch-agent liveness probes work without the key. */
static int el_http_request_authorized(const char* method, const char* path,
const char* hdr_block) {
const char* key = getenv("EL_HTTP_AUTH_KEY");
if (!key || !*key) return 1;
if (method && (strcmp(method, "GET") == 0 || strcmp(method, "HEAD") == 0)
&& path && strncmp(path, "/health", 7) == 0) return 1;
return el_http_header_equals(hdr_block, "x-neuron-auth", key);
}
/* Minimal 401 for unauthorized requests — never reaches an EL handler. */
static void el_http_send_401(int fd) {
static const char* body = "{\"error\":\"unauthorized\",\"code\":\"auth_required\"}";
char resp[256];
int n = snprintf(resp, sizeof(resp),
"HTTP/1.1 401 Unauthorized\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n\r\n%s",
strlen(body), body);
if (n > 0) http_send_all(fd, resp, (size_t)n);
}
el_val_t http_serve(el_val_t port, el_val_t handler) {
/* If `handler` looks like a string name, register it as the active handler. */
const char* hname = EL_CSTR(handler);
@@ -1610,13 +1717,13 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
el_http_apply_bind_addr(&addr);
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
fprintf(stderr, "[http] listening on %s port %d\n", el_http_bind_desc(), p);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
@@ -1866,13 +1973,13 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
el_http_apply_bind_addr(&addr);
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
fprintf(stderr, "[http v2] listening on %s port %d\n", el_http_bind_desc(), p);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
@@ -1968,13 +2075,13 @@ void http_serve_async(el_val_t port, el_val_t handler) {
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
el_http_apply_bind_addr(&addr);
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
fprintf(stderr, "[http] async listening on [::]:%d (dual-stack)\n", p);
fprintf(stderr, "[http] async listening on %s port %d\n", el_http_bind_desc(), p);
HttpServeAsyncArg* a = malloc(sizeof(HttpServeAsyncArg));
if (!a) { close(sock); return; }
a->sock = sock;
@@ -3139,10 +3246,72 @@ static char* jp_parse_string_raw(JsonParser* jp) {
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'u': {
/* Skip 4 hex digits; emit '?' as a placeholder */
for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++;
c = '?';
break;
/* Decode \uXXXX (with surrogate pairs) to UTF-8.
* Ported from lang/releases/v1.0.0-20260501 (2026-08-08
* self-review). This copy carried the identical defect:
* the escape was skipped and a literal '?' emitted, which
* silently destroyed every non-ASCII character in any JSON
* string entering the runtime. Two copies of one parser
* bug is exactly how this class of fault survives, so the
* fix lands in both. See the release copy for the full
* measurement and rationale. */
unsigned cp = 0;
int ok = 1;
for (int i = 0; i < 4; i++) {
if (jp->p >= jp->end) { ok = 0; break; }
char h = *jp->p++;
unsigned d;
if (h >= '0' && h <= '9') d = (unsigned)(h - '0');
else if (h >= 'a' && h <= 'f') d = (unsigned)(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') d = (unsigned)(h - 'A' + 10);
else { ok = 0; break; }
cp = (cp << 4) | d;
}
if (!ok) { c = '?'; break; }
if (cp >= 0xD800 && cp <= 0xDBFF &&
(size_t)(jp->end - jp->p) >= 6 &&
jp->p[0] == '\\' && jp->p[1] == 'u') {
const char* save = jp->p;
unsigned lo = 0; int ok2 = 1;
jp->p += 2;
for (int i = 0; i < 4; i++) {
char h = *jp->p++;
unsigned d;
if (h >= '0' && h <= '9') d = (unsigned)(h - '0');
else if (h >= 'a' && h <= 'f') d = (unsigned)(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') d = (unsigned)(h - 'A' + 10);
else { ok2 = 0; break; }
lo = (lo << 4) | d;
}
if (ok2 && lo >= 0xDC00 && lo <= 0xDFFF)
cp = 0x10000u + ((cp - 0xD800u) << 10) + (lo - 0xDC00u);
else jp->p = save;
}
if (cp >= 0xD800 && cp <= 0xDFFF) cp = 0xFFFD;
char ub[4]; int un;
if (cp < 0x80) {
ub[0] = (char)cp; un = 1;
} else if (cp < 0x800) {
ub[0] = (char)(0xC0 | (cp >> 6));
ub[1] = (char)(0x80 | (cp & 0x3F)); un = 2;
} else if (cp < 0x10000) {
ub[0] = (char)(0xE0 | (cp >> 12));
ub[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
ub[2] = (char)(0x80 | (cp & 0x3F)); un = 3;
} else {
ub[0] = (char)(0xF0 | (cp >> 18));
ub[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
ub[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
ub[3] = (char)(0x80 | (cp & 0x3F)); un = 4;
}
while (len + (size_t)un >= cap) {
cap *= 2;
out = realloc(out, cap);
if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
}
for (int i = 0; i < un; i++) out[len++] = ub[i];
continue; /* bytes already appended */
}
default: c = esc; break;
}
@@ -6048,6 +6217,13 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25
#define ENGRAM_INHIBITION_FACTOR 0.1
/* ENGRAM_WM_CAP: hard global ceiling on nodes holding working_memory_weight
* > 0 at any time. Cowan (2001) puts human WM capacity at ~4 chunks; 24 gives
* the daemon generous headroom while preventing the unbounded growth observed
* in production (wm_active 288-778 per heartbeat "working memory" that is
* really the whole recently-touched graph). Ported from release runtime
* v1.0.0-20260501 Pass 5 on 2026-07-15 self-review. */
#define ENGRAM_WM_CAP 24
/* ── Layered consciousness architecture ──────────────────────────────────────
*
@@ -7252,6 +7428,48 @@ static double engram_goal_bias(const EngramNode* n, const char* query) {
return bias;
}
/* eg_cmp_double_desc — qsort comparator, descending doubles. */
static int eg_cmp_double_desc(const void* a, const void* b) {
double da = *(const double*)a, db = *(const double*)b;
if (da < db) return 1;
if (da > db) return -1;
return 0;
}
/* eg_enforce_wm_cap_global — clamp the store-wide working-memory population
* to ENGRAM_WM_CAP, keeping the top-K by current weight. Runs at every point
* that materializes WM: post-activation persist and snapshot load/merge.
* (Ported from release runtime v1.0.0-20260501 Pass 5, 2026-07-15.) */
static void eg_enforce_wm_cap_global(EngramStore* g) {
int64_t wm_count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) wm_count++;
}
if (wm_count <= ENGRAM_WM_CAP) return;
double* vals = malloc((size_t)wm_count * sizeof(double));
if (!vals) return; /* OOM: over cap this call, no corruption */
int64_t vi = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0)
vals[vi++] = g->nodes[i].working_memory_weight;
}
qsort(vals, (size_t)wm_count, sizeof(double), eg_cmp_double_desc);
double cutoff = vals[ENGRAM_WM_CAP - 1];
free(vals);
int64_t above = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > cutoff) above++;
}
int64_t slots_at_cutoff = ENGRAM_WM_CAP - above;
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
if (n->working_memory_weight <= 0.0) continue;
if (n->working_memory_weight > cutoff) continue;
if (slots_at_cutoff > 0) { slots_at_cutoff--; continue; }
n->working_memory_weight = 0.0; /* evict: over global cap */
}
}
el_val_t engram_activate(el_val_t query, el_val_t depth) {
EngramStore* g = engram_get();
const char* q = EL_CSTR(query);
@@ -7457,6 +7675,12 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
g->nodes[i].working_memory_weight = wm_weights[i];
}
/* Global WM cap: keep only the top ENGRAM_WM_CAP by weight across the
* whole store (see eg_enforce_wm_cap_global). Without this, repeated
* activation calls accumulate hundreds of "promoted" nodes and WM stops
* meaning anything (production heartbeats showed wm_active up to 778). */
eg_enforce_wm_cap_global(g);
/* ── Collect all background-activated nodes for the return value ────
* Callers see both layers. Context compilation uses only promoted nodes
* (working_memory_weight > 0). Sort: promoted first by wm_weight desc,
@@ -7834,6 +8058,9 @@ el_val_t engram_load(el_val_t path) {
}
}
}
/* WM cap discipline applies to every entry point that materializes WM,
* including snapshot restore (see eg_enforce_wm_cap_global). */
eg_enforce_wm_cap_global(g);
free(data);
return 1;
}
@@ -7854,6 +8081,33 @@ el_val_t engram_get_node_json(el_val_t id) {
return el_wrap_str(jb_finish(&b));
}
/* engram_get_node_by_label — find the first node whose label field exactly
* matches the given string. Returns the node as a JSON object string, or "{}"
* if no match is found.
*
* Exact match (strcmp, not substring) because labels like "conv:history"
* must not collide with nodes whose content contains that substring.
*
* Ported from the release runtime 2026-07-16 self-review: chat.el has called
* this since 2026-07-01 but the function only existed in
* releases/v1.0.0-20260501/el_runtime.c the soul daemon (which builds
* against THIS runtime) failed to compile once clang made implicit
* declarations an error. */
el_val_t engram_get_node_by_label(el_val_t label) {
const char* lbl = EL_CSTR(label);
if (!lbl || !*lbl) return el_wrap_str(el_strdup("{}"));
EngramStore* g = engram_get();
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
if (n->label && strcmp(n->label, lbl) == 0) {
JsonBuf b; jb_init(&b);
engram_emit_node_json(&b, n);
return el_wrap_str(jb_finish(&b));
}
}
return el_wrap_str(el_strdup("{}"));
}
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
EngramStore* g = engram_get();
const char* q = EL_CSTR(query);
@@ -8411,6 +8665,9 @@ el_val_t engram_load_merge(el_val_t path) {
}
}
/* Merged nodes can carry snapshot WM weights too — hold the cap here as
* well (see eg_enforce_wm_cap_global). */
eg_enforce_wm_cap_global(g);
free(data);
return (el_val_t)added_nodes;
}
+1
View File
@@ -632,6 +632,7 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);