diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index d091134..a30352c 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -10605,3 +10605,322 @@ void __channel_close(el_val_t ch_v) { pthread_mutex_unlock(&ch->mu); } +/* ── DHARMA runtime additions ──────────────────────────────────────────────── + * + * Functions required by the dharma registry service. Added here so the + * released el_runtime.c includes them without requiring dharma to bundle + * its own stubs. + * + * Functions added: + * list_len — alias for el_list_len (used in handlers.el) + * list_get — alias for el_list_get (used in handlers.el) + * json_array_push — append a pre-encoded JSON element to a JSON array string + * now_millis — milliseconds since Unix epoch (alias for time_now) + * unix_timestamp_ms — same as now_millis (alias) + * time_now_ms — same as now_millis (alias) + * log_info — stderr structured log at INFO level + * log_warn — stderr structured log at WARN level + * config — reads a config value from the environment + * http_patch — HTTP PATCH with JSON Content-Type + * http_post_engram — HTTP POST with optional X-API-Key header + * http_get_engram — HTTP GET with optional X-API-Key header + * str_to_bytes — encode a string as a JSON array of byte values + * bytes_to_str — decode a JSON array of byte values back to a string + * hash_sha256 — SHA-256 hex digest of a string + */ + +/* list_len — return the number of elements in a list. */ +el_val_t list_len(el_val_t list) { + return el_list_len(list); +} + +/* list_get — return the element at index i in a list. */ +el_val_t list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\"" + * or "42") to the JSON array string arr. Returns a new JSON array string. + * Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]" + * json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */ +el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) { + const char* arr = EL_CSTR(arr_v); + const char* elem = EL_CSTR(elem_v); + if (!arr || !*arr) arr = "[]"; + if (!elem || !*elem) elem = "null"; + + /* Trim whitespace, find the closing ']'. */ + const char* p = arr; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '[') { + /* Not an array — return a single-element array. */ + size_t n = strlen(elem) + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + size_t arr_len = strlen(arr); + size_t elem_len = strlen(elem); + + /* Walk from the end to find the matching ']'. */ + const char* end = arr + arr_len - 1; + while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--; + if (*end != ']') { + /* Malformed — wrap elem in a new array. */ + size_t n = elem_len + 4; + char* out = el_strbuf(n); + snprintf(out, n, "[%s]", elem); + return el_wrap_str(out); + } + + /* Content between '[' and ']'. */ + const char* inner_start = p + 1; + const char* inner_end = end; /* points AT ']' */ + /* Check if the array is empty (only whitespace between brackets). */ + const char* q = inner_start; + while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++; + int empty = (q == inner_end); + + /* Build: prefix + (comma if non-empty) + elem + "]" */ + size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */ + size_t sep_len = empty ? 0 : 1; /* "," if non-empty */ + size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */ + char* out = el_strbuf(out_len); + memcpy(out, arr, prefix_len); + if (!empty) out[prefix_len] = ','; + memcpy(out + prefix_len + sep_len, elem, elem_len); + out[prefix_len + sep_len + elem_len] = ']'; + out[prefix_len + sep_len + elem_len + 1] = '\0'; + return el_wrap_str(out); +} + +/* now_millis — milliseconds since Unix epoch. */ +el_val_t now_millis(void) { + return time_now(); +} + +/* unix_timestamp_ms — same as now_millis. */ +el_val_t unix_timestamp_ms(void) { + return time_now(); +} + +/* time_now_ms — same as now_millis. */ +el_val_t time_now_ms(void) { + return time_now(); +} + +/* log_info — write a structured [INFO] line to stderr. */ +void log_info(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[INFO] %s\n", msg ? msg : ""); +} + +/* log_warn — write a structured [WARN] line to stderr. */ +void log_warn(el_val_t msg_v) { + const char* msg = EL_CSTR(msg_v); + fprintf(stderr, "[WARN] %s\n", msg ? msg : ""); +} + +/* config — read a configuration value from the environment. + * Returns "" if the variable is not set (same as __env_get). */ +el_val_t config(el_val_t key_v) { + const char* key = EL_CSTR(key_v); + if (!key || !*key) return EL_STR(""); + const char* val = getenv(key); + if (!val) return EL_STR(""); + return el_wrap_str(el_strdup(val)); +} + +/* http_patch — HTTP PATCH request with Content-Type: application/json. + * Returns the response body (same error convention as http_post_json). */ +el_val_t http_patch(el_val_t url_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_post_engram — HTTP POST with optional X-API-Key header. + * If key is "" no authentication header is sent. */ +el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + const char* body = EL_CSTR(body_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + h = curl_slist_append(h, "Content-Type: application/json"); + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : ""); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* http_get_engram — HTTP GET with optional X-API-Key header. */ +el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) { + const char* url = EL_CSTR(url_v); + const char* key = EL_CSTR(key_v); + if (!url || !*url) return http_error_json("empty url"); + CURL* c = curl_easy_init(); + if (!c) return http_error_json("curl_easy_init failed"); + HttpBuf rb; httpbuf_init(&rb); + char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0'; + struct curl_slist* h = NULL; + if (key && *key) { + size_t n = strlen(key) + 32; + char* hdr = malloc(n); + snprintf(hdr, n, "X-API-Key: %s", key); + h = curl_slist_append(h, hdr); + free(hdr); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms()); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0"); + CURLcode rc = curl_easy_perform(c); + if (h) curl_slist_free_all(h); + curl_easy_cleanup(c); + if (rc != CURLE_OK) { + free(rb.data); + const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc); + return http_error_json(m); + } + return el_wrap_str(rb.data); +} + +/* str_to_bytes — encode a string as a JSON array of unsigned byte values. + * "hello" -> "[104,101,108,108,111]" + * Used by db.el to store binary content in Engram JSON nodes. */ +el_val_t str_to_bytes(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s || !*s) return el_wrap_str(el_strdup("[]")); + size_t n = strlen(s); + /* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */ + char* out = el_strbuf(n * 4 + 3); + size_t pos = 0; + out[pos++] = '['; + for (size_t i = 0; i < n; i++) { + unsigned char b = (unsigned char)s[i]; + if (i > 0) out[pos++] = ','; + /* Write decimal representation of b. */ + if (b >= 100) { + out[pos++] = (char)('0' + b / 100); + out[pos++] = (char)('0' + (b / 10) % 10); + out[pos++] = (char)('0' + b % 10); + } else if (b >= 10) { + out[pos++] = (char)('0' + b / 10); + out[pos++] = (char)('0' + b % 10); + } else { + out[pos++] = (char)('0' + b); + } + } + out[pos++] = ']'; + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* bytes_to_str — decode a JSON array of integer byte values back to a string. + * "[104,101,108,108,111]" -> "hello" + * Inverse of str_to_bytes. */ +el_val_t bytes_to_str(el_val_t arr_v) { + const char* s = EL_CSTR(arr_v); + if (!s) return el_wrap_str(el_strdup("")); + /* Skip whitespace, expect '['. */ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return el_wrap_str(el_strdup("")); + s++; + + /* Count elements to size the output buffer. */ + int64_t n = (int64_t)json_array_len(arr_v); + if (n <= 0) return el_wrap_str(el_strdup("")); + + char* out = el_strbuf((size_t)n + 1); + size_t pos = 0; + + /* Walk the array, parse each integer, store as a byte. */ + while (*s) { + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']' || *s == '\0') break; + /* Parse decimal integer. */ + char* end_ptr; + long v = strtol(s, &end_ptr, 10); + if (end_ptr == s) break; /* parse failure */ + s = end_ptr; + if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + out[pos] = '\0'; + return el_wrap_str(out); +} + +/* hash_sha256 — return the SHA-256 hex digest of a string. + * Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */ +el_val_t hash_sha256(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + unsigned char digest[32]; + el_sha256_oneshot((const unsigned char*)s, strlen(s), digest); + return el_hex_encode(digest, 32); +} + diff --git a/el-compiler/runtime/el_seed.c b/el-compiler/runtime/el_seed.c index 618ed47..5b47903 100644 --- a/el-compiler/runtime/el_seed.c +++ b/el-compiler/runtime/el_seed.c @@ -1097,3 +1097,109 @@ el_val_t __engram_list_layers_json(void) { return engram_list_ el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth) { return engram_compile_layered_json(intent, depth); } + +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ +/* + * SHA-256 — self-contained implementation (no OpenSSL dependency). + * Based on Brad Conte's public-domain reference implementation. + */ + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} _seed_sha256_ctx; + +static const uint32_t _seed_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define _SEED_ROTR32(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define _SEED_CH(x,y,z) (((x)&(y))^(~(x)&(z))) +#define _SEED_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) +#define _SEED_EP0(x) (_SEED_ROTR32(x,2)^_SEED_ROTR32(x,13)^_SEED_ROTR32(x,22)) +#define _SEED_EP1(x) (_SEED_ROTR32(x,6)^_SEED_ROTR32(x,11)^_SEED_ROTR32(x,25)) +#define _SEED_SIG0(x) (_SEED_ROTR32(x,7)^_SEED_ROTR32(x,18)^((x)>>3)) +#define _SEED_SIG1(x) (_SEED_ROTR32(x,17)^_SEED_ROTR32(x,19)^((x)>>10)) + +static void _seed_sha256_transform(_seed_sha256_ctx* ctx, const uint8_t* data) { + uint32_t a,b,c,d,e,f,g,h,t1,t2,m[64]; + for (int i=0,j=0; i<16; i++,j+=4) + m[i]=(uint32_t)(data[j]<<24)|(data[j+1]<<16)|(data[j+2]<<8)|data[j+3]; + for (int i=16; i<64; i++) + m[i]=_SEED_SIG1(m[i-2])+m[i-7]+_SEED_SIG0(m[i-15])+m[i-16]; + a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3]; + e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7]; + for (int i=0; i<64; i++) { + t1=h+_SEED_EP1(e)+_SEED_CH(e,f,g)+_seed_sha256_k[i]+m[i]; + t2=_SEED_EP0(a)+_SEED_MAJ(a,b,c); + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; + ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} + +static void _seed_sha256_init(_seed_sha256_ctx* ctx) { + ctx->datalen=0; ctx->bitlen=0; + ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; + ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; + ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; + ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} + +static void _seed_sha256_update(_seed_sha256_ctx* ctx, const uint8_t* data, size_t len) { + for (size_t i=0; idata[ctx->datalen++] = data[i]; + if (ctx->datalen==64) { _seed_sha256_transform(ctx,ctx->data); ctx->bitlen+=512; ctx->datalen=0; } + } +} + +static void _seed_sha256_final(_seed_sha256_ctx* ctx, uint8_t hash[32]) { + uint32_t i=ctx->datalen; + ctx->data[i++]=0x80; + if (ctx->datalen<56) { while(i<56) ctx->data[i++]=0; } + else { while(i<64) ctx->data[i++]=0; _seed_sha256_transform(ctx,ctx->data); memset(ctx->data,0,56); } + ctx->bitlen+=ctx->datalen*8; + ctx->data[63]=(uint8_t)(ctx->bitlen); ctx->data[62]=(uint8_t)(ctx->bitlen>>8); + ctx->data[61]=(uint8_t)(ctx->bitlen>>16); ctx->data[60]=(uint8_t)(ctx->bitlen>>24); + ctx->data[59]=(uint8_t)(ctx->bitlen>>32); ctx->data[58]=(uint8_t)(ctx->bitlen>>40); + ctx->data[57]=(uint8_t)(ctx->bitlen>>48); ctx->data[56]=(uint8_t)(ctx->bitlen>>56); + _seed_sha256_transform(ctx,ctx->data); + for (i=0; i<4; i++) { + hash[i] =(uint8_t)(ctx->state[0]>>(24-i*8)); + hash[i+4] =(uint8_t)(ctx->state[1]>>(24-i*8)); + hash[i+8] =(uint8_t)(ctx->state[2]>>(24-i*8)); + hash[i+12] =(uint8_t)(ctx->state[3]>>(24-i*8)); + hash[i+16] =(uint8_t)(ctx->state[4]>>(24-i*8)); + hash[i+20] =(uint8_t)(ctx->state[5]>>(24-i*8)); + hash[i+24] =(uint8_t)(ctx->state[6]>>(24-i*8)); + hash[i+28] =(uint8_t)(ctx->state[7]>>(24-i*8)); + } +} + +el_val_t __sha256_hex(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) s = ""; + _seed_sha256_ctx ctx; + _seed_sha256_init(&ctx); + _seed_sha256_update(&ctx, (const uint8_t*)s, strlen(s)); + uint8_t digest[32]; + _seed_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + char* out = malloc(65); + if (!out) return EL_STR(""); + for (int i=0; i<32; i++) { + out[i*2] = hex[(digest[i]>>4)&0xf]; + out[i*2+1] = hex[digest[i]&0xf]; + } + out[64] = '\0'; + return EL_STR(out); +} diff --git a/el-compiler/runtime/el_seed.h b/el-compiler/runtime/el_seed.h index 8bd4d8b..04d53ca 100644 --- a/el-compiler/runtime/el_seed.h +++ b/el-compiler/runtime/el_seed.h @@ -233,6 +233,12 @@ el_val_t __engram_stats_json(void); el_val_t __engram_list_layers_json(void); el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth); +/* ── Cryptographic hashing ────────────────────────────────────────────────── */ + +/* __sha256_hex — return the SHA-256 hex digest of a string. + * The returned string is 64 hex characters (lowercase). */ +el_val_t __sha256_hex(el_val_t s); + /* ── args init (called from main) ────────────────────────────────────────── */ /* Store argc/argv for __args_json. Call once at the start of main(). */ void el_seed_init_args(int argc, char** argv); diff --git a/runtime/env.el b/runtime/env.el index c9883a9..5e8c832 100644 --- a/runtime/env.el +++ b/runtime/env.el @@ -63,6 +63,34 @@ fn state_keys() -> String { return __state_keys() } +// ── DHARMA runtime helpers ───────────────────────────────────────────────── + +// config — read a configuration value from the environment. +// Returns "" if the variable is not set. Alias for env(). +fn config(key: String) -> String { + return __env_get(key) +} + +// log_info — write an [INFO] log line to stdout. +fn log_info(msg: String) { + __println("[INFO] " + msg) +} + +// log_warn — write a [WARN] log line to stdout. +fn log_warn(msg: String) { + __println("[WARN] " + msg) +} + +// list_len — return the number of elements in a list. Alias for el_list_len. +fn list_len(lst: [String]) -> Int { + return el_list_len(lst) +} + +// list_get — return the element at index i in a list. Alias for el_list_get. +fn list_get(lst: [String], i: Int) -> String { + return el_list_get(lst, i) +} + // ── UUID generation ──────────────────────────────────────────────────────── // uuid_new — generate a new random UUID v4. diff --git a/runtime/http.el b/runtime/http.el index 587d3dc..aa0f879 100644 --- a/runtime/http.el +++ b/runtime/http.el @@ -170,6 +170,35 @@ fn http_response(status: Int, headers_json: String, body: String) -> String { return __http_response(status, headers_json, body) } +// ── HTTP client — PATCH ─────────────────────────────────────────────────────── + +// http_patch performs an HTTP PATCH request with Content-Type: application/json. +fn http_patch(url: String, body: String) -> String { + return __http_do("PATCH", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms()) +} + +// ── HTTP client — Engram variants (optional API key) ────────────────────────── +// +// These are used by dharma's db.el to talk to Engram nodes. +// The key parameter is the X-API-Key header value; pass "" for no auth. + +// http_post_engram performs an HTTP POST with Content-Type: application/json +// and an optional X-API-Key header. If key is "" no auth header is added. +fn http_post_engram(url: String, key: String, body: String) -> String { + if str_eq(key, "") { + return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms()) + } + return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\",\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms()) +} + +// http_get_engram performs an HTTP GET with an optional X-API-Key header. +fn http_get_engram(url: String, key: String) -> String { + if str_eq(key, "") { + return __http_do("GET", url, "", "{}", el_http_timeout_ms()) + } + return __http_do("GET", url, "", "{\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms()) +} + // ── SSE — Server-Sent Events streaming ─────────────────────────────────────── // // Usage pattern for an SSE handler: diff --git a/runtime/json.el b/runtime/json.el index b7a0db9..c1b7ba7 100644 --- a/runtime/json.el +++ b/runtime/json.el @@ -157,6 +157,24 @@ fn json_build_array(items: [String]) -> String { return result + "]" } +// json_array_push — append a pre-encoded JSON element to a JSON array string. +// elem must be a valid JSON fragment (e.g. "\"foo\"" or "42"). +// Returns a new JSON array string with elem appended. +// Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]" +fn json_array_push(arr: String, elem: String) -> String { + let n: Int = json_array_len(arr) + if n == 0 { + return "[" + elem + "]" + } + // arr ends with ']'; insert before it + let inner_end: Int = str_last_index_of(arr, "]") + if inner_end < 0 { + return "[" + elem + "]" + } + let prefix: String = str_slice(arr, 0, inner_end) + return prefix + "," + elem + "]" +} + // json_escape_string — escape a raw string so it can be safely embedded as a // JSON string value. // @@ -171,3 +189,25 @@ fn json_escape_string(s: String) -> String { let s5: String = str_replace(s4, "\t", "\\t") return s5 } + +// --------------------------------------------------------------------------- +// DHARMA byte decoding +// --------------------------------------------------------------------------- + +// bytes_to_str — decode a JSON array of integer byte values back to a string. +// "[104,105]" -> "hi" +// Inverse of str_to_bytes (defined in string.el). Defined here because it +// depends on json_array_len and json_array_get_string which live in this file. +fn bytes_to_str(arr: String) -> String { + let n: Int = json_array_len(arr) + if n == 0 { return "" } + let out: String = __str_alloc(n) + let i: Int = 0 + while i < n { + let elem: String = json_array_get_string(arr, i) + let b: Int = __str_to_int(elem) + out = __str_set_char(out, i, b) + i = i + 1 + } + return out +} diff --git a/runtime/string.el b/runtime/string.el index 2292766..f597bbe 100644 --- a/runtime/string.el +++ b/runtime/string.el @@ -876,3 +876,32 @@ fn str_join(parts: [String], sep: String) -> String { } return result } + +// ── DHARMA byte encoding (str_to_bytes) ────────────────────────────────────── +// +// str_to_bytes — encode a string as a JSON array of unsigned byte values. +// "hi" -> "[104,105]" +// Used by db.el to store content in Engram JSON nodes as a byte array. +// Note: bytes_to_str (the inverse) is defined in json.el because it depends +// on json_array_get_string which is defined there. +fn str_to_bytes(s: String) -> String { + let n: Int = __str_len(s) + if n == 0 { return "[]" } + let result: String = "[" + let i: Int = 0 + while i < n { + let b: Int = __str_char_at(s, i) + if i > 0 { result = __str_concat_raw(result, ",") } + result = __str_concat_raw(result, __int_to_str(b)) + i = i + 1 + } + return __str_concat_raw(result, "]") +} + +// ── Cryptographic hashing ───────────────────────────────────────────────────── + +// hash_sha256 — return the SHA-256 hex digest of a string. +// Delegates to the __sha256_hex seed primitive. +fn hash_sha256(s: String) -> String { + return __sha256_hex(s) +} diff --git a/runtime/time.el b/runtime/time.el index df4cf0e..5ceb6ef 100644 --- a/runtime/time.el +++ b/runtime/time.el @@ -400,3 +400,26 @@ fn uuid_new() -> String { fn uuid_v4() -> String { return __uuid_v4() } + +// --------------------------------------------------------------------------- +// DHARMA-compatible aliases — millisecond-precision timestamps. +// +// now_millis, unix_timestamp_ms, and time_now_ms all return the same value: +// milliseconds since the Unix epoch. They exist because different parts of +// the dharma codebase use different names for the same concept. +// --------------------------------------------------------------------------- + +// now_millis — milliseconds since Unix epoch. Alias for time_now(). +fn now_millis() -> Int { + return __time_now_ns() / 1000000 +} + +// unix_timestamp_ms — same as now_millis. +fn unix_timestamp_ms() -> Int { + return __time_now_ns() / 1000000 +} + +// time_now_ms — same as now_millis. +fn time_now_ms() -> Int { + return __time_now_ns() / 1000000 +}