Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 261 additions & 0 deletions .agents/docs/2026-11-06-observable-unit-design-draft.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .agents/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Active design records

- [Observable process units: inventory and recovery draft](2026-11-06-observable-unit-design-draft.md)
records the observed lifetime boundary, the required OS responsibilities and
the fault matrix that precedes a compiler-recovery implementation.
51 changes: 51 additions & 0 deletions .github/checks/build_job_observer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Build the real backend and C ABI consumer with the runner's native compiler."""
import concurrent.futures
import os
from pathlib import Path
import subprocess
import sys

root = Path.cwd()
backend = root / 'backend'
include = root / 'include'
out = root / 'unit-api-build'
out.mkdir(exist_ok=True)
windows = sys.platform == 'win32'

def compile_backend(source):
obj = out / (source.stem + ('.obj' if windows else '.o'))
if windows:
command = ['cl', '/nologo', '/std:c++20', '/c', '/O2', '/GR-', '/EHs-c-', '/GS-',
'/I' + str(include), '/Fo' + str(obj), str(source)]
else:
command = ['c++', '-std=c++23', '-O2', '-ffreestanding', '-fno-exceptions', '-fno-rtti',
'-fno-stack-protector', '-I' + str(include), '-c', str(source), '-o', str(obj)]
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
(out / (source.stem + '.log')).write_text(result.stdout)
if result.returncode:
raise RuntimeError(str(source.name) + '\n' + result.stdout)
return str(obj)

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
objects = list(pool.map(compile_backend, sorted((backend / 'src').glob('*.cpp'))))
consumer = root / '.github/checks/job_observer.cpp'
binary = out / ('job-observer.exe' if windows else 'job-observer')
if windows:
command = ['cl', '/nologo', '/std:c++17', '/EHsc', '/O2', '/I' + str(include), str(consumer),
*objects, '/Fe:' + str(binary), '/link', 'ntdll.lib', 'synchronization.lib',
'shell32.lib', 'kernel32.lib', 'bcrypt.lib']
else:
command = ['c++', '-std=c++17', '-O2', '-pthread', '-I' + str(include), str(consumer),
*objects, '-o', str(binary)]
subprocess.run(command, check=True)
with (out / 'job-observer.jsonl').open('w') as log:
result = subprocess.run([str(binary)], stdout=log, stderr=subprocess.STDOUT, timeout=30)
print((out / 'job-observer.jsonl').read_text())

if result.returncode == 0 and sys.platform == 'linux':
boundary = out / 'retire-boundary'
subprocess.run(['c++', '-std=c++17', '-O2', '-pthread', '-I' + str(include),
str(root / '.github/checks/retire_boundary.cpp'), *objects, '-o', str(boundary)], check=True)
subprocess.run([sys.executable, str(root / '.github/checks/retire_boundaries.py'), str(boundary)], check=True, timeout=30)

raise SystemExit(result.returncode)
205 changes: 205 additions & 0 deletions .github/checks/job_observer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Exercises the implemented C ABI, including a driver with a descendant.
#include <openkal/process.h>
#include <openkal/env.h>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <chrono>
#include <thread>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#ifdef __linux__
#include <sys/prctl.h>
#include <sys/wait.h>
#endif
using namespace std::chrono_literals;
namespace fs = std::filesystem;
void require(bool value,const char* name) {
std::printf("{\"check\":\"%s\",\"ok\":%s}\n",name,value?"true":"false");
std::fflush(stdout); if(!value) throw std::runtime_error(name);
}
std::string image, relative_image;
kal_program_identity root_identity{};
kal_spawn place() {
kal_spawn how{};
for(kal_uintptr i=0;i<kal_fs_preopen_count();++i) {
char name[4096]{}; kal_uintptr n=0; kal_dir d{};
if(kal_fs_preopen(i,&d,name,sizeof name,&n)||n>image.size())continue;
bool same=true;
for(kal_uintptr j=0;j<n;++j) {
char a=name[j],b=image[j];
#ifdef _WIN32
if(a>='A'&&a<='Z')a+=32; if(b>='A'&&b<='Z')b+=32;
#endif
if(a!=b)same=false;
}
if(same && (!how.base.h || n==1 || image[n-1]=='/' || image[n-1]=='\\')) {
how.base=how.work=d; relative_image=image.substr(n);
while(!relative_image.empty()&&(relative_image.front()=='/'||relative_image.front()=='\\'))relative_image.erase(0,1);
return how;
}
}
throw std::runtime_error("program_preopen");
}
kal_process start(std::vector<std::string> arguments,kal_job* job=nullptr,
const kal_spawn_streams* streams=nullptr) {
arguments.insert(arguments.begin(),image);
std::vector<const char*> argv;std::vector<kal_uintptr> lengths;
for(auto& s:arguments){argv.push_back(s.data());lengths.push_back(s.size());}
auto how=place();how.job=job;
kal_process child{};
// An absolute name is resolved by the backend against the directory held.
const int rc=kal_process_spawn(&how,relative_image.data(),relative_image.size(),argv.data(),lengths.data(),argv.size(),
nullptr,nullptr,0,streams,&child);
if(rc)throw std::runtime_error("actual_spawn_"+std::to_string(rc));return child;
}
std::string read_line(kal_stream stream) {
std::string out;char c=0;
while(out.size()<512) {
const auto n=kal_stream_read(stream,&c,1);
if(n!=1)throw std::runtime_error("control_stream_eof");
if(c=='\n')return out;out+=c;
}
throw std::runtime_error("control_stream_bound");
}
void send_line(kal_stream stream,const std::string& value) {
const auto bytes=value+"\n";
if(kal_stream_write(stream,bytes.data(),bytes.size())!=static_cast<kal_intptr>(bytes.size()))
throw std::runtime_error("control_write");
}
int wait(kal_process child,bool success=true) {
int status=0,environment=0;
if(kal_process_wait(child,&status,&environment))throw std::runtime_error("actual_wait");
kal_process_close(child);
if(success && (status||environment))throw std::runtime_error("actual_child_exit_"+std::to_string(status));
return environment ? 128+status : status;
}
kal_u32 state(kal_job job,const std::string& ref) {
kal_u32 value=KAL_JOB_UNKNOWN;
if(kal_process_job_observe(job,ref.data(),ref.size(),&value))return KAL_JOB_UNKNOWN;
return value;
}
void await_empty(kal_job job,const std::string& ref) {
const auto limit=std::chrono::steady_clock::now()+5s;
for(;;) {
#ifdef __linux__
while(waitpid(-1,nullptr,WNOHANG)>0){}
#endif
if(state(job,ref)==KAL_JOB_EMPTY)return;
if(std::chrono::steady_clock::now()>=limit)throw std::runtime_error("whole_unit_not_quiet");
std::this_thread::sleep_for(1ms);
}
}
int helper(const std::string& mode,const fs::path& output,const kal_program_identity& continuation,
const kal_program_identity* expired=nullptr) {
kal_job job{};char bytes[512]{};kal_uintptr used=0;
if(kal_process_job_create(&job,bytes,sizeof bytes,&used))return 41;
const std::string ref(bytes,used);
send_line(mode=="launcher"?kal_stderr():kal_stdout(),ref);
if(read_line(kal_stdin())!="ACK")return 42;
kal_stream read{},write{};
if(kal_process_channel(&read,&write))return 43;
kal_spawn_streams streams{read,{}, {}};
auto driver=start({"driver",output.string()},&job,&streams);
kal_process_channel_close(read);
kal_u32 retired=KAL_JOB_UNKNOWN;
if(expired&&(kal_process_job_retire(job,ref.data(),ref.size(),expired,&retired)||retired!=KAL_JOB_UNKNOWN))return 54;
auto wrong=continuation;++wrong.started_ns;
if(kal_process_job_retire(job,ref.data(),ref.size(),&wrong,&retired)||retired!=KAL_JOB_UNKNOWN)return 49;
if(kal_process_job_retire(job,ref.data(),ref.size(),&continuation,&retired)||retired!=KAL_JOB_OCCUPIED)return 44;
if(mode=="kill") {
send_line(kal_stdout(),"BUSY");
// Only the parent owns the acknowledgement's write end. Hard death
// also closes this helper's writer gate, allowing the orphan to finish.
(void)read_line(kal_stdin());return 45;
}
if(kal_stream_write(write,"R",1)!=1)return 46;
kal_process_channel_close(write);wait(driver);
if(kal_process_job_retire(job,ref.data(),ref.size(),&continuation,&retired)||retired!=KAL_JOB_EMPTY)return 47;
if(kal_process_job_retire(job,ref.data(),ref.size(),&continuation,&retired)||retired!=KAL_JOB_EMPTY)return 48;
send_line(kal_stdout(),"DONE");kal_process_job_close(job);return 0;
}
void exercise(const fs::path& root,const std::string& mode) {
kal_stream input{},gate{},results{},output{};
if(kal_process_channel(&input,&gate)||kal_process_channel(&results,&output))throw std::runtime_error("actual_channels");
kal_spawn_streams streams{input,output,{}};
auto child=start({mode=="retire-parent"?"launcher":"helper",mode,(root/(mode+".object")).string(),
std::to_string(root_identity.id),std::to_string(root_identity.started_ns)},nullptr,&streams);
kal_process_channel_close(input);kal_process_channel_close(output);
const auto ref=read_line(results);kal_job observed{};
if(mode=="retire-parent") {
require(wait(child)==0,"original_caller_unit_ends_before_retirement");child.h=0;
}
require(kal_process_job_open(ref.data(),ref.size(),&observed)==kal_ok,"original_unit_retained_before_admission");
require(state(observed,ref)==KAL_JOB_OCCUPIED,"unstarted_helper_is_occupied");
auto bad=ref;bad[0]='?';require(state(observed,bad)!=KAL_JOB_EMPTY,"malformed_reference_never_grants_quiet");
if(mode=="before") {
require(kal_process_terminate(child)==kal_ok,"controller_ended_before_ack");
kal_process_channel_close(gate);(void)wait(child,false);
require(!fs::exists(root/(mode+".object")),"no_writer_before_ack");
} else {
send_line(gate,"ACK");
if(mode=="kill") {
require(read_line(results)=="BUSY","retirement_detects_external_driver");
require(kal_process_terminate(child)==kal_ok,"controller_ended_with_descendant");
kal_process_channel_close(gate);(void)wait(child,false);
} else {
require(read_line(results)=="DONE","external_members_quiet_before_completion");
kal_process_channel_close(gate);if(child.h)wait(child);
}
}
await_empty(observed,ref);
require(mode=="before"||fs::exists(root/(mode+".object")),"all_admitted_writer_outputs_observed");
require(state(observed,ref)==KAL_JOB_EMPTY,"held_original_unit_positively_empty");
kal_process_job_close(observed);kal_process_channel_close(results);
kal_job recovered{};const int opened=kal_process_job_open(ref.data(),ref.size(),&recovered);
#ifdef _WIN32
require(opened!=kal_ok,"lost_windows_native_reference_is_not_recreated");
#else
require(opened==kal_ok&&state(recovered,ref)==KAL_JOB_EMPTY,"expired_native_group_is_positively_empty");
#endif
if(opened==kal_ok)kal_process_job_close(recovered);
}
int main(int argc,char** argv) {
try {
image=fs::absolute(argv[0]).string();
if(argc>2&&std::string(argv[1])=="writer") {
char gate=0;const auto n=kal_stream_read(kal_stdin(),&gate,1);
if(n<0)return 50;
std::ofstream out(argv[2]);out<<"actual declared unit descendant\n";return out?0:51;
}
if(argc>2&&std::string(argv[1])=="driver") {return wait(start({"writer",argv[2]}));}
if(argc>5&&std::string(argv[1])=="helper") {
const kal_program_identity anchor{std::stoull(argv[4]),std::stoull(argv[5])};
kal_program_identity expired{};
if(argc>7)expired={std::stoull(argv[6]),std::stoull(argv[7])};
return helper(argv[2],argv[3],anchor,argc>7?&expired:nullptr);
}
if(argc>5&&std::string(argv[1])=="launcher") {
kal_job caller{};if(kal_process_job_enter(&caller))return 52;
kal_stream report{},published{};if(kal_process_channel(&report,&published))return 53;
kal_spawn_streams child_streams{{},{},published};
kal_process self{};kal_program_info own{};own.self_size=sizeof own;
if(kal_process_self(&self)||kal_process_info(self,KAL_PINFO_ID|KAL_PINFO_STARTED,&own))return 55;
(void)start({"helper","launcher",argv[3],argv[4],argv[5],std::to_string(own.id),std::to_string(own.started_ns)},nullptr,&child_streams);
kal_process_channel_close(published);
send_line(kal_stdout(),read_line(report));
kal_process_channel_close(report);return 0;
}
kal_process self{};kal_program_info who{};who.self_size=sizeof who;
require(kal_process_self(&self)==kal_ok
&&kal_process_info(self,KAL_PINFO_ID|KAL_PINFO_STARTED,&who)==kal_ok
&&(who.present&(KAL_PINFO_ID|KAL_PINFO_STARTED))==(KAL_PINFO_ID|KAL_PINFO_STARTED),"full_owner_identity_observed");
root_identity={who.id,who.started_ns};kal_process_close(self);
#ifdef __linux__
require(prctl(PR_SET_CHILD_SUBREAPER,1)==0,"native_harness_reaps_fault_orphans");
#endif
const auto root=fs::temp_directory_path()/("job-api-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
fs::create_directories(root);
try {exercise(root,"normal");exercise(root,"before");exercise(root,"kill");exercise(root,"retire-parent");}
catch(...){fs::remove_all(root);throw;}
fs::remove_all(root);return 0;
}catch(const std::exception& e){std::fprintf(stderr,"job observer API: %s\n",e.what());return 1;}
}
52 changes: 52 additions & 0 deletions .github/checks/retire_boundaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Trace Linux x86-64 setpgid entry/exit; no implementation fault hooks."""
import os,signal,subprocess,ctypes,json,time,sys,platform
if sys.platform != "linux" or platform.machine() != "x86_64":
raise SystemExit("retirement syscall boundary control requires Linux x86-64")
from pathlib import Path
binary=str(Path(sys.argv[1]).resolve())
libc=ctypes.CDLL(None,use_errno=True);libc.ptrace.restype=ctypes.c_long
fields='r15 r14 r13 r12 rbp rbx r11 r10 r9 r8 rax rcx rdx rsi rdi orig_rax rip cs eflags rsp ss fs_base gs_base ds es fs gs'.split()
class Registers(ctypes.Structure):_fields_=[(n,ctypes.c_ulonglong) for n in fields]
def ptrace(op,pid,address=0,data=0):
r=libc.ptrace(op,pid,ctypes.c_void_p(address),data if isinstance(data,ctypes.c_void_p) else ctypes.c_void_p(data))
if r==-1:raise OSError(ctypes.get_errno(),str((op,pid)))
return r
rows=[]
for mode in ['live','wrong-birth','dead','cross-session','before-migration','after-migration']:
anchor=subprocess.Popen(['python3','-c','import time;time.sleep(20)'],preexec_fn=os.setsid if mode=='cross-session' else lambda:os.setpgid(0,0))
helper=None
try:
who=subprocess.check_output([binary,'identity',str(anchor.pid)],text=True).split();assert int(who[0])==anchor.pid
if mode=='wrong-birth':who[1]=str(int(who[1])+1)
if mode=='dead':anchor.kill();anchor.wait()
helper=subprocess.Popen([binary,*who],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
assert helper.stdout.readline().strip()=='READY'
if mode.endswith('migration'):
ptrace(16,helper.pid);os.waitpid(helper.pid,0);ptrace(0x4200,helper.pid,0,1)
helper.stdin.write('A');helper.stdin.flush()
if mode.endswith('migration'):
at_exit=False;hit=False
for step in range(500):
ptrace(24,helper.pid);pid,status=os.waitpid(helper.pid,0)
if not os.WIFSTOPPED(status):raise RuntimeError(('unexpected_exit',status))
regs=Registers();ptrace(12,helper.pid,0,ctypes.cast(ctypes.byref(regs),ctypes.c_void_p))
if regs.orig_rax==109:
if not at_exit:
hit=True
if mode=='before-migration':anchor.kill();anchor.wait();ptrace(17,helper.pid);break
at_exit=True
else:
assert regs.rax==0,regs.rax
anchor.kill();anchor.wait();ptrace(17,helper.pid);break
else:raise RuntimeError('retirement syscall boundary not reached')
assert hit
helper.stdin.close();output=helper.stdout.read();error=helper.stderr.read();code=helper.wait(timeout=5)
expected=2 if mode=='live' else 0
assert code==0 and 'RESULT 0 '+str(expected) in output,(mode,code,output,error)
if mode=='live':assert anchor.poll() is None and 'OLD_UNIT_END ' in output
rows.append({'case':mode,'state':'empty' if expected==2 else 'unknown','exit':code,'anchor_preserved':anchor.poll() is None if mode in ['live','wrong-birth','cross-session'] else None,'actual_native_boundary':mode if mode.endswith('migration') else None})
finally:
if helper and helper.poll() is None:helper.kill();helper.wait()
if anchor.poll() is None:anchor.kill();anchor.wait()
(Path(binary).parent/'retire-boundaries.json').write_text(json.dumps(rows,indent=2)+'\n')
print(json.dumps(rows,indent=2))
23 changes: 23 additions & 0 deletions .github/checks/retire_boundary.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <openkal/process.h>
#include <cstdio>
#include <cstdlib>
#include <string>
int main(int argc,char** argv) {
if(argc==3&&std::string(argv[1])=="identity") {
kal_process p{};kal_program_info i{};i.self_size=sizeof i;
if(kal_process_open_id(std::stoull(argv[2]),&p)||kal_process_info(p,KAL_PINFO_ID|KAL_PINFO_STARTED,&i)||!i.started_ns)return 2;
std::printf("%llu %llu\n",(unsigned long long)i.id,(unsigned long long)i.started_ns);kal_process_close(p);return 0;
}
if(argc!=3)return 3;
kal_program_identity anchor{std::stoull(argv[1]),std::stoull(argv[2])};
kal_job job{};char ref[512];kal_uintptr used=0;
if(kal_process_job_create(&job,ref,sizeof ref,&used))return 4;
std::puts("READY");std::fflush(stdout);char ack=0;if(kal_stream_read(kal_stdin(),&ack,1)!=1)return 5;
kal_u32 state=99;const auto rc=kal_process_job_retire(job,ref,used,&anchor,&state);
std::printf("RESULT %d %u\n",rc,state);std::fflush(stdout);
if(!rc&&state==KAL_JOB_EMPTY) {
const auto ended=kal_process_job_terminate(job);
std::printf("OLD_UNIT_END %d\n",ended);std::fflush(stdout);
}
kal_process_job_close(job);return 0;
}
Loading
Loading