From 336d47f978528a72daf61dee9078c600d25ab6d7 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Thu, 8 Oct 2026 08:52:56 +0000 Subject: [PATCH] =?UTF-8?q?es:=20rogue-key=20guard=20=E2=80=94=20=D1=87?= =?UTF-8?q?=D1=83=D0=B6=D0=BE=D0=B9=20audit-=D0=BA=D0=BB=D1=8E=D1=87=20?= =?UTF-8?q?=D0=BF=D0=B8=D1=88=D0=B5=D1=82=20legacy,=20=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20=D0=BD=D0=B5=20=D0=BB=D0=BE=D0=BC=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20(=D0=B8=D0=BD=D1=86=D0=B8=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B=2006.10/08.10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Studio_Participation_Agreement_2026-09.pdf | 168 ++++++++++++ artifacts/studio-conditions-2026-10.html | 74 ++++++ artifacts/studio-conditions-2026-10.md | 45 ++++ docs/digests/hour-2026-10-06-0400.md | 43 +++ docs/digests/hour-2026-10-06-0830.md | 43 +++ docs/digests/hour-2026-10-06-1810.md | 38 +++ harness/dashboard.py | 156 +++++++++++ harness/es.py | 15 ++ harness/roy.html | 83 ++++++ out/dashboard.log | 5 + scripts/deploy-claude-code.sh | 289 +++++++++++++++++++++ 11 files changed, 959 insertions(+) create mode 100644 Studio_Participation_Agreement_2026-09.pdf create mode 100644 artifacts/studio-conditions-2026-10.html create mode 100644 artifacts/studio-conditions-2026-10.md create mode 100644 docs/digests/hour-2026-10-06-0400.md create mode 100644 docs/digests/hour-2026-10-06-0830.md create mode 100644 docs/digests/hour-2026-10-06-1810.md create mode 100644 harness/roy.html create mode 100644 out/dashboard.log create mode 100755 scripts/deploy-claude-code.sh diff --git a/Studio_Participation_Agreement_2026-09.pdf b/Studio_Participation_Agreement_2026-09.pdf new file mode 100644 index 0000000..fa93b08 --- /dev/null +++ b/Studio_Participation_Agreement_2026-09.pdf @@ -0,0 +1,168 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R /F5 7 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /Times-Italic /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/BaseFont /Times-Bold /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +6 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/BaseFont /Times-BoldItalic /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +8 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/PageMode /UseNone /Pages 14 0 R /Type /Catalog +>> +endobj +13 0 obj +<< +/Author (Shkor Labs LLC) /CreationDate (D:20260923161207+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260923161207+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Studio Participation Agreement \(42 Labs\)) /Trapped /False +>> +endobj +14 0 obj +<< +/Count 5 /Kids [ 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages +>> +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2786 +>> +stream +GatmP)msbaC7Co"`4/T(tbZkN2;t/lF4d^3iK"`r8$I^l.]IY>QG:rY4qHhp6@OHtZ+*ub]nWG<:OR]=1\bO!nrVrAW\6uJR\-^!0[(!cMSSa?6&(e9t@FW&agg8fsqNK[;GL6A;58\bcgeA]I*-K7#pKVuYKg#pe<*EhHn3S_t22lQRZsb6DMQj1A.DG*&()d(7AoLIWga2#QtSYEp)]mtNHHqRZ2rZbH/4FP+s//=;*q.Po(Q7"f"u>7ni!!WBR%F@cjNCeH?.Ho.G_U=.a8W?,o[Cjg^-"DA[F;1U0+L#P?Cn/4>p,B?"D)]d.71f`mr_DP3_W9"pR=%D%4cq0?Cf'8OKipo4)6$O0l^l,kn*h_J]!$*_aOUm2m_r9.>qe[JFD9\EKQp]/b9HcGW,?'maV3PH!,/kJJq=YhCP>`m>K9bFd*S9si2t5Vdku/O-R9BSh&rAP[dU:i5L!OAP/g(L>p&c/IO;04jMc[3%_ps@ABi358i%6[Pp"Jmhm[qd]--4J=Zo.Vk1%-H%8aJIn+RYr,mf<=Vk$^6&iS\d,#^?5#K-*U'09Co+HXU'6j`^`dW-#M4mkFQ$)Z47nP*0B;.u;kgs8)S@0HT9+O&o[1mO%\c/&jJE4p'7DQ@;GWVYV\01@MIGaRHW7O]!UGHT;JI/W_d[HHZQj>0-1TnU=Z?f\H<_t52g68i5+7,C/%'=is&_QbVF$Z*^o&odc*(KIt<&TimcfHH(WESeMkBq.N81IK6Wml'Ud?I3SU,0HL^2ic!LAl#p`s@o.+1aA*;G%C-rX&f:eRI9G4^"[SodC)_gFq`B-^:SuLFgV>)PQftmWhOl1^sB>@/W5F!Llg(&Qa"eHJ8]1`BtRXPA!F@!5G(">$@@9"tF>04sGMpQZGQ93Rgq0EH:n!%\nJQ8i!F$i(cY+"i#32GA@V]quab;3?Jerp*3c@8#!j0i>8nINW7,[%V>>2e"LTp1!n^=.g]dN$qk<7-SkkIY:6Rqf(D@UCXW=rS21eZ;\jTU"^@b5Y60K?b"a89!(o#t@c:!pYT]DoBW#B(lR6j%rjXh+4oFJu=iF=P`c_hN\fK_J7XZe&oA#%ZD4B7\YZ.,\)?,W26F!\RS=-+JCKc>UjH#.+F+A[c:oZX6C%guqGRnOR(sHEFp'[gg3YmIKlNhG;nP2%E`FS=gW*b\H\lX4<;-Cp+7JSE1URPM$ScUnC(MmMear&DQ*t2k:,H1C#fFTh!`EJH'^W$'QYTQCE8%Uod9_],Lq!.spjhrcITuiHhXl7E\-8`kBl+31n!GZP#qN5%@._McH!Voscoj8MKY;[K%K.Dphb&S+MRrdGD0JQC+YSs\\D,_\Dd^plA3nuL,i`TQgUe6pc0!B'+`L[.Q.M0rhoK.4`6NG[?h?[rWtA-2;iu$4;/[DcO0DC4]VqlU]2jG"=tIDm[s(O^`,SVU8M.5OjXb6OlG.'36:G;q4epM%!aF.(4Ck',1N3TAWThpD6;P#/We_!-PqZe!e;"f?OcBl$];[4`),[lj2mC5Ah]_!WgBho[L=3E[HGR^[_bT/c&pSDZ]Ost0*V\2OJ8T4Zf1Q2bc>#lR;:f=V;F[:[.s=^5:%(#$R<"hWV*]$o)9.W&;j&aO8aD[#bu]cRWR@3$F9!AhTE39Mqsm?1aM*]3Si@9O.cKlIgGQ6Ai-,.sW]rX;\V61,07(#X[gA=#,f$c&V%[/RT7O4r0`Z,`sZ'qPS%Tn=LstNZu;tI!o],2gm%\3JO1gVYt)GAi$MEN=[u_D,EO,L9aGCS$fR1Z/VfZ\^GDtXOFUp2S`F81P9'^GX7B5nV3B74\bX[q_qF>7"P*XV:2W7c6ZakWK7LrdV>OS37J^rcVpmkHe8D[e(U]aW9TopDL+;TW/fN%"Z/5dUo7&&9;sU_$>JMq^2"Ir(5]pF/VgcrNF[ns%boLMiD4;VgQcJ3^Bd/\oJna3E1G+:=ga)$;%J/6ZVd9%CWhlmch?W1G9Iq89I,Vbi1E7kW]'RU649^K3Z25>Ai#^Nq7;dVL3G8-^#?C9G9cErF-N`#Cp#h/(g&2p01caNh*?*kbuFA-!cMo,]A#4dI8k]A(cS%)\5ee'I/#qbQO:h~>endstream +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3015 +>> +stream +Gau0Dmr-r?')g\%Jgg=hBAofc@EDE"qiI]"@Y%fm0?F8'S.>ioU2=dZaVCeNrpT:b8_KC\2ar6aKA)/1EotW92_tl0O+2`V5Mn0gnU\Bg%9O1ILF!=?hMaVq`]cW"q>/)n$!HQ4k)o+dmLe%&T4UNM!k2[,-=2e2&W:7m+2i65[UG@ape%d[Jj'%V*e'uqc*e3WC"(:!gn%%5`/0gk@2?)sT6ogMB"%Nbn>0`T0V#I\h69@28b("GG:]rP8r<-&J:GE/DQb>YU*L0#4IKX,,_O:kZX=;rfcQ6pABF`')0osZp&[J"P2PE-=g:Q7GH>++]b[[^h_`N%.B7tYCf=D]D2]Y)\ud;\Cs*bLF1LC9eeMC_PY__EeH[%aF3rC/)6SJ)!F7+S-jHE]'VW$VOq^?Snb6fj:ig]Jf42lgM\n&J-0tos;1b2bG>-)Z#X]4Y5eHiDlG^s/$=sMP-OADUD?q[WGqLWJ`i,ja/:Yp=P5>F#pfEC[6G^F^WTgP12^9S+^(io\"E]f2S_io3B?j"rocE6)ji@0dBM8&OMtBZ(/eNGTnf!uVC-4/.8(E_7d5\BV3/h\j"J]^#'6&&X&'kBFt5>9TBB:QW0AOeWf+M^p,+W;)?q/>VR:YAabfM/_QfX^ng`g:mUYr_o/!'t_;U^PMe\9lIL5'ZQuCSj\^iP7^m.,l5ST?@CF*eS[DXoJ9:2e140m[p!JKP>$,3;fL-!TQgN"8sB_M^oJ-o+cJu4VA>r,hT1S@#J"n"]9jQ`)*3HliUp6nK"rdYM[LKOhP/7q&K"AD7Dj=bnDT+TPB'?`,0Ro_8"SQJE4"h/(+^bZ7X7d=YrdX,8Qgt7%[jQg6L2US3)`<[>B#W(">$)OH+eqZ_%YQd"!c*_f6%DFh:P4iE,@lij[Kol&'A>tM&I3/2^^'(i`:Q_%KPJCH9CW+oKWi4'ho\6F63iDdSL.15?4:0t+:@8o"pEPGc'-XQ?6u,((YGDQBn`d]Wp.:uqThASOah([PW&9QLk7[i`>J5+pkQGTnjYgsL?K\4Z=ll3lPHFP)/VqT47>hS*sH?ugH=/a2Jho]HVluc5E:Bg+TBV3m!G&>O[1aP+T"^mC^/Raod&Q%$9LnC2T@6:/Jk-H@RC^(VFHWBX6dd#0V.,u:3['YMk*j:8q>pCkc7Di5+%pn`6U[Z3de+c3Ec^2N)!kph0FQ?!![kl3Vs*+UZ/1t@6=S,%mjK4Nf+;gmhJR(^ND@"D?J+^qb`L@)>,nCWX.GJce26s:lk^ZG-g,2Ws[=9+r"BD*r+71S#1%:/)@%Iob+%7)[?Cc[lE4tQISon[aSa<<;%HMQdB*,I@^`Yf=$*Gr\\Q]^q^ZA(#j;OE4%1ndiY'XK%jioG6f&:.icq*OA]AK/H9ANJm)`#QTL"8s:,AQ*MLhC;4reCF9S@`f_q(a:@AqZmuL%YGTObS=,Xs6Y-gQ0?o)C4q+iA]VS&dr)Y[H`)#^2fePET;:2D.-4(/N<''?#*g`Ri=:h5mY!;X>"GsZT(Z1-NL)N1o1CP80\a(o'VTc%5f?Ph;?U5Z>jB.)2a,oLG=;\b61C);cb`PlP-]g)r'o)WgCqXkmD'C@Zd3X9(UJ:^i]SF0/uZWQW-3):ck&hbpD5fJ8Q$Y*6@HCkb`LpEB0XiO06&:o'H^X4H:QXt@WUX!3R/ga('DVD#_F>uk=%7tV]6f\&PSBW$dmD,el-f@)Oi[DsIl1V$!n$)p_N&U#YgH8.oYH:3JLOEL,;7J:$*7Jr?XnPiZ*>JgjbTONs:2F[o?uQfn.e6eOpU(1""c=SfIC\,RO*2j!4'RPhh+61rPRGPYg?;ci"V6k&=uJo"aCD6!hlU9FCQX)aD69t$D..0R&s"i?#V&^a1_URY]WU56d7*VMDn]`@BGuAUUu"3C'E==;NQ3jmcGED!Klq''`l.ZK#B%1KOLae^,u'V_+MrW.ADMXfB1Wk)JrL!/l$EBoL:Re@5)co"O;5j.I]f.n8%4Gendstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2659 +>> +stream +Gatm_D#P6^g2.e6PW#(X8[E&BT1S#gZZ$\ae=V'O1j7XOUA;s;72SeQ8nk0AM\LNH4KqLZV%H/Z#62WpgV&d"Mioc)du!MsZXefPlc=Z&%"Se*b6$HVMK,^\_(_R(;>>t%`U95RiKj!c%V;%Zfr?M/O8[5-%GPl>&$/`2Y,#8MR)1N$e;Eee^j)Gd@1:%K,p9N5XfD'6Xa@KV0%E&4#3jS[7m!P!0>(WPK<^3ffWW?CLF&rtq>#(?D);lEA;qf$Z"IB^IqE`lPEb+$FC_!`!fibgjA0ZP40MFE4WhpmM,*UbpB-G]3R_4mfBt1[NYe,OhEMS`12K9eKa?TlqV-%FDf&C:2W?lS[idKed,P<*GT'-P)We*@DCALtq7m1(GSp(3"1#6UF%$dc/'Q\_MGb%2.D;SE>-1pA?1-V_`PX>%$Nfbua2J/@9D-^t_h`3:j@E]^$<@^WCaK)aF@`3G8sTV*_QE\!&rdhkliH\+io,'d,X,5#6$5X/C"&OSEDU.gLGVAZbn8'/+J0PT"pD4KnJ@muN"2RX1*Lb6!bA)!):IkXJ.J8%hQ3;/58e?PiY-05SS\&?>T,7U/62?:*6/"S/&k3)T7*aG^ojA>C,YX6CjAGK>OlbjV5)3XhO9N=@18cY%[]+)6@/'A@^.D=C#LHdULG7I/,j.LusB1KG+E&Q<7\GYO$PPPA;`V3ZrNjbEioh@^P6FjGQFWUYE;QG'XE,^(kM3H^b$0+%23QPn?>8QrgK>YrC;&.?4uPHmu"PfC$P4Z8:CbJgFJhfr90MuS_RcSF\!N($(C%TI(c7S1KRX)qV\=Y;HdOJ\#"1sg@@P\(j\1XY0X&c+=Z-V5o^V\O#R'8/2Z=S]I.3_2,3i(ZtL2>#eSdYcn%6cE1F/%7Ji6sVC8FH@QWU\)Bi6K1`*ecKE5A55Os>\+Y(Cg(-h^Yo>6NRlJ+$($agM)BLb'O^i?B[r@6)@FF4.l-:8F@g_2C*CP<2qE3hV,;W9LM$=]`PNQ9$?i7/!hHB,=boSG>"TT'LU-VBMh_i8qGF@qCoL#t`dDGm:fZ)t1oK^ZO4i]plW8)AC8]>r,B@6>p!tSO2<>(C]1-hIS/?B"S5b&h]`8:5-Wjjg0@FP[B>:WL0%6]$C^C]g2+>!6Wdk30$8*R*(F!OVUVG)o\sSJLA*^`,n3G4:n:"Y(%d;MAnG_FSf]I=U6;@l&!90eYf;cjjA$W65h-eeX7O(JT,o$,"FgsP&;DZLSO\>$isFTqa_d5AgFZ.]kiL65<;*cWU?,;WDN@(B8)k\KGCD$iIJUGNi+u%l!"RY6ljk*$W8S>AM,]p$NN.'VSC!Mf`)J\DcV#*ɲp=S%CeoYh+cTuH>]4lScAA%`"_?0VK$H4NXh45cacNDafI0Vdk2T=`=ZcMT2MT@NK994\$$]/Ars4e3WZoUIr:K=e&a.(FmIWYG$6urKMINa*(tMM.H/fi+^J`t(D$IYBscbg=S:73?k&8aOJ$!*[\iK#kFm$Y8O^S16>TNiIshZ\1$,CAU*pRTKu9Vt@s557&*tBb*8bSnmcKp8pT,a5>kb$k1/#Ns4DB3)0k=-8/_:qW>+m!M7M7CeLE)NG(_Gk>m,f%%5%[FsJ`2#g/Ek]n2JDYMR&7T>.1/LRq5)RC0:dfr5B1MK_q/?3q#9%!dp1-]bQ"M^O347]31]*cdoVd`$lXXd'aIa%-0%bgk[f#kg1Ci+F;3%f^Yn)ZdBGS0"8m9GqZ~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2487 +>> +stream +Gatm<_/e:"'#"/mqR6$^KEXUR"QE@cYu@Vo;Vm;>p7f_A(m&5<0Z_I'2#i#3%fd5=CeLU&fd0JuB$f#qp$:4#.)s"Ir%&e]TC1oM*th1D\uR:56lW`7NS`R+q,g+Fd8WT)_tf%KOOfV^9rE]3mY:6.]JnuZ]75`k2+"]g[eq5-3bpcjrAgjd/m>%U\"Q2.rW]q6m_0P#eDIlGsq0h'L_PocO^\ZV"n$-fpDhje*A$X?KhZ8X;P*_@^Q`bN\NV[:k-8=0[VB(LWF0?@E!IZm^f;0qu"7kfpAA#j3qWb#3LAJ1[Mq*Mo#oCu+cW!NdcndVPCYJ@jCi#b#LD/"F(sl$J>/RR[Dgdd@ao@_!oelBPrWoa"==K>:K.C)9b&`_hnp=YY`UGT;9T]P\rFQPpYs6kLe,*E@AiS;m-jW'eXmWU#@h%t>u]OLaYhBM6Ik<@u._!cu+ShEX'HiZX>=rXo*qC"uD:h98O7*Do5+gedETs8M?=J(RGBe><_Qhk;Du28=2q1i:]un@rT=7L@sLRb??OC`'EC`2."gG#%J"]:&C8Id2mlJi`lEB)&g"Zo-en/rflQDVsRGXBMe04+:B0m*,Bp/pBlb9%M;HsKJjhePa)KT6.lBar$%+`-p[i.C*ue^:CJ#KfXITXO]'uE5;+IMmui&d%LVRd`rfKU(Xl4Q?%/1Rp[,NWo!E1j\j_9H#%`P%9.$5hJ1/seZe'C[]F!e>M]Vp\WRJ$U[BV!(WlLi3(@)Amb^21\/1F\GHF<((R$0iB&:*:Z$!IWL"1t&EA^C5U>/2neRRn2EG1c/*FT!G)o`;K=8u3j>6YgH3&f2&?TPANGV&$(V3dp6Cf0T\58DCe0bTDi;:Hs>m^[@c+M2NW"Yfd7A0=#CK4V%k2p#\/[(q#j`?^7W1`-oiYN2$h7@\bDU.#_O9B^n6MrD-SAbC$[8EOm[[_,mO3ErYiF1;_+k)U':n'oe,c(QI*W4lboKIcC;5_Wl*rTp]R.N3GDtF8Uob%khnjk(U/&cR1Nne\f(>$nr/jSkD&Y4ff-rio/g6XfQ#UCMci7n&KgE6qL7gWgc?[MfbE[1TnN#C\T-0!>^e&R:6\MnZu4;]ZtcI;KSX$R4YL$%g\^Aj`2X+nUPc'fU3D@5g2..H3!I\Ph3-oUPdV"Z@EDEX7,a`!X>0"j+7'uf@JU&Z70T!?1TnH`H1_8fKe9%>oS!IdqjRs`^s#=H?#S_:\7^`qKo*1*Z&!$1m_T@bj4R>(q7D>ri.PF3Iaki#H[UtI3/"#nqWP>r:N+p"%o^]lU]0#.6g]hA#g(QS2:O=kZSYui:?Uendstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2420 +>> +stream +Gb!;d>B;!t&q8_Fd185PS?g1V^YkH/_2/gE4,0l-;oZ3W1BOp)7RRm/r;4skb[=O?9_OuUH.Q9o^dtTqYJ7fn!?6M.o/iKDZj1PriIKt*?jr'6q(T=1mbl!T+3YZ?@Oj.9Cs/qeiL9>PR-6t6-..]>bDB^>4Q!H8+MqtOB\\aWL#<4@T4.5AhJL2W&8Rtr'im(Kj92.k->qg.Ce%MGq*n%JOKjD2lUESj!X*p\F:"]WUjQ$1RVs28)sUbQ$=h6e>o+\<5bVV<-KA=RZ(3.^i6/mip=S;KqJ6TB=s$]D"^5EfEe;rKnTbG#aDrm-39LGn.V>b)MXdDI,Yb8&TK)So,%DTp0c!/,Yt3D3:nU6(JLF>no*g"_*`J6^-R)s<$T5mtYai"@=rV/@,HW55j!ZX[Dk%,k4AlK-4Jo)=1X[leSr=#$Ui_WpjZ2*'rY](\jXgl[I=0H^rSc-AgsIKQ/1C.g"P(D.7J2A-.:l:3gX;cMU?DopBeqB3&QVHD(A&#+Yl_X9;NW/5`<7ejO_:+KS)gA9l$DliXN2ff)qqff2a"CS>e8C`p/a9)$;.Mu8G^K0L4tW>JtMfO'nt64MCd)H_rQ3=W-hO/0X0eA`]UGM(O&*.\D$l(f:?C5j$=1\mG,%ujR-"4ZS]"lj%0g2,-]h8$nI^I0;DQR"BSkgG;i/n;9Lc+Z-4qK"K]2E9=HJm_(El03AR$3jp2WStH',Fb_HcSp;CtN_Db"Q(3-D$?;$`@aoM[JI?s4Nk5oH3H@0Xm.H`\/#/0tJ,?5/$XPWWO>fn*QHfu3AF7m68_9C8r$=eYB?5/K`f+97d"F@9TP'M>!]o%BN_/`Q@g0UXX/HZ(tZ%GGd`ruQ<>"kQGF1e1Xr?b@CpJK'#k!e+*an2d%CDA38>P7emOh*NhiD5<1M9P4&IduVpfAAOMmNF\5&r^IlKopc<,0HdU&+XtfGpfd-#DP^h0Uk5XaR`%s;-`%$cls%N)hHC\Vn"gLaC%Q]Hf#Xoc@)*t#aEYpL,RW(ZGS]q]@T\MpRQBQmc$H9?Vd!n\=MP$j0nIu=B$lYgbH91WCotFN_=udOf&bG75/=Y#O**dN&K[rk@DQADYZuu?;k/tu)AdZ'1roMHGem8[*77dUMhFuo0&1gS&++=Cau(cgM/>8Ums4>EgqB)L8nUMg>%g@:?C_pKRN$*d$IJ:n$E;TLFp14K&c0'ln?0M]6IT9I-c!eGP`*@@\93>@.7-4d+c-JFcICGqB_<%H+(t+VEb&1'\DlPS*5L+En7/$c"8'"_f!UOakpI_oEIOtJ%Dbo2/Q*g?2O#3U>=%=:\L4][O=^kH[#'SpNusqHj?/*-1UkVX"cE*1)4V3c4p"3]e3PqpAU)KiEcT;Xm&&fm)q\AmBu:/nq1tUWYg_,$4B?;7?gJpJ:#1FCrkb+?!jH4V_#'47's08<_ZOR5N.B?YlQbc96SkLeVUY&Yu/V'#-?ue/=E)+\deJZRD(k0J`k;Um"Tkn_E3=J:N@X[-V1os6?%tY$8u(P(Hs-*5CH_AF(nJ)7.>.@gi"H;PJHmm8sPU)Q6Y6L1P<,8jfGTU?\.`&Y?+7SeRK]6/:6"PC1IMr5uY$:B?N$+qN@`dE5hRIb@8EuH:U"Vml_Y/ai[k&#TS1KX/[VVO*$ji7'E%EE=D!Q!cTHGRtp`(:?_(T=+/El%g0'93kOMQuAkB,IHH`j#Cf>5OD'nn7o!%Yhh`3p??\;j0)idm[3;@.@2n56eG#AO"r"(Bk\VY/#@6Z,Z65'ul=rlAC>,*WB1OIokYd972eF#BVk^6obZIo*X%kaeaEq+t23PXfa>i&aRY<2OXk3giAYbK^eg:mT%FEJZo(UMS6s8:gURQ>oec=pHhKH/thm$72X\1T$#eI2oQ-.(q!m^iAr4q>JcrrNUjQNI~>endstream +endobj +xref +0 20 +0000000000 65535 f +0000000061 00000 n +0000000132 00000 n +0000000239 00000 n +0000000348 00000 n +0000000458 00000 n +0000000566 00000 n +0000000771 00000 n +0000000885 00000 n +0000001090 00000 n +0000001295 00000 n +0000001501 00000 n +0000001707 00000 n +0000001777 00000 n +0000002088 00000 n +0000002174 00000 n +0000005052 00000 n +0000008159 00000 n +0000010910 00000 n +0000013489 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 13 0 R +/Root 12 0 R +/Size 20 +>> +startxref +16001 +%%EOF diff --git a/artifacts/studio-conditions-2026-10.html b/artifacts/studio-conditions-2026-10.html new file mode 100644 index 0000000..050b588 --- /dev/null +++ b/artifacts/studio-conditions-2026-10.html @@ -0,0 +1,74 @@ + + + + + +42 Labs — условия студии и инвестиционной программы + + + +
+
+

42 Labs — условия студии

+

Участие · Инвестиционная программа · Управление. Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор.

+
+ +

1. Как устроено участие

+

Набор в студию — раз в квартал. Условия участия подробно: 42.cil.by, объяснение договора: cil.by/static/dogovor_obiascenie.html.

+
    +
  • Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня.
  • +
  • Платишь большее из двух и только его: месячный взнос $999 или [X]% от выручки проекта. Месяца, где платишь и то и другое, не существует.
  • +
  • Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko):
  • +
+
Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей
+
    +
  • Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц.
  • +
+ +

2. Инвестиционная программа (SAFE MFN)

+

Инвесторы вкладываются по SAFE с MFN (most favored nation). Голос инвестора определяется на момент инвестиции:

+
Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения
+

Голоса фиксируются в момент входа и дальше не переоцениваются — растёт или падает биткоин после инвестиции, число голосов не меняется.

+ +

3. Квартальные уровни инвестиций студии

+

Раз в квартал студия распределяет инвестиции по уровням:

+ + + + + + +
УровеньРазмер инвестиции
1$1 000
2$10 000
3$100 000
4$1 000 000
+
    +
  • Подаваться нужно на текущий уровень — перепрыгивать уровни нельзя.
  • +
  • Количество победителей на уровне регулирует правление. Сейчас — по 1 победителю на уровень.
  • +
+ +

4. Ежедневные звонки: 3 через 3

+

В рабочие дни звонки идут по схеме «3 через 3». Время распределяется пропорционально текущим голосам инвесторов. Если у инвестора ноль голосов — ему отводится 3 минуты.

+ +
+42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · 42.cil.by
+Эта страница объясняет условия, но не заменяет договор. При расхождении действует договор. +
+
+ + diff --git a/artifacts/studio-conditions-2026-10.md b/artifacts/studio-conditions-2026-10.md new file mode 100644 index 0000000..30dda0f --- /dev/null +++ b/artifacts/studio-conditions-2026-10.md @@ -0,0 +1,45 @@ +# 42 Labs — условия студии + +Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор. +Подробно: [42.cil.by](https://42.cil.by/) · Объяснение договора: [cil.by/static/dogovor_obiascenie.html](https://cil.by/static/dogovor_obiascenie.html) + +## 1. Как устроено участие + +Набор в студию — **раз в квартал**. + +- Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня. +- Платишь **большее из двух** и только его: месячный взнос **$999** или **[X]% от выручки проекта**. Месяца, где платишь и то и другое, не существует. +- Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko): + + > Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей. + +- Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц. + +## 2. Инвестиционная программа (SAFE MFN) + +Инвесторы вкладываются по **SAFE с MFN** (most favored nation). Голос инвестора определяется на момент инвестиции: + +> **Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения** + +Голоса фиксируются в момент входа и дальше не переоцениваются. + +## 3. Квартальные уровни инвестиций студии + +Раз в квартал студия распределяет инвестиции по уровням: + +| Уровень | Размер инвестиции | +|---|---| +| 1 | $1 000 | +| 2 | $10 000 | +| 3 | $100 000 | +| 4 | $1 000 000 | + +- Подаваться нужно **на текущий уровень** — перепрыгивать уровни нельзя. +- Количество победителей на уровне регулирует правление. Сейчас — **по 1 победителю на уровень**. + +## 4. Ежедневные звонки: 3 через 3 + +В рабочие дни звонки идут по схеме «3 через 3». Время распределяется **пропорционально текущим голосам инвесторов**. Если у инвестора ноль голосов — ему отводится **3 минуты**. + +--- +42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · 42.cil.by diff --git a/docs/digests/hour-2026-10-06-0400.md b/docs/digests/hour-2026-10-06-0400.md new file mode 100644 index 0000000..806f648 --- /dev/null +++ b/docs/digests/hour-2026-10-06-0400.md @@ -0,0 +1,43 @@ +# Дайджест hour 2026-10-06 ~04:00 UTC + +## Батч задач (2/2 закрыты, обе [M]) + +1. **t-261006034257-2cacd1** — MCP-инструменты OpenCode FR#4-#7. В гейт + cil.by/mcp добавлены 5 read-only тулов: `list_members` (31 член: + роль/проекты/паи/агенты), `list_applications` (заявки+голоса), + `projects_list` (22 проекта, фильтры owner/status), `list_agents` + (18 агентов + last_heartbeat — дока ask_agent теперь валидна), + `treasury_status` (wallet-treasury **3733.60 USDA**, кошельки членов, + паевые, последние движения). E2E через гейт: все ok, tools/list=68, + фильтры проверены. Дока MCP_CONNECT.md дополнена. Портал перезапущен. + Бэкап mcp.py.backup-1006-readtools. +2. **t-261006034507-f41aa2** — аудит фича-запросов OpenCode. mcp_audit: + пришло ровно 4 (все ok) — в портале ничего не потерялось, но в Kan + была потеря: только 2 карточки-дубля [FR#7], #4/#5/#6 отсутствовали. + Причина: **гонка в `_kan_psql`** — общий remote-файл + /tmp/kan_agent_fr.sql перезаписывался при 4 параллельных + feature_request (портал threaded=True), ssh выполнял чужой SQL. + Фиксы: уникальный remote-файл (uuid) + настоящий cardNumber вместо + id фичи (раньше писалось kan_card_number=id). Бэкфилл: [FR#4]№27, + [FR#5]№28, [FR#6]№29, дубль #7 скрыт. E2E полного тракта FR→Kan + верифицирован, тестовые записи вычищены. Итог: 4 запроса = 4 + карточки №26-29, feature_backlog синхронен. + +## Инфра/события + +- Мин-тики часа: 6× gate:quiet — событий выше порога нет, LLM не + запускался (экономия burn). +- Фоновые задачи: нет (list пуст). +- Эскалации: открытых 0 (все acked; свежая e-261006025634 по TG-токену + Лунары — вне этого тика, наблюдалась ранее). +- Доска: только 2 старых blocked S-клона (02.10, ждут уточнений), + ready пусто. +- coop-portal.service перезапускался 3× в течение тика (по фиксам), + финально active, health ok. + +## AUM-нотация + +Тулы состояния (казна/члены/проекты/агенты) — база для агентских +контуров кооператива (twin-lite студия, AUM-симуляции): агенты больше +не ходят вслепую, а фикс гонки гарантирует, что фича-бэклог не теряет +запросы — прямой вклад в долгосрочную базу. diff --git a/docs/digests/hour-2026-10-06-0830.md b/docs/digests/hour-2026-10-06-0830.md new file mode 100644 index 0000000..626faf5 --- /dev/null +++ b/docs/digests/hour-2026-10-06-0830.md @@ -0,0 +1,43 @@ +# Дайджест hour 2026-10-06 ~08:30 UTC + +## Батч задач (1/1 закрыта) + +1. **t-261006072315-18199d** [L] — Доска фича-реквестов на странице + проекта (Alex). На портале cil.by `/project/` добавлен раздел + «Фича-реквесты»: агрегирует все входящие MCP `feature_request` с их + статусами. Сделано: + - миграция: `agent_feature_requests.project_id` + индекс (init_db, + применена на проде); + - API в `domains/boards.py`: `GET /api/projects//feature-requests` + (FR проекта + общий бэклог портала `project_id=''`, голоса, can_manage) + и `PATCH ...//status` (владелец/Председатель; статусы + new/accepted/in_progress/done/rejected); + - UI `static/project_detail.html` в стиле портала: карточки + (title, агент, дата, Kan №, голоса ⬡, проблема/предложение/usage_hint), + селектор статуса у владельца, счётчики по статусам; + - MCP `feature_request` теперь привязывает запрос к проекту агента + (`agents.project_id`). + E2E на изолированной копии БД: 7 FR на проекте 1, PATCH ок, невалидный + статус 400, member 403 на PATCH, изоляция проектов ок, 404 на несущ., + `node --check` ok. Прод: coop-portal рестартован, active, миграция + применена. Бэкапы `*.backup-1006-frboard`. + +## Инфра/события + +- Мин-тики часа: 6× gate:quiet — событий выше порога нет, LLM не + запускался (экономия burn). +- Фоновые задачи: нет (list пуст). +- Эскалации: открытых 0, все acked. Свежая e-261006065717 (mail_send + 500) уже закрыта фикс-коммитом `bc3c3e5` (import sys → NameError). +- coop-portal.service: 1 рестарт в тике (деплой доски), финально + active, health ok, агенты поллят штатно. +- Git /root/coop: изменения в 4 файлах (app.py, boards.py, mcp.py, + project_detail.html) — уйдут очередным auto-commit. + +## AUM-нотация + +Доска фича-реквестов замыкает контур обратной связи агент→владелец: +запросы больше не лежат мёртвым списком, а имеют видимый статус и +приоритет (голоса) прямо на странице проекта. Это повышает отдачу от +агентского контура кооператива (больше полезных фич → больше ресурса +на длинной дистанции) — прямой вклад в долгосрочную AUM-базу. diff --git a/docs/digests/hour-2026-10-06-1810.md b/docs/digests/hour-2026-10-06-1810.md new file mode 100644 index 0000000..780d94a --- /dev/null +++ b/docs/digests/hour-2026-10-06-1810.md @@ -0,0 +1,38 @@ +# Дайджест hour 2026-10-06 ~18:10 UTC + +## Эскалации (обе закрыты в этом тике) + +- **e-261006145651 (critical, «стор скомпрометирован»)** — ЛОЖНАЯ ТРЕВОГА, разобрано и закрыто. + Полный аудит `cli.py verify`: OK по всем 1.3M событий (подписи + тампер-цепочка, ключ faf8d19d9aed71fe). + Событие 1303573 перепроверено точечной криптопроверкой — подпись ВАЛИДНА. Единственная дырка: 1303601 + (сама эта EscalationRaised) записана без sig из-за транзиентного отказа подписи — аудит корректно + считает её legacy-границей, не ошибкой. Причина паники: упавший процесс верифицировал дельту чужим + ключом (изоляты /tmp/simbiot-iso-* имеют собственные ключи аудита) → false positive. +- **e-261006145703 (warn, дублирующий трейсбек)** — тот же инцидент, закрыт вместе с первым. + Рекомендация на day: прокинуть key_id стора в верификатор/не считать чужой key_id ошибкой. + +Состояние после разбора: проекции догнали голову стора (last_event = max id), CLI работает, +min-тик жив, inbox чист (0 без ответа; m-516173853/854 из контекста эскалации давно replied). + +## Батч задач (2/2 закрыто) + +1. **t-261006175404 (M) deploy-claude-code.sh — ГОТОВО.** + `scripts/deploy-claude-code.sh` (289 строк, +x): воспроизводимая развёртка Claude Code + ttyd web-TUI + по боёвому опыту Лунара (node 22 nodesource/tarball-фолбэк, npm i -g @anthropic-ai/claude-code, + ttyd 1.7.7 под архитектуру, OS-юзер, systemd-юнит с basic-auth). Опции --port/--user/--token/--service/ + --ttyd-version/--claude-version/--interface/--env K=V/--readonly/--dry-run/--check. Протестировано: + bash -n, два dry-run (включая лунар-профиль — ExecStart совпал с боевым), валидация аргументов + (exit 2), --help, --check. Идемпотентен (не дёргает restart при неизменном юните). +2. **t-261006175504 (M) API для поиска товаров по Shopify Collective — ГОТОВО.** + Публичного Collective-поиска НЕ СУЩЕСТВУЕТ (searchShopifyCollectiveSuppliers отсутствует в схеме, + 404). Официально: приглашения/коннекты/просмотр каталогов — только UI приложений Collective; подтверждено + стаффом Shopify (тред 2025-04) и changelog (свежего нет). Рабочий путь: авто-импорт policies → товары + с тегом 'Shopify Collective' + {supplier_name} → Admin GraphQL products(query:"tag:...") + вебхуки + products/create|update|delete; поставщиком — productCreate + publishablePublish в 'Collective (Supplier)'. + Детали в ответе задачи (доставлено в TG). + +## Сводка состояния + +- Задачи: доска чиста от ready по этому батчу; tasks_done растёт (обе M закрыты с доставкой в TG). +- Эскалации открытых: 0 (было 2). +- Верхний контекст: стор здоров, DRY-инцидент закрыт без потери данных. diff --git a/harness/dashboard.py b/harness/dashboard.py index dde21d0..b94c9e7 100644 --- a/harness/dashboard.py +++ b/harness/dashboard.py @@ -28,6 +28,10 @@ from harness import commands, es # noqa: E402 _HERE = os.path.dirname(os.path.abspath(__file__)) _INDEX = os.path.join(_HERE, "dashboard.html") +_ROY_INDEX = os.path.join(_HERE, "roy.html") +# ttyd (Claude Code) на агенте Roy — t-261008084337-3afbca +ROY_TTYD_HOST = os.environ.get("SIMBIOT_ROY_TTYD_HOST", "178.104.52.234") +ROY_TTYD_PORT = int(os.environ.get("SIMBIOT_ROY_TTYD_PORT", "7681")) _ADDR = os.environ.get("SIMBIOT_DASH_ADDR", "0.0.0.0") _PORT = int(os.environ.get("SIMBIOT_DASH_PORT", "8765")) @@ -541,6 +545,142 @@ class _Handler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) + # ── WebSocket-прокси на ttyd агента Roy (t-261008084337-3afbca) ──────── + def _roy_ws_proxy(self): + """Прозрачный WS-прокси клиент ↔ ws://178.104.52.234:7681/ws. + + stdlib-only: свой минимальный WS-клиент (рукопожатие + чтение фреймов) + + переупаковка ответов ttyd (серверные фреймы без маски, клиенту + маскируем). Управляющие фреймы проксируем на обе стороны. + """ + import base64 as _b64 + import socket as _socket + import struct as _struct + + host, port = ROY_TTYD_HOST, ROY_TTYD_PORT + try: + up = self.headers.get("Upgrade", "").lower() + key = self.headers.get("Sec-WebSocket-Key", "") + if up != "websocket" or not key: + return self._json({"error": "expected websocket upgrade"}, code=400) + version = self.headers.get("Sec-WebSocket-Version", "13") + try: + sk = _socket.create_connection((host, port), timeout=10) + except OSError as e: + return self._json({"error": f"ttyd {host}:{port} unreachable: {e}"}, + code=502) + sk.settimeout(None) + # рукопожатие с ttyd (без cookie: ttyd на Roy — credential=false) + req = (f"GET /ws HTTP/1.1\r\nHost: {host}:{port}\r\n" + "Upgrade: websocket\r\nConnection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\n" + f"Sec-WebSocket-Version: {version}\r\n\r\n") + sk.sendall(req.encode()) + buf = b"" + while b"\r\n\r\n" not in buf: + chunk = sk.recv(4096) + if not chunk: + sk.close() + return self._json({"error": "ttyd closed during handshake"}, + code=502) + buf += chunk + if len(buf) > 16384: + sk.close() + return self._json({"error": "ttyd handshake too large"}, code=502) + head, rest = buf.split(b"\r\n\r\n", 1) + status = head.split(b"\r\n", 1)[0].decode("latin-1", "replace") + if " 101 " not in status + " ": + sk.close() + return self._json({"error": f"ttyd handshake failed: {status}"}, + code=502) + # отвечаем клиенту тем же 101 (echo его ключа — валидация на браузере) + resp_head = head.decode("latin-1", "replace") + self.connection.sendall((resp_head + "\r\n\r\n").encode("latin-1")) + + def _read_frame(sock, masked): + hdr = b"" + while len(hdr) < 2: + c = sock.recv(2 - len(hdr)) + if not c: + raise ConnectionError("eof") + hdr += c + fin_op = hdr[0] + op = fin_op & 0x0F + ln = hdr[1] & 0x7F + if ln == 126: + ext = b"" + while len(ext) < 2: + ext += sock.recv(2 - len(ext)) + ln = _struct.unpack(">H", ext)[0] + elif ln == 127: + ext = b"" + while len(ext) < 8: + ext += sock.recv(8 - len(ext)) + ln = _struct.unpack(">Q", ext)[0] + mask = b"" + if masked: + while len(mask) < 4: + mask += sock.recv(4 - len(mask)) + data = b"" + while len(data) < ln: + data += sock.recv(min(65536, ln - len(data))) + if masked: + data = bytes(b ^ mask[i % 4] for i, b in enumerate(data)) + return op, data + + def _send_frame(sock, op, data, mask_it): + fin = 0x80 | op + n = len(data) + if n < 126: + hdr = _struct.pack(">BB", fin, (0x80 if mask_it else 0) | n) + elif n < 65536: + hdr = _struct.pack(">BBH", fin, (0x80 if mask_it else 0) | 126, n) + else: + hdr = _struct.pack(">BBQ", fin, (0x80 if mask_it else 0) | 127, n) + if mask_it: + mk = os.urandom(4) + hdr += mk + data = bytes(b ^ mk[i % 4] for i, b in enumerate(data)) + sock.sendall(hdr + data) + + # мост: ttyd→клиент в фоне, клиент→ttyd в текущем потоке + self.connection.settimeout(None) + import threading as _th + + def _upstream(): + try: + while True: + op, data = _read_frame(sk, masked=False) + _send_frame(self.connection, op, data, mask_it=False) + except Exception: + pass + finally: + try: + self.connection.shutdown(_socket.SHUT_RDWR) + except Exception: + pass + + t = _th.Thread(target=_upstream, daemon=True) + t.start() + try: + while True: + op, data = _read_frame(self.connection, masked=True) + _send_frame(sk, op, data, mask_it=True) + if op == 0x8: # close от клиента + break + except Exception: + pass + finally: + try: + sk.close() + except Exception: + pass + except Exception as e: # прокси не должен ронять дашборд + try: + self._json({"error": f"ws proxy: {e}"}, code=502) + except Exception: + pass + # ── routes ── def do_GET(self): route = self.path.partition("?")[0] @@ -572,6 +712,22 @@ class _Handler(BaseHTTPRequestHandler): return self._json(build_agent(lvl)) except Exception as e: return self._json({"error": str(e)[:500]}, code=503) + if route == "/roy": + # страница-обёртка ttyd/Claude Code на агенте Roy + # (t-261008084337-3afbca): UI в дашборде, терминал — через /roy/ws + if not self._authorized(): + return self._forbidden() + try: + with open(_ROY_INDEX, encoding="utf-8") as f: + return self._html(f.read()) + except OSError as e: + return self._json({"error": f"roy.html not found: {e}"}, code=500) + if route.startswith("/roy/ws"): + # прокси WebSocket → ttyd Roy (178.104.52.234:7681): пробрасываем + # заголовки апгрейда, буферизуем фреймы как есть (Opcode 0-2/A). + if not self._authorized(): + return self._forbidden() + return self._roy_ws_proxy() if route == "/api/health": return self._json({"ok": True, "ts": _utcnow()}) if route in ("/", "/index.html"): diff --git a/harness/es.py b/harness/es.py index b9ee3fe..97e99d6 100644 --- a/harness/es.py +++ b/harness/es.py @@ -295,6 +295,7 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict) version = (row["v"] or 0) + 1 ts = datetime.now(timezone.utc).isoformat() data_str = json.dumps(data, ensure_ascii=False) + sig_f = {} try: from . import audit as _audit prev = db.execute( @@ -302,6 +303,20 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict) sig_f = _audit.sign_event_row(aggregate, aggregate_id, event_type, data_str, ts, version, prev["sig"] if prev else None) + # t-261008084600 (rogue-key guard): стор с чужими подписями не подпишем + # своим ключом. Инциденты 06.10/08.10: cli из /tmp-копии репо наследовал + # SIMBIOT_HOME прод-стора, не находил рядом свой .audit-ключ, генерил + # НОВЫЙ и вписывал его подпись в прод-стор → run_projections падал + # «стор скомпрометирован» каждую секунду. Правило: если стор уже подписан, + # key_id нового ключа обязан совпадать с key_id существующих событий; + # иначе пишем без подписи (legacy-граница, проекции живы). + if sig_f.get("key_id"): + dom = db.execute( + "SELECT key_id FROM events WHERE key_id IS NOT NULL " + "ORDER BY id DESC LIMIT 1").fetchone() + if dom and dom["key_id"] and dom["key_id"] != sig_f["key_id"]: + sig_f = {} + # чужой ключ → без подписи; верификатор пометит legacy except Exception: sig_f = {} # без подписи (legacy) — аудит отразит, харнес не падает try: diff --git a/harness/roy.html b/harness/roy.html new file mode 100644 index 0000000..931c546 --- /dev/null +++ b/harness/roy.html @@ -0,0 +1,83 @@ + + + + + +Simbiot · Roy — Claude Code (ttyd) + + + + +
Roy · 178.104.52.234 · Claude Code (ttyd) + подключение…
+
+ + + + + diff --git a/out/dashboard.log b/out/dashboard.log new file mode 100644 index 0000000..4a046c7 --- /dev/null +++ b/out/dashboard.log @@ -0,0 +1,5 @@ +[dash] http://0.0.0.0:8765/ (health 127.0.0.1:8766/api/health) refresh=5s token=off +[dash] 127.0.0.1 "GET /api/health HTTP/1.1" 200 - +[dash] 185.203.154.209 "GET /api/state?t=1791449512284 HTTP/1.1" 200 - +[dash] 185.203.154.209 "GET /api/state?t=1791449517287 HTTP/1.1" 200 - +[dash] 185.203.154.209 "GET /api/state?t=1791449522283 HTTP/1.1" 200 - diff --git a/scripts/deploy-claude-code.sh b/scripts/deploy-claude-code.sh new file mode 100755 index 0000000..7989d66 --- /dev/null +++ b/scripts/deploy-claude-code.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +# ============================================================================ +# deploy-claude-code.sh — развернуть Claude Code + ttyd web-TUI на любом сервере +# ============================================================================ +# Воспроизводит боевую развёртку Лунара (t-261005155643: 2.28.121.212, user +# lunar, Claude Code 2.1.289 + ttyd 1.7.7, systemd-юнит ttyd-claude.service): +# 1) node/npm (если нет: apt → nodesource 22.x, иначе статический tarball) +# 2) npm i -g @anthropic-ai/claude-code +# 3) ttyd (бинарник с github-releases под архитектуру) +# 4) OS-юзер (если нет), systemd-юнит ttyd-claude.service +# 5) daemon-reload + enable --now + финальная проверка версий и здоровья +# +# Использование (root или sudo): +# bash scripts/deploy-claude-code.sh # дефолты: :7681, юзер claude, токен сгенерится +# bash scripts/deploy-claude-code.sh --port 7681 --user lunar --token MySecret +# bash scripts/deploy-claude-code.sh --dry-run --port 9000 --user bob --token x +# bash scripts/deploy-claude-code.sh --check # только здоровье, без установки +# +# Опции: +# --port N порт ttyd (по умолчанию 7681, как на Лунаре) +# --user NAME OS-юзер сервиса И логин basic-auth (по умолчанию claude) +# --token SECRET пароль basic-auth (по умолчанию: openssl rand -hex 12) +# --service NAME имя systemd-юнита (по умолчанию ttyd-claude) +# --ttyd-version V версия ttyd (по умолчанию 1.7.7) +# --node-version V версия node для tarball-фолбэка (по умолчанию 22.14.0) +# --claude-version V пин версии claude-code (по умолчанию latest) +# --interface IF на чём слушать (по умолчанию 0.0.0.0) +# --env K=V доп. Environment= в юнит (повторяемый), например +# --env ANTHROPIC_BASE_URL=https://api.z.ai +# --readonly терминал только для чтения (без -W) +# --dry-run показать план, ничего не делать (root не нужен) +# --check только проверка здоровья (версии + HTTP + сервис) +# -h | --help эта справка +# +# Безопасность: веб-терминнал закрыт basic-auth (-c user:token) — как на Лунаре +# (логин lunar / Zm9yZXZ0b25lClaude01). Сервис работает от непривилегированного +# юзера (User=), никакого root-shell в браузере. +# ============================================================================ +set -euo pipefail + +PORT="7681" +USER_NAME="claude" +TOKEN="" +SERVICE="ttyd-claude" +TTYD_VERSION="1.7.7" +NODE_VERSION="22.14.0" +CLAUDE_VERSION="" +INTERFACE="0.0.0.0" +READONLY=0 +DRY_RUN=0 +CHECK_ONLY=0 +UNIT_ENV=() + +log() { printf '[deploy-claude-code] %s\n' "$*"; } +die() { printf '[deploy-claude-code] ОШИБКА: %s\n' "$*" >&2; exit 2; } +have() { command -v "$1" >/dev/null 2>&1; } + +usage() { sed -n '2,37p' "$0" | sed 's/^# \{0,1\}//'; } + +# ── парсинг аргументов ────────────────────────────────────────────────────── +while [ $# -gt 0 ]; do + case "$1" in + --port) PORT="${2:-}"; shift 2 ;; + --user) USER_NAME="${2:-}"; shift 2 ;; + --token) TOKEN="${2:-}"; shift 2 ;; + --service) SERVICE="${2:-}"; shift 2 ;; + --ttyd-version) TTYD_VERSION="${2:-}"; shift 2 ;; + --node-version) NODE_VERSION="${2:-}"; shift 2 ;; + --claude-version) CLAUDE_VERSION="${2:-}"; shift 2 ;; + --interface) INTERFACE="${2:-}"; shift 2 ;; + --env) UNIT_ENV+=("${2:-}"); shift 2 ;; + --readonly) READONLY=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + --check) CHECK_ONLY=1; shift ;; + -h|--help) usage; exit 0 ;; + --port=*|--user=*|--token=*|--service=*|--ttyd-version=*|--node-version=*|\ + --claude-version=*|--interface=*|--env=*) + # форма --opt=value + opt="${1%%=*}"; val="${1#*=}" + case "$opt" in + --port) PORT="$val" ;; --user) USER_NAME="$val" ;; + --token) TOKEN="$val" ;; --service) SERVICE="$val" ;; + --ttyd-version) TTYD_VERSION="$val" ;; --node-version) NODE_VERSION="$val" ;; + --claude-version) CLAUDE_VERSION="$val" ;; --interface) INTERFACE="$val" ;; + --env) UNIT_ENV+=("$val") ;; + esac; shift ;; + *) usage >&2; die "неизвестная опция: $1" ;; + esac +done + +# ── валидация ──────────────────────────────────────────────────────────────── +[[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] \ + || die "--port должен быть числом 1..65535 (получили: '$PORT')" +[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \ + || die "--user должен быть валидным unix-логином (a-z, 0-9, _, -; получили: '$USER_NAME')" +[[ "$SERVICE" =~ ^[a-zA-Z0-9_.@-]+$ ]] || die "--service: некорректное имя юнита '$SERVICE'" +case "$TOKEN" in + *:*) die "--token не может содержать ':' (ломает -c user:token)" ;; + *' '*) die "--token не может содержать пробелы" ;; +esac +if [ "${#UNIT_ENV[@]}" -gt 0 ]; then + for e in "${UNIT_ENV[@]}"; do + [[ "$e" =~ ^[A-Za-z_][A-Za-z0-9_]*=.+$ ]] || die "--env ожидает KEY=VALUE (получили: '$e')" + done +fi + +# токен: явный или сгенерированный (в dry-run без openssl — плейсхолдер) +if [ -z "$TOKEN" ]; then + if have openssl; then TOKEN="$(openssl rand -hex 12)"; else TOKEN="CHANGE-ME-$(date +%s)"; fi +fi + +RUN() { # RUN 'описание' cmd args... — единая точка для dry-run + local desc="$1"; shift + if [ "$DRY_RUN" -eq 1 ]; then + printf '[dry] %s\n -> %s\n' "$desc" "$*" + else + log "$desc" + "$@" + fi +} + +# ── --check: только здоровье ───────────────────────────────────────────────── +health_check() { + local fail=0 + if have claude; then + printf 'claude : %s\n' "$(claude --version 2>&1 | head -1)" + else + printf 'claude : НЕ УСТАНОВЛЕН\n'; fail=1 + fi + if have ttyd; then + printf 'ttyd : %s\n' "$(ttyd --version 2>&1 | head -1)" + else + printf 'ttyd : НЕ УСТАНОВЛЕН\n'; fail=1 + fi + if [ "$(id -u)" -eq 0 ] && have systemctl; then + if systemctl is-active --quiet "$SERVICE.service"; then + printf 'service: %s active\n' "$SERVICE" + else + printf 'service: %s НЕ активен\n' "$SERVICE"; fail=1 + fi + fi + local code + code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)" + [ -n "$code" ] || code=000 + # 200 = отвечает; 401 = basic-auth требует логин — тоже «жив» + if [ "$code" = "200" ] || [ "$code" = "401" ]; then + printf 'http : :%s -> %s (жив)\n' "$PORT" "$code" + else + printf 'http : :%s -> %s (НЕ отвечает)\n' "$PORT" "$code"; fail=1 + fi + [ "$fail" -eq 0 ] && log 'здоровье: OK' || log 'здоровье: есть проблемы' + return "$fail" +} + +if [ "$CHECK_ONLY" -eq 1 ]; then + health_check || exit 1 + exit 0 +fi + +# ── предварительные проверки окружения ─────────────────────────────────────── +[ "$(id -u)" -eq 0 ] || [ "$DRY_RUN" -eq 1 ] || die "запусти от root (sudo bash $0 ...)" +have curl || die "нужен curl (apt-get install -y curl)" +if [ "$DRY_RUN" -eq 0 ]; then + have systemctl || die "нужен systemd (systemctl не найден)" +fi + +log "план: порт=$PORT user=$USER_NAME service=$SERVICE ttyd=$TTYD_VERSION readonly=$READONLY" +[ "$DRY_RUN" -eq 1 ] && log 'режим DRY-RUN: ничего не устанавливается' + +# ── 1) node/npm ────────────────────────────────────────────────────────────── +node_major() { node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1; } +node_ok() { + have node && have npm && [ "$(node_major)" -ge 18 ] 2>/dev/null +} +if node_ok; then + log "node $(node -v) / npm $(npm -v) уже есть — пропускаю" +else + if have apt-get; then + # путь Лунара: nodesource setup_22.x + apt + RUN 'устанавливаю node 22 (nodesource + apt)' \ + bash -c 'set -e; curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/n.sh && bash /tmp/n.sh >/dev/null 2>&1; apt-get install -y nodejs >/dev/null 2>&1 || apt-get install -y nodejs' + else + # фолбэк: статический tarball с nodejs.org (любой дистрибутив) + NARCH=''; case "$(uname -m)" in + x86_64) NARCH='x64' ;; + aarch64) NARCH='arm64' ;; + armv7l) NARCH='armv7l' ;; + *) die "неподдерживаемая архитектура для node-tarball: $(uname -m)" ;; + esac + RUN "устанавливаю node $NODE_VERSION (tarball linux-$NARCH в /usr/local)" \ + bash -c "set -e; curl -fsSL -o /tmp/node.tar.xz https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$NARCH.tar.xz && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1" + fi + [ "$DRY_RUN" -eq 1 ] || { node_ok || die 'node не установился (см. вывод выше)'; } +fi + +# ── 2) Claude Code ─────────────────────────────────────────────────────────── +if have claude && [ -z "$CLAUDE_VERSION" ] && [ "$DRY_RUN" -eq 0 ]; then + log "claude уже установлен: $(claude --version 2>&1 | head -1) — обновляю до latest" +fi +if [ -n "$CLAUDE_VERSION" ]; then + RUN "npm i -g @anthropic-ai/claude-code@$CLAUDE_VERSION" npm install -g "@anthropic-ai/claude-code@$CLAUDE_VERSION" +else + RUN 'npm i -g @anthropic-ai/claude-code (latest)' npm install -g @anthropic-ai/claude-code +fi +if [ "$DRY_RUN" -eq 0 ]; then + have claude || ln -sf "$(npm prefix -g)/bin/claude" /usr/local/bin/claude 2>/dev/null || true + have claude || die 'claude не найден после npm i' + log "claude: $(claude --version 2>&1 | head -1)" +fi + +# ── 3) ttyd ────────────────────────────────────────────────────────────────── +TARCH=''; case "$(uname -m)" in + x86_64) TARCH='x86_64' ;; + aarch64) TARCH='aarch64' ;; + armv7l|armv6l) TARCH='arm' ;; + i686) TARCH='i686' ;; + *) die "неподдерживаемая архитектура для ttyd: $(uname -m)" ;; +esac +if have ttyd && [ "$DRY_RUN" -eq 0 ]; then + log "ttyd уже есть: $(ttyd --version 2>&1 | head -1) — пропускаю скачивание" +else + RUN "ставлю ttyd $TTYD_VERSION ($TARCH) в /usr/local/bin" \ + bash -c "set -e; curl -fsSL -o /usr/local/bin/ttyd https://github.com/tsl0922/ttyd/releases/download/$TTYD_VERSION/ttyd.$TARCH && chmod +x /usr/local/bin/ttyd" +fi + +# ── 4) OS-юзер и systemd-юнит ──────────────────────────────────────────────── +if [ "$DRY_RUN" -eq 0 ]; then + if ! id -u "$USER_NAME" >/dev/null 2>&1; then + log "создаю юзера $USER_NAME (без root-shell в веб-терминале)" + useradd -m -s /bin/bash "$USER_NAME" + fi +fi + +WRITABLE='-W'; [ "$READONLY" -eq 1 ] && WRITABLE='' +UNIT_PATH="/etc/systemd/system/${SERVICE}.service" +ENV_LINES="Environment=HOME=/home/${USER_NAME} +Environment=PATH=/usr/local/bin:/usr/bin:/bin" +if [ ${#UNIT_ENV[@]} -gt 0 ]; then + for e in "${UNIT_ENV[@]}"; do ENV_LINES+=" +Environment=$e"; done +fi +UNIT_CONTENT="[Unit] +Description=ttyd web terminal -> Claude Code (deploy-claude-code.sh) +After=network-online.target +Wants=network-online.target + +[Service] +User=${USER_NAME} +${ENV_LINES} +ExecStart=/usr/local/bin/ttyd ${WRITABLE} -c ${USER_NAME}:${TOKEN} -p ${PORT} -i ${INTERFACE} claude +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +" +if [ "$DRY_RUN" -eq 1 ]; then + printf '[dry] пишу %s:\n%s\n' "$UNIT_PATH" "$UNIT_CONTENT" +else + CHANGED=1 + if [ -f "$UNIT_PATH" ] && [ "$(cat "$UNIT_PATH")" = "$UNIT_CONTENT" ]; then + CHANGED=0; log 'юнит не изменился — перезапуск не нужен' + fi + printf '%s\n' "$UNIT_CONTENT" > "$UNIT_PATH" + systemctl daemon-reload + systemctl enable --now "$SERVICE.service" >/dev/null 2>&1 || true + if [ "$CHANGED" -eq 1 ] || ! systemctl is-active --quiet "$SERVICE.service"; then + systemctl restart "$SERVICE.service" + fi + sleep 1 +fi + +# ── 5) финальная проверка ──────────────────────────────────────────────────── +if [ "$DRY_RUN" -eq 1 ]; then + log 'dry-run завершён: план выше, запускай без --dry-run' + exit 0 +fi +IP="$(hostname -I 2>/dev/null | awk '{print $1}')" +[ -n "$IP" ] || IP='' +log '─' +health_check || die 'финальная проверка не прошла (см. выше)' + +log '─' +log "ГОТОВО: Claude Code + ttyd web-TUI" +log " URL : http://${IP}:${PORT}" +log " логин : ${USER_NAME}" +log " пароль : ${TOKEN}" +log " сервис : systemctl status ${SERVICE}" +log " первый вход: открой URL, пройди авторизацию claude (или передай --env ANTHROPIC_BASE_URL=... при развёртке)"