/* Deliberately small, stateless loopback HTTP adapter. No files or subprocesses. * Included after lp_predict so the executable uses its embedded model directly. */ #include #include #include #include #include #include #define RH_BODY 8192 #define RH_HEADER 8192 #define RH_TOKENS 64 #define RH_STRING 2048 /* A bounded JSON tree: children are linked siblings; decoded strings are UTF-8. */ typedef struct { int type, child, next; char value[RH_STRING]; } RhToken; typedef struct { const unsigned char *p; RhToken t[RH_TOKENS]; int count; } RhJson; static int rh_utf8(const unsigned char *p,size_t n) { for(size_t i=0;i=0xc2 && c<=0xdf) {k=1;v=c&31;} else if(c>=0xe0 && c<=0xef) {k=2;v=c&15;} else if(c>=0xf0 && c<=0xf4) {k=3;v=c&7;} else return 0; if(i+(size_t)k>n) return 0; for(int j=0;j0x10ffff||(v>=0xd800&&v<=0xdfff))return 0; } return 1; } static int rh_hex(unsigned c) {if(c>='0'&&c<='9')return (int)c-'0';if(c>='a'&&c<='f')return (int)c-'a'+10;if(c>='A'&&c<='F')return (int)c-'A'+10;return -1;} static void rh_ws(RhJson *j) {while(*j->p==' '||*j->p=='\t'||*j->p=='\r'||*j->p=='\n')j->p++;} static int rh_string(RhJson *j,char *out) { size_t n=0;if(*j->p++!='"')return 0; while(*j->p && *j->p!='"') { unsigned c=*j->p++;if(c<32)return 0; if(c=='\\') { c=*j->p++;if(!c)return 0; if(c=='u') { unsigned v=0;for(int i=0;i<4;i++){int h=rh_hex(*j->p);if(h<0)return 0;j->p++;v=(v<<4)|(unsigned)h;} if(!v||(v>=0xd800&&v<=0xdfff))return 0; if(v<128)c=v; else {if(n+3>=RH_STRING)return 0;if(v<2048)out[n++]=(char)(0xc0|(v>>6));else {out[n++]=(char)(0xe0|(v>>12));out[n++]=(char)(0x80|((v>>6)&63));}c=0x80|(v&63);} } else if(c=='b')c='\b';else if(c=='f')c='\f';else if(c=='n')c='\n';else if(c=='r')c='\r';else if(c=='t')c='\t';else if(c!='"'&&c!='\\'&&c!='/')return 0; } if(n+1>=RH_STRING)return 0;out[n++]=(char)c; } if(*j->p!='"')return 0;j->p++;out[n]=0;return 1; } static int rh_parse_value(RhJson *j,int depth) { rh_ws(j);if(depth>8||j->count>=RH_TOKENS)return -1; int id=j->count++;RhToken *t=&j->t[id];t->child=t->next=-1; unsigned c=*j->p; if(c=='"'){t->type='s';return rh_string(j,t->value)?id:-1;} if(c=='{'||c=='[') { t->type=(int)c;j->p++;rh_ws(j);unsigned end=c=='{'?'}':']';int last=-1; if(*j->p==end){j->p++;return id;} while(1) { int item; if(c=='{') { if(*j->p!='"'||j->count>=RH_TOKENS)return -1; item=j->count++;RhToken *key=&j->t[item];key->type='k';key->child=key->next=-1; if(!rh_string(j,key->value))return -1; for(int k=t->child;k>=0;k=j->t[k].next)if(!strcmp(j->t[k].value,key->value))return -1; rh_ws(j);if(*j->p++!=':')return -1; key->child=rh_parse_value(j,depth+1);if(key->child<0)return -1; } else {item=rh_parse_value(j,depth+1);if(item<0)return -1;} if(last<0)t->child=item;else j->t[last].next=item;last=item; rh_ws(j);if(*j->p==end){j->p++;return id;}if(*j->p++!=',')return -1;rh_ws(j); } } if(c>='0'&&c<='9') { t->type='n';size_t n=0; do {if(n>=20)return -1;t->value[n++]=(char)*j->p++;}while(*j->p>='0'&&*j->p<='9'); t->value[n]=0;if(n>1&&c=='0')return -1;return id; } const char *literals[]={"false","true","null"}; for(int i=0;i<3;i++)if(!strncmp((const char*)j->p,literals[i],strlen(literals[i]))) {t->type='l';strcpy(t->value,literals[i]);j->p+=strlen(literals[i]);return id;} return -1; } static int rh_parse(RhJson *j,const char *s) { memset(j,0,sizeof(*j));j->p=(const unsigned char*)s; if(!rh_utf8(j->p,strlen(s))||rh_parse_value(j,0)!=0)return 0;rh_ws(j);return !*j->p; } static int rh_get(RhJson *j,int obj,const char *name) { if(obj<0||j->t[obj].type!='{')return -1; for(int k=j->t[obj].child;k>=0;k=j->t[k].next)if(!strcmp(j->t[k].value,name))return j->t[k].child;return -1; } static int rh_fields(RhJson *j,int obj,const char *const *names,int count) { if(obj<0||j->t[obj].type!='{')return 0; for(int k=j->t[obj].child;k>=0;k=j->t[k].next){int found=0;for(int n=0;nt[k].value,names[n]))found=1;if(!found)return 0;}return 1; } static int rh_eq(RhJson *j,int id,int type,const char *s){return id>=0&&j->t[id].type==type&&!strcmp(j->t[id].value,s);} static int rh_uint(const char *s,uint64_t max,uint64_t *out) { uint64_t n=0;if(!*s)return 0;for(;*s;s++){if(*s<'0'||*s>'9'||n>(max-(unsigned)(*s-'0'))/10)return 0;n=n*10+(unsigned)(*s-'0');}*out=n;return 1; } static int64_t rh_now(void){struct timespec t;if(clock_gettime(CLOCK_MONOTONIC,&t))return 0;return (int64_t)t.tv_sec*1000+t.tv_nsec/1000000;} static ssize_t rh_read(int fd,void *buf,size_t n,int64_t deadline) { for(;;){int64_t left=deadline-rh_now();if(left<=0)return -1;struct pollfd p={fd,POLLIN,0};int r=poll(&p,1,(int)left);if(r<0&&errno==EINTR)continue;if(r<=0)return -1;return recv(fd,buf,n,0);} } static void rh_reply(int fd,int code,const char *body) { char h[256];const char *reason=code==200?"OK":code==400?"Bad Request":code==404?"Not Found":code==405?"Method Not Allowed":code==413?"Content Too Large":code==415?"Unsupported Media Type":"Request Timeout"; int n=snprintf(h,sizeof(h),"HTTP/1.1 %d %s\r\nContent-Type: application/json\r\nContent-Length: %zu\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\n\r\n",code,reason,strlen(body)); const char *parts[]={h,body};size_t sizes[]={(size_t)n,strlen(body)}; int64_t deadline=rh_now()+5000; for(int p=0;p<2;p++){size_t off=0;while(off=0&&!rh_eq(&j,stream,'l',"false"))||(store>=0&&!rh_eq(&j,store,'l',"false"))){rh_error(fd,400,"Streaming and storage are unsupported; use false or omit");return;} int input=rh_get(&j,0,"input"); if(input>=0&&j.t[input].type=='[') { int msg=j.t[input].child;const char *mf[]={"role","content"};const char *cf[]={"type","text"}; if(msg<0||j.t[msg].next>=0||!rh_fields(&j,msg,mf,2)||!rh_eq(&j,rh_get(&j,msg,"role"),'s',"user")){rh_error(fd,400,"Input must contain exactly one user message");return;} int content=rh_get(&j,msg,"content"),part=content>=0?j.t[content].child:-1; if(content<0||j.t[content].type!='['||part<0||j.t[part].next>=0||!rh_fields(&j,part,cf,2)||!rh_eq(&j,rh_get(&j,part,"type"),'s',"input_text")){rh_error(fd,400,"Content must contain exactly one input_text part");return;} input=rh_get(&j,part,"text"); } if(input<0||j.t[input].type!='s'||!rh_parse(&e,j.t[input].value)){rh_error(fd,400,"Input text must be a JSON task envelope");return;} const char *ef[]={"command","binary_hex","base_offset"};int command=rh_get(&e,0,"command"),hex=rh_get(&e,0,"binary_hex"),base=rh_get(&e,0,"base_offset");uint64_t offset=0; if(!rh_fields(&e,0,ef,3)||command<0||e.t[command].type!='s'||!strlen(e.t[command].value)||strlen(e.t[command].value)>96||hex<0||e.t[hex].type!='s'||(base>=0&&(e.t[base].type!='n'||!rh_uint(e.t[base].value,UINT64_C(9007199254740736),&offset)))){rh_error(fd,400,"Envelope requires command (1..96 UTF-8 bytes), binary_hex, optional nonnegative bounded integer base_offset");return;} size_t size=strlen(e.t[hex].value);unsigned char bytes[256]; if(!size||size>512||size%2){rh_error(fd,400,"binary_hex must encode 1..256 bytes");return;} for(size_t i=0;i=4&&!memcmp(header+n-4,"\r\n\r\n",4))break;} if(n<4||memcmp(header+n-4,"\r\n\r\n",4)){rh_error(fd,400,"Header too large");return;}header[n]=0; for(size_t i=0;i=127){rh_error(fd,400,"Invalid header characters");return;} for(size_t i=0;iv&&(tail[-1]==' '||tail[-1]=='\t'))*--tail=0; if(!strcasecmp(p,"Transfer-Encoding")||!strcasecmp(p,"Expect")||!strcasecmp(p,"Origin")){rh_error(fd,400,"Transfer-Encoding, Expect and browser Origin requests are unsupported");return;} if(!strcasecmp(p,"Content-Length")){if(cl++||!rh_uint(v,UINT64_MAX,&length)){rh_error(fd,400,"Invalid or duplicate Content-Length");return;}} if(!strcasecmp(p,"Content-Type")){if(ct++||(strcasecmp(v,"application/json")&&strcasecmp(v,"application/json; charset=utf-8"))){rh_error(fd,415,"Use application/json");return;}} if(!strcasecmp(p,"Host")){char expected[64];snprintf(expected,sizeof(expected),"127.0.0.1:%u",port);if(host++||strcmp(v,expected)){rh_error(fd,400,"Host must match the loopback listener including port");return;}} p=end+2; } if(!host){rh_error(fd,400,"Host is required");return;} if(length>RH_BODY){rh_error(fd,413,"Body exceeds 8192 bytes");return;} if(!strcmp(method,"GET")&&(!strcmp(path,"/health")||!strcmp(path,"/v1/models"))){if(length){rh_error(fd,400,"GET body unsupported");return;}rh_reply(fd,200,!strcmp(path,"/health")?"{\"status\":\"ok\",\"model\":\"lightnest-patch-v2\",\"stateless\":true,\"max_command_utf8_bytes\":96,\"max_binary_bytes\":256,\"streaming\":false,\"semantic_validation\":false}":"{\"object\":\"list\",\"data\":[{\"id\":\"lightnest-patch-v2\",\"object\":\"model\",\"created\":0,\"owned_by\":\"local\"}]}");return;} if(strcmp(path,"/v1/responses")){rh_error(fd,404,"Unknown route");return;}if(strcmp(method,"POST")){rh_error(fd,405,"Use POST");return;} if(!cl||!ct||!length){rh_error(fd,400,"JSON Content-Type and nonzero Content-Length are required");return;} n=0;while(n<(size_t)length){ssize_t got=rh_read(fd,body+n,(size_t)length-n,deadline);if(got<=0){rh_error(fd,408,"Request read timeout or premature EOF");return;}n+=(size_t)got;}body[n]=0; if(memchr(body,0,n)||!rh_utf8((unsigned char*)body,n)){rh_error(fd,400,"Body must be UTF-8 without NUL");return;}rh_response(fd,model,body,serial); } static int rh_serve(const PatchModel *model,const char *port_string) { uint64_t port;if(!rh_uint(port_string,65535,&port)||!port)die("--serve requires port 1..65535"); int server=socket(AF_INET,SOCK_STREAM,0);if(server<0)die("cannot create loopback socket"); struct sockaddr_in addr={0};addr.sin_family=AF_INET;addr.sin_port=htons((uint16_t)port);addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK); if(bind(server,(struct sockaddr*)&addr,sizeof(addr))||listen(server,8)){close(server);die("cannot bind/listen on 127.0.0.1");} fprintf(stderr,"LightNest native stateless Responses subset listening on http://127.0.0.1:%u (no authentication; local processes only)\n",(unsigned)port); uint64_t serial=0;for(;;){int fd=accept(server,NULL,NULL);if(fd<0){if(errno==EINTR)continue;close(server);die("accept failed");}rh_client(fd,model,++serial,(unsigned)port);close(fd);} }