2|(e*Ryx#^ry|wbD1!%){nC{cxkB{)Rl`_Nt;COJn1%ExMz
zZzH7HHKKmoA3xRq=~RB!cj=|FPaT*Kd}mFVv*?U#=+J2i>d~3x@;=~OMu&H!
zlEabJf=?G1b$@)Za@O0vt(uK_JxAm=u3WEq)ZkFH)3jxohD@PM9S>xUZkSG$f74?-
z;kAIa)7d`hBkD&@1^#*}-k1}zfJN5($=Mguo8C`6bGqej#p2M_s{DvSL55vo7SFTYzREc^_ufQsc9Iyx1ZqWPYYi1irid&Y0VP3FIwv&
z{6fO`9Nx4%OOZP|VYYAl*Jh89qt7MO4|ixysZtO;73y55%(L>`j6&tjXIE~heWkN(
zWwSwF%2G26Qw|fkC=1)DO9Do-&M_1>xas-s4Jj?Ewh#F`>FwH8LI+~sgm_Qp6b(MZ
za{0Gt=4Q`);jDiQ%UrV>4U&0z*ZwQEv46v>X*=ipMlFZ>OTM1P7Vj5KG?F^Baqgq#
zE{D6{`r7|E@ORS8&qw%GXI;LsiYs+j=wmltoqu~gCj2XmuuuzI`Z?y&p+`R=!krvi
z{-{?5&$z$muhaUfa?FH~^jHfH7(t!Z68OW)mvD|ivHv%go;7_=u6||=YQbOQx1L?_
zCh6|gttUBt$41Sv-sMoa{`8Fry5|n7JioJ|tLVv^9n%#JeI80Js^RB&9<(Hnv(!u@
zUGO6lOD4ypy;`3eY)xyHzma$U+wsppo9piUjTa9xp0hk{ImKf8kL)qOayG`)+hut$pxVY4?h5>{?YfCA<8EZ^M%M
z)8fY_-h7^(#_kt3Eq4LS#x=Wqk~70>-i7r{{+uZG_-Gfek+~*2OYMn(&7X6sq!dg!
z14SoT^>yq!u}x(6U#8~1%iS;JE^afI^NXy!GA;0LgRJQJ$Z8?R&3@g?MT{Md9r11^
z8+-zG?eGk+x>hN6c(G4;;F2!mwRxHS?Q{QnoZ;vAuuo#87dKN|}c3S+}A~L8I(Z%3`~)DOdH^
z9lDUjY8!eq>g%2(dxO55zm&gf*QeMkf+j(}bq`GT2~S!j&sF*F1iR~!nKm)MjrK-{
z>dLYwtE#a1o5=+%13b!Bo>qUl
zUy3=uJvZhk8l8A#&=g8EsuH%&eVO0Cjma)Q$2RbzP(A-a#foQ9E~(sl
z#SB|}r&KIZ+rC;&@!Phot~So~kD6EC3Qzp<_4Q{S#xha4r%6*K!_T}o^qi=0RV4CD
z#>dSQZ=893t@27==;B^$ZLTQgFDae94NKZru2N*y>|b}`SkPJLZ7vmt^M#LwzG}|=
zpS9!9glnSzX1kp#v2bCUI88t5)0PzZ_If!WAcRJ*2#-`k5o!=Yra`>XUVPG&D-pjsm?83ay}#M=*BlmVN!ih
zdRN`=`I=G``DV@knM+)|Cksh@u6PsA>axgC>6B%f`Kc6<7spP0yj|+_%;wP3C1+=E
zkGJz^Q{s2u7P01F!r@FYgUc%SSpJ;5m9&vP=3d{H&RcsdJL~sFuXWkx{&~l(4B2&-
zq4Qa*CX~M4{N_>q<<2QZ8zvvJ3OIU3|FGiAFuBjO-qaZ?m96-*VqYj%zUtn#H(PX+
zj~UE#_&7somfPVuhME^nhYIH&`ToSkZKm`dhM?|^A8mh6Xir>GQ1$Op$~5C|9rBz!
zsiw=EcKN4rab$2`_370SF}v+`g!Qe*(K}|!->iayFRk$3#eOcZj_cCnS0|nxT_yZn
zYG3dEzADejNwF8j9jsRIsoF^?OwqP{cSgN0#eU~Lw;!EXm1oWSz|3{5r>H?8VS(F(
zCG+{qHh%iQ=v`-@wjIXH
zCg*A?zx?vW*7Dq=B_6L^1h?u}R|*Sg9DBt5Xt$fl(}@Yed93coikJ9TFS#)#YTkt>
zE>ib)yXGuP*~iIZqgABYEOKy~?r-0Yyc)5+O?Nr^thO?wZ9M&xGqZ7#`{Jx6s#9jH
zs0a}j_KG>)75AgVe#T@8{^+HXKYLb2Z%_*iNs{s{f6%d*(`?&&UtO`eOM+IPS)=?&
zV{e|=5v}7EyB1fy&$u~hO4ywVZ_J%@R;^nsYZ@rcUEechQ!`s#ZoACB^Ltuf)Y%qI
z|6g#&I#Aa0PxfX`-jY(do9b5Ro)_k_*3$bEcH1vj6d>AyIQTW6kS%XQA=
zjy5+eRBY_`4aiwlJSinsqRl)x%zb{7=9|*19WIvll+`vCpIe}kac1#U=ft2VYvYTr
zxd_b(jul@ibA96O{MDyQYTtV3b=`kD>2!{VX63yZ#V5P@Hx|q}o+hHUJyd3DbMEy+
zD<#=;_$RxZxuyKOL^MPARUwy}`ybKD_xz`it4visyWW4IK<`^&m+%|^rf%mH3~x;o
z+`N0=z3)?(7zUkho%x&Vk;YCF#kTY*t5#^tZ#6u*ro881Qo!tX4nv=okB1NRs;uWt
z&Fo6`+;Q*t8mEK&9O3gScn*d9*=jZ?w&UP3+s@Jlb5x}Jtz}$)tYdJ{(RjXTN|^h!
zR__Pb0{K2TPwi;zz_XUaVX0iFh^nw#bB;n;
zOLuS0v)>&m)9&W|Xn0hx+SbTpEl2zDvvPZrFPC)`bgYW+KN0hV`GeM_df!VfCyp5L
z-Z>Y@ayUf7A>HEBpVOP2FL5`{K+uOViPPH+A
zX6ilEU^#br&x~h0)+WX0o6F7_Oq-Svp7ccd)tqk`$4ruz1zux1r?mEFiq*>x=Y9X^
z8fzYFX8%@kJ$JVk%l;rWA1|E~mAy^>Tk|DK59p>(bVxknwDSG-j(0ojC)7WEsM1~>
z>lxj=qW&M8v`+!E%fTU}|CeJp7?dEDoE!EVr?b#pARyD=k
z>XfUv(uPS#H+9wvJ4ZI17LZ&V%RTYthyIz+U8+>3IuNw`zy8y=V(sv~pP}4|_w`l6zasBBv;w7&htcHt(N*wK2!VX_XzLXGf!JB&c5~b!g(=$uas{QEEk^!Yy1h{YIpSZrON=O!}4F5h8M{SUvmgoImbOVGVazDENQ9_E+lb&h1Pw
z=_?gqTb?UhwLD5!&he>wq>X9QspH#v0)0hxt8IJhk{`&+$RGIbTBg_UUH{Gr$sJHF
zcYHQ;|AJY`d2xQRw!#W$T7St&>exCjew?@e&e~5F8p3Y=ww|rCcCI^6I^~URa#HWH
zwfFD7{`)fTOU_2^oqr^*a!$GFaCFv{`e>0uS{3HY5A2;Ce1UtV!feSU?q+Z9ewq7&
z^&QK8ha9bt;+?i1esQt%1u#sEI{)~ln((dDr}TR#*)Ry7*uh_QL&04{z2jx6%DfO3
zHSbl&VuaV)OrL1>=G=UY+VsI#f@M}<3-x83h@-BI>4V5exd!xi9=b0(i2U@c?ndZy#&v~>qI)2$Mvu#dl`@CFA_A9SW-MRXs
z0E>#d&8H&`Cz_n6sZHn7b2**(fZyvOZzfxizn*O4%-^D)@+OGMnI@i%Wvo1*@vEz{
z!T0##j&qv(?T%$mHoJJ@?^1`P%7`l;8Xk&GS@Tf7I468shR+fvTju?|FZY~ao9@qb
zcrVXFJt+o81=ZCTHujc8UHd7MSo-8=!8R9}RST!Meh8k+oObHQG~FYGs&ye}zAxIU
zr&6`T>+a615B4hS&+;7Oar<4on^S7JH+Rw_4XLYsvbR5+cW=o49(KR(iOJmw;YZi_
zAN=gLE07~($0CNk>TI9q^*p$ADR0gT{e$23$ntMl;$CSa_2l%5+#P1O&4p{6b_-0t
z^6iFT_FUz-imfXO<|y8{S^87JE-ygZTW0wZr8l{UjF+ol?KF?!TR+Psc(r0u92o5rpq&*ht3z66x)>%&^@pDMs5Dh^?p}tH#3Rcs$R2u
zo1(5(S;3XK+4&FoxBAWqdS&SNAhk2+kGajH%^|mv!<}b1*zH{B)c)y=-ETkV>dcIE
z&Ge_zJ}Gw}n0KHYrbYj{d@k7NKixCiwuC{T@4|yA#oAlu?%FVK!fY|$pr>EYbUo58
zHYs#)ky;(bS+QbC+$!~tha~>+YA<=Y;bPVQ8SgaZZ0`2jHF<75S*0^I(O!7_jJ+(;
zKHi?aKiW4%9k@TC=WDsM!LjQdxBo6HIO3I{oWCw>nv~T=zbw969?Fvs2Wc|N*=>0+
z?d8!0kr}P?&PXb_Ju-Om_U00ssyAtGRLmG(hAx&5JK-Vd#m_w7?h?EEjfcU$rfvn-
zAAMBZdh-C=+dc6dPXAjD?W|$1HhrWXXpk@YD&)v-ql-Q}J0EKcWEb8oOP$}YFLvp|
z($E7l?RFmNt3Jw^JyEqkcVmf*YuU~9wcB;GPIQ%Y1UCI{jj`I{Z(qv0&uPh#<@0x4
zC{Zw~{k77-(c+P1$ue`dQ3hC+@U`CI4U4
zbhdsFOTS&j>HCfEC$HUs&5G0B7H{b@<=FMdePZsenrcm6O^;v4cwgMiTzbk%yFg32
zVQK67^w^y@CWUQHT`MI%n??0s*hh=k1qnBPt>JdKWN!1q*`PhY=2qO9uIY?VbF^=p
z%?kDSSmL~7I-^<9fx0_;YuWDV`#FCPW{u%G+wymVhz_IKYAf}`50~aGl2|$EvqeX(
zn}n=3$9XnuPYczQXNm$A+bxqB(wsbIZ|@A_yP~XRlB}q^&rR&>!rMP|B@+^z
z;qhio{fvOSlXE-@zEArWIbrTco}jb(aRPcu?e9hUJ!B8sHOpmiom`~U+mISJ<&(wx
zJzMtF&Dec=+pMQ6eEU8fK6BJydGZJCDedhW65Vu-W(cfWc-S!VjB8<+%A`fgy(>2x
zTRe(2I&T@YL(sul=CNx^irn?gPzFcA_{Y|rtG3A~SSGr7PCF`iQs`yW?1^(`C^UA?
zus<1`HM`9;)b8S|pIjMRHhoavsq$W_m*?%awYRVSoA_tif(a81EOLbwS$=rS*>H(N
z=lp|p<)>90`<_2|xAGG)p0jx`%AcxmG06(5Y<#n0gUf{t^*uL>_Z0jw3S--=
zl>I01=Uth@B0t+4uJt^8R4Kn<%A~~;+5=g6?w@&MRkW*AK6F=*ba6`KjNDCvw+jp|xZ}FXd;lW)Ac|ZA0
zQ60((k=2J?ZZJIcpCa%&=)v|U$#23Qvp*>9sp}ERK6SO@AmdJD=LvJg8kZcrrxee{
zk>BAq*W+j5>M4drbAL&^P-8fiI4@zDCYy(YpCHRhLp{~y<=0j6opa3ub~ouY9i5an
z?@~nA!|3+DH4Crt3$1?SCh%iZrxwqdB9D1Tjh-CpUi6w-ZTd!YzW~84XS)w<+0Y`i
zE~RLWW#3$GzJlV5E4wY3FIst>-niqKS&)oQZCdITmyDarxhCtEbw6u=Q~B#n>FdDk
zz#ofEK6WmcvZnH=M{+dBZcjg#EZv?;Q|A~PznuaVh1n*5Ixl?e`7$0z)!=6+M0{P$r3L*&&h*XJ)}e)PWc+8(#Qh@z+Y35{FW
z=LE_x@c1Grv|aPD>J$C*ZBAzr1f3VpzLwB&iYkcG304j(g12`TQC%w4i>d&Zvh^IFb0V-O_Gm`hQWQAURRQgZAY0jj@!k&w2UwFJ)
ze5w7b@HSO(!HH*1tZ(va?YCr!yzWrDYe{jlvCt(ZWsw~rJA^}@IkdDc4|?^*Q_CsV
z`+7pJL!zf{_lI-)_L-Z0K3njY{jNyjlw_}#=*q?^;{MGZJQIYS8F%ii?3ZZ}?Caa@
z)w<1%x9XV1_f%7lDFy-z;W_{#d=MxJMR0(+1kI6nDL$4q-CYsq3gNkYp%{`Hk&OLYudEw
zMDT8Y_h*L%IND!)Ub4gOfAXTlbG#Q1{oq|t74%k$`PAlVD*pvIcihkj4Up%V>6_Wk
zQu|U}{cCPX&L!}(y
zjieXP?f>B*c&mSVWBS_KBQ>>Av%VRoMhhRlwdaTPUp>w<`#0YT75pudepI}w;7yWG
zs%>W8$GI&l3f!0?E%So4bNN-z=59?n$^J}eM@w3F)szRm5A_U+MG_ib{q5M8tzt20
z8tWT=$$ud~u4ZM`@qD-^)@gf6>7G)M{iEgEV-*fGyjMGuVVO`Tz;UJi<^2B)|NM^_
zod0L`Yk&FsJ3l%9|13BBe}5grpS(ZMkG{3u-v71!{r#PPC;xk2-}G<2R40AH4s?
z?GS5Px@Ma9tyc|JdbNB1uiyJQGWT)Up^EF@HQy;|D|WM7=e1Bw$hxfOvpe9$G7*z)
zDd8^HIQnHC$I30K-D%_b^W1;y`k2|11Ww(Vy7{>~=QNb^gky>Z)%-^%q-2Hf6m`yF8iAiS1<9l2WdGHo%ds(Eq~kMLK`bVFYOcAs
zd0?4C*|{Mk}U$GFX+a{xl~b
z_q1Czn}nH$(g{oZ38$Y*{=1ZT!lPHlZfm=Wl?z|*Gj-==hrroB=brvM-dXnM*0zHs
z8|zLB@SpPbS9PCiesz+uzsbQx`z;Hv{ah#1-IUk$CM50Wo^?BSS#P*j_V+TgUD(lW
zO54A!xPDAH|3>BITM3dDZhCHE^DY0$nO2-p&_A?Uqn|@>PWI7B5*nF?dM9VAEe^7n
zdt=)@u`7~dJIzctaU8Cg!Nl=+)kYl?%iC&qI^+*>yM%Zxako;jVg6s&f4StS`16xz
zR~MA&y%ueKtE;^)V$q!i_ZnvD#|d4!uB2-GYHtqfQR^7TWt|61%reDqW{(0(pZICV8|t3kq$ph_Yu0x?tL-$0PqyV0E8!Dr7pFS9DSekyxM=RZ
zzHm=*%Ja;5WyPD92R?T1`}|ZMm!G
zrgt8{`Rd)w!^gc=3f27D@R|R@v*=&D9PdV7`MH6oSo>Y=T$ZFmH{&@|nH$=|BF*Gy
ztW;=n6WGnR#^S8nif7mIn4KP%iO;p*yJ&sa@HfYN!8cx=MxpxpTjg1+7N_`~6FkEi
z6BjRIdA{`xi&R_n>B23=_Q$jupH*Js`m6AN-}-g`Pvo!Mm)vYw>~9!p9V04zK-Bus
z*Lk(m`R_g5K281WHilc_4!4)>U%zhl<2?rK+Bt^5^8Vy73jbReY4GPwyue;xxwS`4
zbWenAa!}{GRP^=Nh1etC*rwcNd4I?BSFM6k$bZ?}iPJ70c>Jg!&6B&(|AJm=hP*-+e|2pq8qgR|h{ux)7oS*-1
z(px^Iqvz-UGkeljBy{WM^8W0C{R=K^npwT-W2A@2!%OWaj`lTp-@l!*>cO^<58o7o
zPH(#MD{!t}{@!Q)<#|;}p6~A_7t3%xxwUagC5Ly`x-jh>mu+{8Z&77)XSg4ptUT}f
zH5UfIa-$piskc4*thj_v_?2(m^=x1IUXf@Uz8gVbBg&RJ|GB=-_Icyv%el*Wtx9bU
zRQPVbw(nZ8_J@|LiPlB`d%p`UTl+!vlym8=tEY_8&l_4bo3easzFWV;T_MrGxZ5J%
zv31S*mQ~6ps_mZs-^qL|b;oxe@qd}qZEwzaZRQ(xMviZauJ5szAD*t&a4SC3F>Ucd
zC$CJ!ugVw7#S|-4`Bp8sms=fBo^+JOKzRRM9t)LIcD_#n`V5a>`Ts_Ern7C&!pKFp
zgpEq2EIh13S&bQvSFqZ9M4x0i))NpoMZ%?~AoF3;o&^`OZ027Oecs?H+;)zSYjR)R
z{uzF+v^{0&8kV)Vp6hlnx!vY`FnrPa{pJqf5?BbZ+CBBf9%hB^FM#*|0(%)`uETM
zCUrGI|DHcxJ$-%uuD|yD$N%-mCr_#6+_<4^*AZ7~Jw~-pk6P~wb2pv*UgkDu*^jik
z<4wWFwGN)goXRpMJrj2jn0V&EVIx1umDx`NHa`-0Q&RVDS@n?)mddq@dO5tmstPWe
zy=Ov>$I6bGx6ZLP_)6vQKGI=2?JlxsquCcL))!y<*O)b&h@4!Mw3CPJ=SR_vSx>il
zw|-eFa5Z|R;&t`)+h5P#Eq&_wLl2LZN8IZVY-f7-yyDKqd4_$3J;^eUC5~GhLuJ-{
R&+V^g$ga=!WSGFn0085vpril*
literal 0
HcmV?d00001
diff --git a/docs/runtime-v2/design/revision-notes.md b/docs/runtime-v2/design/revision-notes.md
new file mode 100644
index 0000000..46179eb
--- /dev/null
+++ b/docs/runtime-v2/design/revision-notes.md
@@ -0,0 +1,33 @@
+# klbr plan v2 — what changed
+
+Date: September 6, 2026. Design revision only; no klbr runtime implementation or failure testing was performed.
+
+## Read this version
+
+`implementation-plan.md` is the integrated full plan. `hooks-and-policy.md` defines the detailed extension contract. `behavior-contracts.md` preserves the original source evidence unchanged.
+
+The earlier plan suggested deferring a policy boundary and mainly making skills/settings editable. That is superseded: Python policy hooks are now a first-class requirement implemented before the replacement turn loop. Rust is a deliberate foundation for typed resource ownership, reliability engineering and efficient always-on execution, not merely a way to reuse an existing codebase.
+
+## Architecture
+
+One Rust daemon owns persistence, provider/transport execution, state transitions, resource limits, receipts, scheduling mechanisms and recovery. Python owns ordinary policies, skills and guidance. One persistent workbench per active agent session provides interactive computation. A supervised persistent hook-worker role evaluates published callbacks independently of that workbench, using the same Python runtime and transport. It is not another agent orchestrator.
+
+The host exposes single-owner typed decision slots, ordered representation transforms and asynchronous post-commit observers. Meaningful hook families cover input/attention, turns, context/history, model planning, execution/results, explicit delivery, LCM, jobs/children and runtime/revisions. Rust validates each proposed change against domain invariants. The public hook vocabulary is broad; arbitrary internal functions and database writes are not public mutation points.
+
+Default attention/context/model/social policies are ordinary editable Python. Durable hooks are published module entrypoints, not cell-local closures. The workbench can discover, edit, test, bind and activate them. Ordinary callable skills still need no hook registration.
+
+## The important failure boundary
+
+A Python cell awaiting `discord.send` must not wait for a review callback queued behind itself in the same workbench. The independent hook worker solves that placement problem. Host invocation also releases transactions/mutation locks, permits its bounded read RPC and uses Rust-owned deadlines; otherwise moving the callback alone would not solve the deadlock.
+
+Optional hook failure may produce documented degradation. Required send/execution review failure holds the specific operation. Durable ingress and operator cancellation/rollback remain available even when every Python process is unhealthy. Post-commit observers can retry without duplicating atomically recorded host commands; arbitrary direct Python effects still do not gain an exactly-once guarantee.
+
+## Self-editability improvement
+
+A single authoritative activation manifest references instructions/settings, skills/environment and hooks. A compatible hook-only edit activates a new worker without clearing workbench variables. Shared dependency changes replace all affected roles. In-flight attempts/effects remain epoch-pinned; observer activation has explicit cursors and does not replay old events merely because code changed. Broken candidate hooks cannot veto their own repair.
+
+## Plan delta
+
+Slice 2 now defines hook contracts and invocation state. New slice 3a implements the worker, defaults and dispatcher. Slice 4 uses hooks in the real turn loop rather than deferring them. Subsequent domain slices add their own hooks. Slice 9 adds component activation, dry runs and state/replay compatibility. Cutover adds hook latency, queue/backlog and blocked-worker tests.
+
+The primary success case is now: improve attention, context/model policy, delivery formatting or result handling entirely in Python, activate it without Cargo, preserve a compatible live workbench, and keep accepted work and honest receipts through a failed next edit.
diff --git a/docs/runtime-v2/evidence/checks-python.log b/docs/runtime-v2/evidence/checks-python.log
new file mode 100644
index 0000000..5c06766
--- /dev/null
+++ b/docs/runtime-v2/evidence/checks-python.log
@@ -0,0 +1,53 @@
+test_duplicate_keys_and_nonfinite_values_rejected (test_contracts.FrameTests.test_duplicate_keys_and_nonfinite_values_rejected) ... ok
+test_length_prefix_roundtrip_unicode (test_contracts.FrameTests.test_length_prefix_roundtrip_unicode) ... ok
+test_oversize_rejected_before_payload_read (test_contracts.FrameTests.test_oversize_rejected_before_payload_read) ... ok
+test_shared_golden_frames (test_contracts.FrameTests.test_shared_golden_frames) ... ok
+test_truncated_frames_fail (test_contracts.FrameTests.test_truncated_frames_fail) ... ok
+test_burst_has_one_inbox_row_per_input (test_contracts.SchemaTests.test_burst_has_one_inbox_row_per_input) ... ok
+test_confirmed_effect_requires_receipt (test_contracts.SchemaTests.test_confirmed_effect_requires_receipt) ... ok
+test_dedup_is_by_request_not_message_text (test_contracts.SchemaTests.test_dedup_is_by_request_not_message_text) ... ok
+test_foreign_keys_prevent_orphaned_inbox (test_contracts.SchemaTests.test_foreign_keys_prevent_orphaned_inbox) ... ok
+test_impossible_ready_with_deadline_is_rejected (test_contracts.SchemaTests.test_impossible_ready_with_deadline_is_rejected) ... ok
+test_originals_cannot_be_updated_or_deleted (test_contracts.SchemaTests.test_originals_cannot_be_updated_or_deleted) ... ok
+test_publication_transaction_rolls_back_receipt_and_event_together (test_contracts.SchemaTests.test_publication_transaction_rolls_back_receipt_and_event_together) ... ok
+test_schema_identity (test_contracts.SchemaTests.test_schema_identity) ... ok
+test_source_identity_is_unique_per_session (test_contracts.SchemaTests.test_source_identity_is_unique_per_session) ... ok
+test_unfinished_execution_unique_with_independent_sessions (test_contracts.SchemaTests.test_unfinished_execution_unique_with_independent_sessions) ... ok
+test_yield_does_not_open_a_second_foreground_slot (test_contracts.SchemaTests.test_yield_does_not_open_a_second_foreground_slot) ... ok
+test_behavior_identity_changes_with_source (test_contracts.ValueTests.test_behavior_identity_changes_with_source) ... ok
+test_behavior_root_symlinks_rejected (test_contracts.ValueTests.test_behavior_root_symlinks_rejected) ... ok
+test_behavior_symlinks_rejected (test_contracts.ValueTests.test_behavior_symlinks_rejected) ... ok
+test_decision_shapes_are_explicit (test_contracts.ValueTests.test_decision_shapes_are_explicit) ... ok
+test_oversized_behavior_file_rejected (test_contracts.ValueTests.test_oversized_behavior_file_rejected) ... ok
+test_shared_behavior_digest (test_contracts.ValueTests.test_shared_behavior_digest) ... ok
+test_bad_candidate_does_not_affect_existing_worker (test_processes.PolicyProcessTests.test_bad_candidate_does_not_affect_existing_worker) ... ok
+test_blocked_policy_does_not_block_workbench_and_can_be_killed (test_processes.PolicyProcessTests.test_blocked_policy_does_not_block_workbench_and_can_be_killed) ... ok
+test_editable_defaults_match_attention_contract (test_processes.PolicyProcessTests.test_editable_defaults_match_attention_contract) ... ok
+test_hook_cannot_use_workbench_effect_sdk (test_processes.PolicyProcessTests.test_hook_cannot_use_workbench_effect_sdk) ... ok
+test_hook_worker_available_while_workbench_cpu_blocked (test_processes.PolicyProcessTests.test_hook_worker_available_while_workbench_cpu_blocked) ... ok
+test_replacing_hook_worker_preserves_workbench_heap (test_processes.PolicyProcessTests.test_replacing_hook_worker_preserves_workbench_heap) ... ok
+test_send_waits_for_independent_hook_without_circular_wait (test_processes.PolicyProcessTests.test_send_waits_for_independent_hook_without_circular_wait) ... ok
+test_wrong_hook_return_is_failure_not_approval (test_processes.PolicyProcessTests.test_wrong_hook_return_is_failure_not_approval) ... ok
+test_background_effects_expire_with_originating_cell (test_processes.WorkbenchTests.test_background_effects_expire_with_originating_cell) ... ok
+test_background_output_is_not_reattributed_to_next_cell (test_processes.WorkbenchTests.test_background_output_is_not_reattributed_to_next_cell) ... ok
+test_catching_yield_does_not_restore_effect_scope (test_processes.WorkbenchTests.test_catching_yield_does_not_restore_effect_scope) ... ok
+test_cooperative_interrupt_preserves_globals (test_processes.WorkbenchTests.test_cooperative_interrupt_preserves_globals) ... ok
+test_explicit_send_roundtrip (test_processes.WorkbenchTests.test_explicit_send_roundtrip) ... ok
+test_future_flags_survive_cells (test_processes.WorkbenchTests.test_future_flags_survive_cells) ... ok
+test_host_error_is_inspectable_and_kernel_recovers (test_processes.WorkbenchTests.test_host_error_is_inspectable_and_kernel_recovers) ... ok
+test_invalid_wait_values_never_reach_host (test_processes.WorkbenchTests.test_invalid_wait_values_never_reach_host) ... ok
+test_late_reply_after_interrupt_is_discarded (test_processes.WorkbenchTests.test_late_reply_after_interrupt_is_discarded) ... ok
+test_many_alternating_writes_bound_record_count (test_processes.WorkbenchTests.test_many_alternating_writes_bound_record_count) ... ok
+test_native_exit_does_not_claim_completion (test_processes.WorkbenchTests.test_native_exit_does_not_claim_completion) ... ok
+test_output_is_bounded_and_truncation_explicit (test_processes.WorkbenchTests.test_output_is_bounded_and_truncation_explicit) ... ok
+test_persistent_namespace_and_last_expression (test_processes.WorkbenchTests.test_persistent_namespace_and_last_expression) ... ok
+test_print_and_native_stdout_cannot_send_or_forge_control (test_processes.WorkbenchTests.test_print_and_native_stdout_cannot_send_or_forge_control) ... ok
+test_stale_generation_is_connection_fatal (test_processes.WorkbenchTests.test_stale_generation_is_connection_fatal) ... ok
+test_syntax_and_runtime_errors_do_not_destroy_namespace (test_processes.WorkbenchTests.test_syntax_and_runtime_errors_do_not_destroy_namespace) ... ok
+test_top_level_await_and_background_computation (test_processes.WorkbenchTests.test_top_level_await_and_background_computation) ... ok
+test_wait_is_terminal_not_a_sleep (test_processes.WorkbenchTests.test_wait_is_terminal_not_a_sleep) ... ok
+
+----------------------------------------------------------------------
+Ran 48 tests in 16.187s
+
+OK
diff --git a/docs/runtime-v2/evidence/checks.json b/docs/runtime-v2/evidence/checks.json
new file mode 100644
index 0000000..57a47b2
--- /dev/null
+++ b/docs/runtime-v2/evidence/checks.json
@@ -0,0 +1,34 @@
+{
+ "created_at_utc": "2026-09-05T21:52:17.203334+00:00",
+ "python": "3.13.5 (main, Jul 15 2026, 20:25:40) [GCC 14.2.0]",
+ "platform": "Linux-6.18.35-x86_64-with-glibc2.41",
+ "python_sqlite": "3.46.1",
+ "python_sqlite_scope": "serial in-memory migration tests only; not runtime WAL qualification",
+ "cargo_available": false,
+ "rustc_available": false,
+ "rust_checks_requested": false,
+ "steps": [
+ {
+ "name": "python",
+ "argv": [
+ "/opt/pyvenv/bin/python",
+ "-B",
+ "-m",
+ "unittest",
+ "discover",
+ "-s",
+ "python/klbr-runtime/tests",
+ "-v"
+ ],
+ "exit_code": 0,
+ "elapsed_seconds": 16.787,
+ "log": "checks-python.log"
+ }
+ ],
+ "manifests": {
+ "status": "passed",
+ "meaning": "manifest/lock-reference consistency only; Cargo resolver not run"
+ },
+ "rust_status": "not run; pass --rust on a machine with Cargo/rustc",
+ "requested_checks_passed": true
+}
diff --git a/docs/runtime-v2/evidence/full-checks-python.log b/docs/runtime-v2/evidence/full-checks-python.log
new file mode 100644
index 0000000..98b58ff
--- /dev/null
+++ b/docs/runtime-v2/evidence/full-checks-python.log
@@ -0,0 +1,53 @@
+test_duplicate_keys_and_nonfinite_values_rejected (test_contracts.FrameTests.test_duplicate_keys_and_nonfinite_values_rejected) ... ok
+test_length_prefix_roundtrip_unicode (test_contracts.FrameTests.test_length_prefix_roundtrip_unicode) ... ok
+test_oversize_rejected_before_payload_read (test_contracts.FrameTests.test_oversize_rejected_before_payload_read) ... ok
+test_shared_golden_frames (test_contracts.FrameTests.test_shared_golden_frames) ... ok
+test_truncated_frames_fail (test_contracts.FrameTests.test_truncated_frames_fail) ... ok
+test_burst_has_one_inbox_row_per_input (test_contracts.SchemaTests.test_burst_has_one_inbox_row_per_input) ... ok
+test_confirmed_effect_requires_receipt (test_contracts.SchemaTests.test_confirmed_effect_requires_receipt) ... ok
+test_dedup_is_by_request_not_message_text (test_contracts.SchemaTests.test_dedup_is_by_request_not_message_text) ... ok
+test_foreign_keys_prevent_orphaned_inbox (test_contracts.SchemaTests.test_foreign_keys_prevent_orphaned_inbox) ... ok
+test_impossible_ready_with_deadline_is_rejected (test_contracts.SchemaTests.test_impossible_ready_with_deadline_is_rejected) ... ok
+test_originals_cannot_be_updated_or_deleted (test_contracts.SchemaTests.test_originals_cannot_be_updated_or_deleted) ... ok
+test_publication_transaction_rolls_back_receipt_and_event_together (test_contracts.SchemaTests.test_publication_transaction_rolls_back_receipt_and_event_together) ... ok
+test_schema_identity (test_contracts.SchemaTests.test_schema_identity) ... ok
+test_source_identity_is_unique_per_session (test_contracts.SchemaTests.test_source_identity_is_unique_per_session) ... ok
+test_unfinished_execution_unique_with_independent_sessions (test_contracts.SchemaTests.test_unfinished_execution_unique_with_independent_sessions) ... ok
+test_yield_does_not_open_a_second_foreground_slot (test_contracts.SchemaTests.test_yield_does_not_open_a_second_foreground_slot) ... ok
+test_behavior_identity_changes_with_source (test_contracts.ValueTests.test_behavior_identity_changes_with_source) ... ok
+test_behavior_root_symlinks_rejected (test_contracts.ValueTests.test_behavior_root_symlinks_rejected) ... ok
+test_behavior_symlinks_rejected (test_contracts.ValueTests.test_behavior_symlinks_rejected) ... ok
+test_decision_shapes_are_explicit (test_contracts.ValueTests.test_decision_shapes_are_explicit) ... ok
+test_oversized_behavior_file_rejected (test_contracts.ValueTests.test_oversized_behavior_file_rejected) ... ok
+test_shared_behavior_digest (test_contracts.ValueTests.test_shared_behavior_digest) ... ok
+test_bad_candidate_does_not_affect_existing_worker (test_processes.PolicyProcessTests.test_bad_candidate_does_not_affect_existing_worker) ... ok
+test_blocked_policy_does_not_block_workbench_and_can_be_killed (test_processes.PolicyProcessTests.test_blocked_policy_does_not_block_workbench_and_can_be_killed) ... ok
+test_editable_defaults_match_attention_contract (test_processes.PolicyProcessTests.test_editable_defaults_match_attention_contract) ... ok
+test_hook_cannot_use_workbench_effect_sdk (test_processes.PolicyProcessTests.test_hook_cannot_use_workbench_effect_sdk) ... ok
+test_hook_worker_available_while_workbench_cpu_blocked (test_processes.PolicyProcessTests.test_hook_worker_available_while_workbench_cpu_blocked) ... ok
+test_replacing_hook_worker_preserves_workbench_heap (test_processes.PolicyProcessTests.test_replacing_hook_worker_preserves_workbench_heap) ... ok
+test_send_waits_for_independent_hook_without_circular_wait (test_processes.PolicyProcessTests.test_send_waits_for_independent_hook_without_circular_wait) ... ok
+test_wrong_hook_return_is_failure_not_approval (test_processes.PolicyProcessTests.test_wrong_hook_return_is_failure_not_approval) ... ok
+test_background_effects_expire_with_originating_cell (test_processes.WorkbenchTests.test_background_effects_expire_with_originating_cell) ... ok
+test_background_output_is_not_reattributed_to_next_cell (test_processes.WorkbenchTests.test_background_output_is_not_reattributed_to_next_cell) ... ok
+test_catching_yield_does_not_restore_effect_scope (test_processes.WorkbenchTests.test_catching_yield_does_not_restore_effect_scope) ... ok
+test_cooperative_interrupt_preserves_globals (test_processes.WorkbenchTests.test_cooperative_interrupt_preserves_globals) ... ok
+test_explicit_send_roundtrip (test_processes.WorkbenchTests.test_explicit_send_roundtrip) ... ok
+test_future_flags_survive_cells (test_processes.WorkbenchTests.test_future_flags_survive_cells) ... ok
+test_host_error_is_inspectable_and_kernel_recovers (test_processes.WorkbenchTests.test_host_error_is_inspectable_and_kernel_recovers) ... ok
+test_invalid_wait_values_never_reach_host (test_processes.WorkbenchTests.test_invalid_wait_values_never_reach_host) ... ok
+test_late_reply_after_interrupt_is_discarded (test_processes.WorkbenchTests.test_late_reply_after_interrupt_is_discarded) ... ok
+test_many_alternating_writes_bound_record_count (test_processes.WorkbenchTests.test_many_alternating_writes_bound_record_count) ... ok
+test_native_exit_does_not_claim_completion (test_processes.WorkbenchTests.test_native_exit_does_not_claim_completion) ... ok
+test_output_is_bounded_and_truncation_explicit (test_processes.WorkbenchTests.test_output_is_bounded_and_truncation_explicit) ... ok
+test_persistent_namespace_and_last_expression (test_processes.WorkbenchTests.test_persistent_namespace_and_last_expression) ... ok
+test_print_and_native_stdout_cannot_send_or_forge_control (test_processes.WorkbenchTests.test_print_and_native_stdout_cannot_send_or_forge_control) ... ok
+test_stale_generation_is_connection_fatal (test_processes.WorkbenchTests.test_stale_generation_is_connection_fatal) ... ok
+test_syntax_and_runtime_errors_do_not_destroy_namespace (test_processes.WorkbenchTests.test_syntax_and_runtime_errors_do_not_destroy_namespace) ... ok
+test_top_level_await_and_background_computation (test_processes.WorkbenchTests.test_top_level_await_and_background_computation) ... ok
+test_wait_is_terminal_not_a_sleep (test_processes.WorkbenchTests.test_wait_is_terminal_not_a_sleep) ... ok
+
+----------------------------------------------------------------------
+Ran 48 tests in 15.714s
+
+OK
diff --git a/docs/runtime-v2/evidence/full-checks.json b/docs/runtime-v2/evidence/full-checks.json
new file mode 100644
index 0000000..1224202
--- /dev/null
+++ b/docs/runtime-v2/evidence/full-checks.json
@@ -0,0 +1,38 @@
+{
+ "created_at_utc": "2026-09-05T21:49:44.459865+00:00",
+ "python": "3.13.5 (main, Jul 15 2026, 20:25:40) [GCC 14.2.0]",
+ "platform": "Linux-6.18.35-x86_64-with-glibc2.41",
+ "python_sqlite": "3.46.1",
+ "python_sqlite_scope": "serial in-memory migration tests only; not runtime WAL qualification",
+ "cargo_available": false,
+ "rustc_available": false,
+ "rust_checks_requested": true,
+ "steps": [
+ {
+ "name": "python",
+ "argv": [
+ "/opt/pyvenv/bin/python",
+ "-B",
+ "-m",
+ "unittest",
+ "discover",
+ "-s",
+ "python/klbr-runtime/tests",
+ "-v"
+ ],
+ "exit_code": 0,
+ "elapsed_seconds": 16.332,
+ "log": "full-checks-python.log"
+ },
+ {
+ "name": "rust",
+ "exit_code": 127,
+ "reason": "Cargo/rustc unavailable; no Rust tests or integration checks ran"
+ }
+ ],
+ "manifests": {
+ "status": "passed",
+ "meaning": "manifest/lock-reference consistency only; Cargo resolver not run"
+ },
+ "requested_checks_passed": false
+}
diff --git a/klbr-runtime/AGENTS.md b/klbr-runtime/AGENTS.md
new file mode 100644
index 0000000..be05c0c
--- /dev/null
+++ b/klbr-runtime/AGENTS.md
@@ -0,0 +1,15 @@
+# New runtime slice
+
+Read `../RUNTIME-V2.md`, `../docs/runtime-v2/STATUS.md` and `HANDOFF.md` first.
+This crate is an additive replacement boundary, not yet the live daemon.
+
+The producer had no Rust toolchain. Compile, format and run the real Rust tests before
+expanding the design; do not cite the Python peer tests as Rust integration coverage.
+Keep core invariants in Rust and ordinary policies in editable Python. No SQL transaction
+or domain mutation lock across a hook. No automatic replay of arbitrary cells. Preserve
+scratchpad/workbench-output versus explicit speech. A missing required guard holds its effect.
+Hook-only replacement pins in-flight cells and must not wipe a compatible workbench namespace.
+
+Add hooks only with their real domain, typed input/result and failure contract. Do not add
+an empty facade for every future slot. The Python test peer is not production orchestration.
+Do not migrate or replace the old memory DB until a deterministic importer/cutover is tested.
diff --git a/klbr-runtime/Cargo.toml b/klbr-runtime/Cargo.toml
new file mode 100644
index 0000000..06e680a
--- /dev/null
+++ b/klbr-runtime/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "klbr-runtime"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.89"
+publish = false
+
+[dependencies]
+anyhow.workspace = true
+rusqlite.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+sha2.workspace = true
+tempfile.workspace = true
+tokio.workspace = true
+uuid.workspace = true
diff --git a/klbr-runtime/examples/workbench.rs b/klbr-runtime/examples/workbench.rs
new file mode 100644
index 0000000..cb7e73f
--- /dev/null
+++ b/klbr-runtime/examples/workbench.rs
@@ -0,0 +1,74 @@
+//! Run from the workspace root: cargo run -p klbr-runtime --example workbench
+//! No providers, credentials or real Discord effects are used.
+use anyhow::Result;
+use klbr_runtime::{
+ behavior::Release, protocol::SessionId, session::SessionRuntime, store::Store,
+ worker::WorkerConfig,
+};
+use std::{fs, path::PathBuf, time::Duration};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .unwrap()
+ .to_path_buf();
+ let state = tempfile::tempdir()?;
+ let release = Release::publish(&root.join("behavior"), &state.path().join("releases"))?;
+ let store = Store::open(state.path().join("runtime.sqlite"))?;
+ let session = SessionRuntime::start(
+ store,
+ SessionId::fresh(),
+ WorkerConfig::source_tree(&root),
+ release,
+ )
+ .await?;
+ session
+ .store
+ .scratchpad(
+ &session.id,
+ "i am preparing a small experiment, not sending this text".into(),
+ )
+ .await?;
+ let first = session
+ .execute(
+ "answer = 6 * 7\nprint('workbench output, not speech')\nanswer".into(),
+ Duration::from_secs(10),
+ )
+ .await?;
+ let second = session.execute("from klbr import local, hooks\nreceipt = await local.send(f'the answer is {answer}')\nreceipt".into(),Duration::from_secs(10)).await?;
+ // A hook-only source change; the variable `answer` stays in the workbench.
+ let draft = state.path().join("draft");
+ fs::create_dir_all(draft.join("klbr_hooks"))?;
+ fs::copy(
+ root.join("behavior/manifest.json"),
+ draft.join("manifest.json"),
+ )?;
+ fs::copy(
+ root.join("behavior/klbr_hooks/__init__.py"),
+ draft.join("klbr_hooks/__init__.py"),
+ )?;
+ let mut source = fs::read_to_string(root.join("behavior/klbr_hooks/defaults.py"))?;
+ source.push_str("\nasync def delivery(request, context):\n return DeliveryDecision.hold('testing a new guard')\n");
+ fs::write(draft.join("klbr_hooks/defaults.py"), source)?;
+ let candidate = Release::publish(&draft, &state.path().join("releases"))?;
+ session
+ .activate_hooks(session.hook_epoch().await, candidate)
+ .await?;
+ let held = session
+ .execute(
+ "assert answer == 42\n(await local.send('this stays held')).status".into(),
+ Duration::from_secs(10),
+ )
+ .await?;
+ let third = session.execute("from klbr import runtime\nawait runtime.wait(reason='waiting for an input')\nraise RuntimeError('must not run')".into(),Duration::from_secs(10)).await?;
+ for report in [first, second, held, third] {
+ println!("{}", serde_json::to_string_pretty(&report)?);
+ }
+ println!(
+ "durable history:\n{}",
+ serde_json::to_string_pretty(&session.store.history(&session.id, 0, 100).await?)?
+ );
+ session.shutdown().await;
+ Ok(())
+}
diff --git a/klbr-runtime/migrations/001_runtime.sql b/klbr-runtime/migrations/001_runtime.sql
new file mode 100644
index 0000000..5e9d527
--- /dev/null
+++ b/klbr-runtime/migrations/001_runtime.sql
@@ -0,0 +1,77 @@
+-- New runtime database only. Never apply this migration to the legacy memory DB.
+BEGIN IMMEDIATE;
+PRAGMA application_id = 1263288882; -- KLB2
+PRAGMA user_version = 1;
+
+CREATE TABLE releases (
+ id TEXT PRIMARY KEY,
+ path TEXT NOT NULL
+) STRICT;
+
+CREATE TABLE sessions (
+ id TEXT PRIMARY KEY,
+ state TEXT NOT NULL DEFAULT 'ready' CHECK(state IN ('ready','waiting')),
+ wake_at_ms INTEGER,
+ hook_revision TEXT REFERENCES releases(id),
+ hook_epoch INTEGER NOT NULL DEFAULT 0 CHECK(hook_epoch >= 0),
+ CHECK(state = 'waiting' OR wake_at_ms IS NULL)
+) STRICT;
+
+CREATE TABLE events (
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
+ session_id TEXT NOT NULL REFERENCES sessions(id),
+ kind TEXT NOT NULL CHECK(kind IN (
+ 'input','scratchpad','execution.started','execution.completed',
+ 'local.sent','delivery.held','kernel.reset','session.waited',
+ 'session.woke','hooks.activated'
+ )),
+ source_key TEXT,
+ payload TEXT NOT NULL CHECK(json_valid(payload)),
+ created_ms INTEGER NOT NULL CHECK(created_ms >= 0),
+ UNIQUE(session_id, source_key)
+) STRICT;
+CREATE INDEX events_session_order ON events(session_id, seq);
+CREATE TRIGGER events_no_update BEFORE UPDATE ON events BEGIN
+ SELECT RAISE(ABORT, 'historical events are immutable');
+END;
+CREATE TRIGGER events_no_delete BEFORE DELETE ON events BEGIN
+ SELECT RAISE(ABORT, 'historical events are immutable');
+END;
+
+CREATE TABLE inbox (
+ event_id INTEGER PRIMARY KEY REFERENCES events(seq),
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','consumed'))
+) STRICT;
+
+CREATE TABLE executions (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL REFERENCES sessions(id),
+ generation TEXT NOT NULL,
+ hook_revision TEXT NOT NULL REFERENCES releases(id),
+ state TEXT NOT NULL CHECK(state IN ('running','ok','error','yielded','interrupted')),
+ started_event INTEGER NOT NULL UNIQUE REFERENCES events(seq),
+ completed_event INTEGER UNIQUE REFERENCES events(seq)
+) STRICT;
+CREATE UNIQUE INDEX one_running_execution ON executions(session_id) WHERE completed_event IS NULL;
+
+CREATE TABLE effects (
+ id TEXT PRIMARY KEY,
+ execution_id TEXT NOT NULL REFERENCES executions(id),
+ request_id TEXT NOT NULL,
+ text TEXT NOT NULL,
+ status TEXT NOT NULL CHECK(status IN ('proposed','confirmed','held')),
+ event_id INTEGER UNIQUE REFERENCES events(seq),
+ reason TEXT,
+ UNIQUE(execution_id,request_id),
+ CHECK((status = 'confirmed' AND event_id IS NOT NULL AND reason IS NULL)
+ OR (status = 'held' AND event_id IS NULL AND reason IS NOT NULL)
+ OR (status = 'proposed' AND event_id IS NULL AND reason IS NULL))
+) STRICT;
+
+CREATE TABLE hook_traces (
+ effect_id TEXT PRIMARY KEY REFERENCES effects(id),
+ revision TEXT NOT NULL REFERENCES releases(id),
+ decision TEXT NOT NULL CHECK(json_valid(decision)),
+ fault TEXT
+) STRICT;
+COMMIT;
diff --git a/klbr-runtime/src/behavior.rs b/klbr-runtime/src/behavior.rs
new file mode 100644
index 0000000..59cbd81
--- /dev/null
+++ b/klbr-runtime/src/behavior.rs
@@ -0,0 +1,164 @@
+//! Content-addressed behavior snapshots, separate from activation authority.
+use crate::protocol::HookSlot;
+use anyhow::{ensure, Context, Result};
+use serde::Deserialize;
+use sha2::{Digest, Sha256};
+use std::{
+ collections::BTreeMap,
+ fs,
+ io::Write,
+ path::{Path, PathBuf},
+};
+
+#[derive(Clone, Debug)]
+pub struct Release {
+ pub id: String,
+ pub path: PathBuf,
+ pub hooks: BTreeMap,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Manifest {
+ version: u16,
+ hooks: BTreeMap,
+}
+
+fn collect(
+ root: &Path,
+ directory: &Path,
+ entries: &mut Vec<(String, Vec)>,
+ bytes: &mut usize,
+) -> Result<()> {
+ for entry in fs::read_dir(directory)? {
+ let entry = entry?;
+ let metadata = entry.file_type()?;
+ ensure!(
+ !metadata.is_symlink(),
+ "behavior snapshots cannot contain symlinks"
+ );
+ if metadata.is_dir() {
+ collect(root, &entry.path(), entries, bytes)?;
+ continue;
+ }
+ ensure!(metadata.is_file(), "behavior entry must be a regular file");
+ let path = entry.path();
+ ensure!(
+ matches!(
+ path.extension().and_then(|s| s.to_str()),
+ Some("py" | "json" | "md")
+ ),
+ "unsupported behavior file"
+ );
+ ensure!(
+ entry.metadata()?.len() <= 2_097_152,
+ "behavior file too large"
+ );
+ let content = fs::read(&path)?;
+ *bytes += content.len();
+ ensure!(
+ *bytes <= 2_097_152 && entries.len() < 128,
+ "behavior snapshot exceeds limits"
+ );
+ let name = path
+ .strip_prefix(root)?
+ .to_str()
+ .context("non-UTF8 behavior path")?
+ .replace('\\', "/");
+ entries.push((name, content));
+ }
+ Ok(())
+}
+
+fn read(root: &Path) -> Result<(String, Vec<(String, Vec)>, Manifest)> {
+ ensure!(
+ !fs::symlink_metadata(root)?.file_type().is_symlink(),
+ "behavior root cannot be a symlink"
+ );
+ let mut entries = Vec::new();
+ let mut total = 0;
+ collect(root, root, &mut entries, &mut total)?;
+ entries.sort_by(|a, b| a.0.cmp(&b.0));
+ let mut digest = Sha256::new();
+ for (name, content) in &entries {
+ digest.update((name.len() as u64).to_be_bytes());
+ digest.update(name.as_bytes());
+ digest.update((content.len() as u64).to_be_bytes());
+ digest.update(content);
+ }
+ let manifest: Manifest = serde_json::from_slice(
+ &entries
+ .iter()
+ .find(|(name, _)| name == "manifest.json")
+ .context("missing manifest.json")?
+ .1,
+ )?;
+ ensure!(
+ manifest.version == 1,
+ "unsupported behavior manifest version"
+ );
+ ensure!(
+ manifest.hooks.len() == 2
+ && manifest.hooks.contains_key(&HookSlot::Attention)
+ && manifest.hooks.contains_key(&HookSlot::Delivery),
+ "both implemented hook slots are required"
+ );
+ Ok((format!("{:x}", digest.finalize()), entries, manifest))
+}
+
+impl Release {
+ /// Snapshot draft bytes once, then atomically publish their directory. No
+ /// active pointer is changed here. Deployment must protect published paths.
+ pub fn publish(source: &Path, releases: &Path) -> Result {
+ let (id, entries, _) = read(source)?;
+ fs::create_dir_all(releases)?;
+ let destination = releases.join(&id);
+ if !destination.exists() {
+ let candidate = tempfile::Builder::new()
+ .prefix(".candidate-")
+ .tempdir_in(releases)?;
+ for (name, content) in entries {
+ let path = candidate.path().join(name);
+ fs::create_dir_all(path.parent().context("file has no parent")?)?;
+ let mut file = fs::File::create(&path)?;
+ file.write_all(&content)?;
+ file.sync_all()?;
+ }
+ sync_directories(candidate.path())?;
+ // fs::rename is atomic within this directory. A competing publisher
+ // may have won; verify its content rather than overwrite blindly.
+ if let Err(error) = fs::rename(candidate.path(), &destination) {
+ if !destination.is_dir() {
+ return Err(error.into());
+ }
+ }
+ }
+ fs::File::open(releases)?.sync_all()?;
+ let release = Self::load(&destination)?;
+ ensure!(release.id == id, "existing published snapshot has changed");
+ Ok(release)
+ }
+
+ pub fn load(path: &Path) -> Result {
+ let (id, _, manifest) = read(path)?;
+ Ok(Self {
+ id,
+ path: path.canonicalize()?,
+ hooks: manifest.hooks,
+ })
+ }
+}
+
+// Files were synced when written. Sync directory entries bottom-up before
+// publishing, then sync the release parent after rename. Actual crash durability
+// remains subject to the filesystem and storage stack.
+fn sync_directories(path: &Path) -> Result<()> {
+ for entry in fs::read_dir(path)? {
+ let entry = entry?;
+ if entry.file_type()?.is_dir() {
+ sync_directories(&entry.path())?;
+ }
+ }
+ fs::File::open(path)?.sync_all()?;
+ Ok(())
+}
diff --git a/klbr-runtime/src/hooks.rs b/klbr-runtime/src/hooks.rs
new file mode 100644
index 0000000..fe6fc82
--- /dev/null
+++ b/klbr-runtime/src/hooks.rs
@@ -0,0 +1,162 @@
+//! Policies return proposals. Validation and failure disposition belong here.
+use crate::{
+ behavior::Release,
+ protocol::*,
+ worker::{no_host_operations, WorkCommand, Worker, WorkerConfig},
+};
+use anyhow::{bail, ensure, Result};
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use std::{sync::Arc, time::Duration};
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
+pub enum DeliveryDecision {
+ Allow,
+ Hold { reason: String },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
+pub enum AttentionDecision {
+ Wake { priority: Priority },
+ Defer { seconds: f64 },
+ Ignore { reason: String },
+}
+#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Priority {
+ Operator,
+ Directed,
+ Ambient,
+}
+
+#[derive(Clone, Debug)]
+pub struct ReviewedDelivery {
+ pub decision: DeliveryDecision,
+ pub fault: Option,
+}
+
+pub struct Policy {
+ pub release: Release,
+ pub worker: Worker,
+}
+impl Policy {
+ pub async fn start(
+ config: &WorkerConfig,
+ session: SessionId,
+ release: Release,
+ ) -> Result> {
+ let worker = Worker::spawn(config, session, Role::Hooks, Some(&release)).await?;
+ Ok(Arc::new(Self { release, worker }))
+ }
+
+ async fn invoke(&self, slot: HookSlot, input: Value) -> Result {
+ let outcome = self
+ .worker
+ .call(
+ WorkCommand::Invoke {
+ id: OperationId::fresh(),
+ slot,
+ input,
+ },
+ Duration::from_secs(2),
+ no_host_operations(),
+ )
+ .await?;
+ match outcome {
+ Outcome::Hook { value } => Ok(value),
+ Outcome::Failed { error } => bail!("hook failed: {error}"),
+ _ => bail!("invalid policy result"),
+ }
+ }
+
+ /// Unavailable required review means hold, never accidental approval.
+ pub async fn review(&self, effect: &EffectId, text: &str) -> ReviewedDelivery {
+ let result = async {
+ let value = self
+ .invoke(
+ HookSlot::Delivery,
+ json!({"effect_id": effect, "destination":"operator", "text":text}),
+ )
+ .await?;
+ let decision: DeliveryDecision = serde_json::from_value(value)?;
+ if let DeliveryDecision::Hold { reason } = &decision {
+ ensure!(
+ !reason.trim().is_empty() && reason.len() <= 2048,
+ "invalid hold reason"
+ );
+ }
+ Ok::<_, anyhow::Error>(decision)
+ }
+ .await;
+ match result {
+ Ok(decision) => ReviewedDelivery {
+ decision,
+ fault: None,
+ },
+ Err(error) => ReviewedDelivery {
+ decision: DeliveryDecision::Hold {
+ reason: "required delivery policy unavailable or invalid".into(),
+ },
+ fault: Some(format!("{error:#}")),
+ },
+ }
+ }
+
+ /// This slice exposes and validates attention policy but does not yet own a
+ /// model scheduler. The caller must not acknowledge input merely for a plan.
+ pub async fn attention(
+ &self,
+ event_id: i64,
+ source: &str,
+ received_ms: i64,
+ ) -> Result {
+ let decision: AttentionDecision = serde_json::from_value(
+ self.invoke(
+ HookSlot::Attention,
+ json!({"event_id":event_id,"source":source,"received_ms":received_ms}),
+ )
+ .await?,
+ )?;
+ match &decision {
+ AttentionDecision::Defer { seconds } => ensure!(
+ seconds.is_finite() && *seconds > 0.0 && *seconds <= 600.0,
+ "invalid defer duration"
+ ),
+ AttentionDecision::Ignore { reason } => ensure!(
+ !reason.trim().is_empty() && reason.len() <= 2048,
+ "invalid ignore reason"
+ ),
+ AttentionDecision::Wake {
+ priority: Priority::Operator,
+ } => ensure!(
+ source == "operator",
+ "external input cannot request operator authority"
+ ),
+ _ => (),
+ }
+ if source == "operator" {
+ ensure!(
+ matches!(
+ decision,
+ AttentionDecision::Wake {
+ priority: Priority::Operator
+ }
+ ),
+ "operator attention cannot be suppressed"
+ );
+ }
+ Ok(decision)
+ }
+
+ pub fn describe(&self, slot: HookSlot) -> Value {
+ json!({"slot":slot,"kind":"decision","placement":"hook_worker","revision":self.release.id,
+ "handler":self.release.hooks.get(&slot),"deadline_ms":2000,
+ "failure":match slot { HookSlot::Delivery => "hold_effect", HookSlot::Attention => "retain_pending_input" },
+ "host_effects":false})
+ }
+ pub fn list(&self) -> Value {
+ json!({"revision":self.release.id,"slots":[self.describe(HookSlot::Attention),self.describe(HookSlot::Delivery)]})
+ }
+}
diff --git a/klbr-runtime/src/lib.rs b/klbr-runtime/src/lib.rs
new file mode 100644
index 0000000..546402e
--- /dev/null
+++ b/klbr-runtime/src/lib.rs
@@ -0,0 +1,12 @@
+//! New runtime boundary, intentionally independent of the legacy memory/tool core.
+//!
+//! This crate is a development slice, not yet the production daemon. See
+//! docs/runtime-v2/STATUS.md for executed checks and integration boundaries.
+#![forbid(unsafe_code)]
+
+pub mod behavior;
+pub mod hooks;
+pub mod protocol;
+pub mod session;
+pub mod store;
+pub mod worker;
diff --git a/klbr-runtime/src/protocol.rs b/klbr-runtime/src/protocol.rs
new file mode 100644
index 0000000..fe2abc8
--- /dev/null
+++ b/klbr-runtime/src/protocol.rs
@@ -0,0 +1,261 @@
+//! Host-owned wire vocabulary. Python values never deserialize into host code.
+use anyhow::{bail, ensure, Result};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::{fmt, str::FromStr};
+use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
+
+pub const VERSION: u16 = 1;
+pub const MAX_FRAME: usize = 1_048_576;
+pub const MAX_CODE: usize = 262_144;
+pub const MAX_OUTPUT: usize = 65_536;
+
+macro_rules! identity {
+ ($name:ident) => {
+ #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+ #[serde(try_from = "String", into = "String")]
+ pub struct $name(String);
+ impl $name {
+ pub fn fresh() -> Self {
+ Self(uuid::Uuid::new_v4().to_string())
+ }
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+ }
+ impl TryFrom for $name {
+ type Error = String;
+ fn try_from(value: String) -> std::result::Result {
+ if value.is_empty()
+ || value.len() > 128
+ || !value
+ .bytes()
+ .all(|b| b.is_ascii_alphanumeric() || b"_.-".contains(&b))
+ {
+ return Err(concat!("invalid ", stringify!($name)).into());
+ }
+ Ok(Self(value))
+ }
+ }
+ impl From<$name> for String {
+ fn from(id: $name) -> Self {
+ id.0
+ }
+ }
+ impl FromStr for $name {
+ type Err = String;
+ fn from_str(value: &str) -> std::result::Result {
+ value.to_owned().try_into()
+ }
+ }
+ impl fmt::Display for $name {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+ }
+ };
+}
+identity!(SessionId);
+identity!(Generation);
+identity!(OperationId);
+identity!(RequestId);
+identity!(EffectId);
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Role {
+ Workbench,
+ Hooks,
+}
+impl Role {
+ pub fn argument(self) -> &'static str {
+ match self {
+ Self::Workbench => "workbench",
+ Self::Hooks => "hooks",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+pub enum HookSlot {
+ #[serde(rename = "attention.plan")]
+ Attention,
+ #[serde(rename = "delivery.review")]
+ Delivery,
+}
+impl HookSlot {
+ pub fn name(self) -> &'static str {
+ match self {
+ Self::Attention => "attention.plan",
+ Self::Delivery => "delivery.review",
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct Envelope {
+ pub version: u16,
+ pub session_id: SessionId,
+ pub generation: Generation,
+ pub message: Message,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
+pub enum Message {
+ Hello {
+ token: String,
+ role: Role,
+ revision: Option,
+ slots: Vec,
+ },
+ Execute {
+ id: OperationId,
+ code: String,
+ },
+ Invoke {
+ id: OperationId,
+ slot: HookSlot,
+ input: Value,
+ },
+ Interrupt {
+ id: OperationId,
+ },
+ Shutdown,
+ HostRequest {
+ id: RequestId,
+ execution_id: OperationId,
+ request: HostRequest,
+ },
+ HostReply {
+ id: RequestId,
+ execution_id: OperationId,
+ reply: HostReply,
+ },
+ Completed {
+ id: OperationId,
+ outcome: Outcome,
+ },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "method", deny_unknown_fields)]
+pub enum HostRequest {
+ #[serde(rename = "local.send")]
+ LocalSend { text: String },
+ #[serde(rename = "runtime.wait")]
+ Wait {
+ seconds: Option,
+ reason: String,
+ },
+ #[serde(rename = "hooks.list")]
+ HooksList,
+ #[serde(rename = "hooks.describe")]
+ HooksDescribe { slot: HookSlot },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
+pub enum HostReply {
+ Ok { value: Value },
+ Error { code: String, message: String },
+}
+impl HostReply {
+ pub fn error(code: &str, message: impl Into) -> Self {
+ Self::Error {
+ code: code.into(),
+ message: message.into(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
+pub enum Outcome {
+ Cell {
+ status: CellStatus,
+ output: Vec