es: rogue-key guard — чужой audit-ключ пишет legacy, стор не ломается (инциденты 06.10/08.10)
This commit is contained in:
parent
665db4343a
commit
336d47f978
11 changed files with 959 additions and 0 deletions
168
Studio_Participation_Agreement_2026-09.pdf
Normal file
168
Studio_Participation_Agreement_2026-09.pdf
Normal file
|
|
@ -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
|
||||||
|
Gatm<?#SK-&q05P(_h2!mY.e,bHd@\ng7*D^eK5e#=XdIJX0u=)d.8S.Tu5D^I(o)/Usk._4j@+>P)msbaC7Co"`4/T(t<N!rP65M]30h+!rf5677H.&-e=/5<kM_5/^4J4E;IWHA#qt?SU/f]ZQrj7ueD+!<f+VHJKYAC^I3UR8`T=_;l,or)`pPQjk1h7M@/LD!os?7S]3CM$L<?F5gM>bZkN2;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(7AoLIW<!\XOI6-hq)e(#Jn#0G&-e/0kZe+sMRgDam?Uhs4A8j)d&'ih,'+.Zq%k5_iO:"TO*Ot,:E]PR.OR.[te2\I7'I>ga2#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@;<B;68%ZHacl=Dq1o(,&$oD/&a8<WT?,oVEtZM4F;:>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.<oCHV_mY!_;IRqf=&^%0Q0kHD-q:p&NL"94VM0-]f$9/S[0"O"ZQr7&Lem_,8s-i$Br,+$WtcGnGWf)ZXn;.U,nlaHC4Y6c(U+F9kOaoc%P:jBMn_l4%44kB,>+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?J<VX=s%oj?U*:'?7Oi!X+L1&Z@;c:maGAS)G77G.$8\";FBh_()@7F/NV$r,qr`5#8_+[5MRuG).S?Fb&46TLR_=Z(H'*@cg#_GtUe[%M9as(,)m<tZs8_\ST9OJoKW)VKt)fk%cs/pMX#B$#"eMfp>erp*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:o<Ri):+<,MopFr`tl]Y[D\GH4JP]L]RuF^+CQ9Fs"=6c+H5%%HAnrQ6$Y]-M/oE3.Y?8Y>ZX6C%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&aO8a<gGgP^%*1Dri*O=N(1@&^>D[#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__Ee<J'ifl3dW2V2dj()Q=7XCS.EQI5\]2"dm-)g8EiK@f^9eC,Q6JoOS]Lg9V#57(p>H[%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[6<nA;CJC'NB<Yud\b@2Z?^:!UV]ZaZim4Z'gofAb;6[tk[#MEt'[lm\u9.$&eR1Yh;8UhAl/TEA,7D$%@eA1i[4%_WB+`Y_B5MXd*V7(b[HK(!Vf,aqHeLr)=NeJW=C,ClQbhUm>G^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:QW0AOeW<OV%5ZNM'&*E#7A'aKVD6X6bk2sAi6/<Neqr.K/%*kEk.D?QE18si]&oXK-&"*B5HL[IM0sNA*\==l9IWZj_\_<RQ]TF8B)(H(s/:#)E5E\AiI2G`Rqpb0JQ;=H"Kd`,<H[)O*>f+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<!o!s-gl+]aP--+BYgM$DX0d$Q./coPu3cT?PKNo0jI`t0ih+Tm*&,9Rkj?=BhL%eV5+gXd\hE%aMm-6E^$t4(O0)Oc0):3F4"*\]+^`9%&2u70NRN-&3Z,<h6/gq/$I,ojI5)&R1`\iqbh'T0A'/\!77$d6(D@lQ`bt)YDtuWJ`Q;u!VPOeB8g!9pkkicd&%;j.H6uI**8;@CK$?#k9p8Q!BO@WmbnE@,iE"UdDrq4/8,<jn[%Vf>&K"AD7Dj=bnDT+TPB'?`,0Ro_8"SQJE4"h/(+^bZ7X7d=YrdX,8Qgt7%[jQg6L2US3)<gs8?aL3rKT&R<?lre0t;X`H"d`,s$$*/hDq&:K`uh%7oMWmW]Oo)2TWdh[l+gq[#.5lf3-F4t?*1-#h'iTNFRh$OVa?mijoDe<ZOUUIOr:)qHY&U$)FU\FJqc!,aWSK>`<[>B#W(">$)OH+eqZ_%YQd"!c*_f6%DFh:P4iE,@lij[K<m.kJf!JOn5+4>ol&'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)!<Tk_h%T<`>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]^<N\cer>q^ZA(#j;OE4%1ndiY'XK%jioG6f&:.icq*OA]AK/H9ANJm)`#QTL"8s:,AQ*MLhC;4reCF9S@`f_q(a:@AqZmuL%YG<PN+=GC;skR/"FSAMBrV!No.N>TObS=,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`<dD2oLdJJ<rm34$jOiN?<dY@,NkTCf^ogXZ.66[Fd$::E'&TjMN0ecKCcH<6IlQdfp-2VVCAUACPCF"*_Y'u!03LDRH&_%+G0$amcW9kY'h<A1X>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%4G<Q6hj3<"Y+CIlLu9.h;!M@9*(%n8<Ki(sHTk]/*7gW6d7WFJ76sh)nZ3X8SSWs$#H+\-%^d[iH`V5Kb7Oa_-Wdp)NNff=MB<'gnOD,i^OR]KFu/co'f[+C2M@2M6mD!H-Z<,0*gZk'thM&1I~>endstream
|
||||||
|
endobj
|
||||||
|
17 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2659
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gatm<?'!]1(4FN4.#epp?I9gD@8-6VlqCG3AS8@(]Ig0]@2D`Xj!%LbB%UJ*kE]th_1WS-obTIHEntf^^%Yb:bf<fCIV2m)50_d,T*#j^Cqa@Em)<f?L&F&^oDO[G,\3l5E?G!_fR/?0os\8VU7g0o9G9'MdtgGtk6I2"0&*\Z=2#G$H=6>_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%`U<so2@(r7l89OBdUX<"f>95RiKj!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^I<b[Z<dVGeIoW>qE`lPEb+$FC_!`!fib<g>gjA0ZP40MFE4WhpmM,*UbpB-G]3R_4mfBt<W."qB\^9dR'AHfd%U/tZTIj$DTiq+bQC6G6W3,!D3)bpnFsj@>1[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>%<LI"!n<f-!e6f$*N/W?/N4Qe1d,a53u`hS/`Mnh^eN>$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<YmhL16E5d%=DY0L-npU/V#A8WPCr0$'TJj;lKpDUcgeaa=b2#,:*OiqsWEls3Fo49[*pL%RJX8+s!8]NQA.e+Cg-CXY1T#O3+tHmZ`i7^'L%JeI&ilAnQ^!$G.+K4Nm8a(ilE4PuIhQH\H8MR^Gu!m+g1<,S%$BVlait.*$f#hcnp2>!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.<r#p$1M-h#'0D\'?d-X9T15@ma\9+;=-0.t&?)#,^%cY=%2W3X":T-n!%:dtaJLJh[+_+.$t]F-aP]ud]B0ieMC9u+k!`^G>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,;W<C*s*`nP[B;K%c=@N5H)G#,H(cqd[d300a'U(2IUqkfM8\VS)^1p1JA;jrR>9LM$=]`PNQ9$?i7/!hHB,=boSG>"TT'LU-VBMh_i8qGF@qCoL#t`dDGm:fZ)t1oK^ZO4i]plW8)AC8]>r,B@6>p!tSO2<>(C]1-<m8tcCm;1*Qk8j)gPcX\h.&T^#dF#*83g6V%K\UY&1?:`rd6'9Kl$d=-.C^:B]E6u3Ze^sOMqui3dp1hBcE8n%C?TW&2\91TriYHi:cEt]8J2?jn-&!O\13OtqBTL/9/]VOb&g1$b:83]6AArKoX`9WHW'[0U;'0]3_o7S]1OZG,+LhZ,*t4W3O*rU)<`efIM-iD7paV7MZ[4R@U`"6&Rd8%9_s7m5X;$#9VLhA\8SiPo!t2%<k5EY'qF"6pW$H/dh^CH'=k$(#nri>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!"RY6l<i$=<J:c/8m^/fD'">jk*$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<LbbP#<?<_kf=ba%NpTphp[j;_md.k#mr0q3om(-r.34/8Jql;&giuhU:4-])*kY%bn-8k=pj_)u#liZd#5G8;i/lGSfJR:"\T@.r%lSBtC(n=L.K`aV4'f<1:OD;U6ER+2gA1Q0$CS92jQH\(EMfE+E45OuOnrKHC/"SgUE.R\.hr_!/=<K!lY=[+ul\Hp#ek';Z<>,g+F<Mc_J*ocolnr)(n7^9<cpqc+m>d8WT)_tf%KOOfV^9rE]3mY:6.]JnuZ]75`k2+"]g[eq5-3bpcjrAg<A)=rKE.*]'1-peJh\GlXq??1'YW?.i[6*4j>jd/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"7kf<D@#<9q).BV\'PNubYC/kVCc^08XW>pAA<RO&R@1HsO;BqZ3m'UBF'GBf'3jc1Z/sclJm+%Mi3cT$Sr\7!=lMq%U7(&9Il""A;QR$?0j_BE7+*//HDm^2)'Hi5*^@fW!l(/;n]A("AN*Z,tbguT5\&eiu7$L(rVfX0/`&.P5k2aBl;"\cQd@NqQK?tn>#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+<Y)3n+_GX]lY8$qZ\\(!KgJ_p;'ibn&8!dDLP!AiO+_Ban((u3QbE!f)-"o6$M%>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(Xl4<QPs"m<mXg]@e:+B)MjL.CNToIqdC'plJ%tn`u8WEr@Im[\*L2,Vjg*Rfc=g+]Ek'HMs`d'T7n)4C&^).NH,%L6A,;6#Eu8s&Bsn&AJ2-*360U$/UQ(*pB&J'"?Zp;QEhJ-LqsU&/2e5K%m3I=c.HV>Q?%/1Rp[,NWo!E1j\j_9H#%`P%9.$5hJ1/seZe'C[]F!e>M]Vp\WRJ$U[BV!(WlLi3(@)Amb^21\/1F\GHF<((R$0i<U$$nXF-+#B&ceK7EI14#IsM#CG*M^nK01'3nRU.+NcK_#"^elZ2?ZX6oSto9\./]Y_t;@,$!K)Uj$]YT$Gd\Q0GuAkRU%qoE#rtV0keJWCH\KP,Df8F`q\t4A"Q"_!^T#/4RQ-,@+-5_\=]$e[HYcW!XPUH]D5r[QU'2\#^@Zg<]D,EeTUka5*J4]k56s"mi^ZXTMM<9"oq)+S2-Q!;TMXd^5&@,eqF+kPRIFbZC]i!mY_i;8fq20.CHk\:0AD%N1K9W3^#1JaZ@,Ghm/&"G\O-3:b>B&:*: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@EDEX<u?dHlkiC:n*[1](j4Z-XbZ;iOW^45mA2QVj\+]TZTn-.u?QDqgWhu1m')Dd]88s3.6790hASd+nX#7h[O/BA:@:?/&DjiDhl@dSrolILa4J@FId88l:2W6RIT[#3L6iWcQs*LRSiXUO[FiG!!W`OT]$YC9e\a$h"j[7?-m<qe\,N5\9.Q$@[e)-d.`R;*r*E>7,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:?U<s=_/K4AIY2,fNo'JCFL@W$,-.!43h[s~>endstream
|
||||||
|
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/0X<W:ffIH9b6+m!NT)qId+']$ACc5&[:5."K.3#2RKuMuZqu8p-:V-+'Z3353[[R'ZcHfrCol\VqMH]$?iRk"kcdXdi^JL>0eA`]U<Cn@'-]2+h38`9S>GM(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>E<q-^C.fGuh]t&cr<kAlD=6I29JjuHIC-jGKcWDnDF\]Fn"ds%1>gqB)L8nUMg>%g@:?C_pKRN$*d$IJ:n$E;TLFp14K&c0'ln?0M]6IT9I-c!eGP`*@@<eGs^".?#iK_rcb(#Zkq"1G!r?2$QVO2fnV&ADs>\93>@.7-4d+c-JFcICGqB_<%H+(t+VEb&1'\DlPS*5L+En7/$c<?q(5_^;fi"JCg:%2HT>"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<G6l5JuI5.Z0O5<PV=Gsls:UjHdHElWP='TOqY`HMk(i/"T8TMJ8f5sBJ4df0M)F-,OL_(kHeI/3i?M:N+U,-V0kCdF!9^08Vme-W`."7?,0mZs25gURBj&%csQBF/(7/$].h'De35dtP_E"gQ*^-.70ZM=IsiEmMXT*1L$4fJr2.B]'U5,4<D"giq/Or$9WJ3N;06lPf9pi8=cA`[79CGu85?p%cRoQQ7ia.<c[S515@FpR[/SuM27Ut@Sfe;S<X(+SC3%.hR0#[+@CQd[^Z$l_UD\eA7[!j>)+\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'93k<ZZ>OMQuAkB,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
|
||||||
|
[<e39ac81b70f6b5dd3bd1a9be5a60bf19><e39ac81b70f6b5dd3bd1a9be5a60bf19>]
|
||||||
|
% ReportLab generated PDF document -- digest (opensource)
|
||||||
|
|
||||||
|
/Info 13 0 R
|
||||||
|
/Root 12 0 R
|
||||||
|
/Size 20
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
16001
|
||||||
|
%%EOF
|
||||||
74
artifacts/studio-conditions-2026-10.html
Normal file
74
artifacts/studio-conditions-2026-10.html
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>42 Labs — условия студии и инвестиционной программы</title>
|
||||||
|
<style>
|
||||||
|
:root{--paper:#faf8f3;--ink:#15150f;--signal:#FF4F00;--dim:#6b675e;--line:#d8d3c8;--card:#f3f0e8;--ok:#1d7a3e}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif}
|
||||||
|
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
|
||||||
|
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:40px}
|
||||||
|
h1{font-size:34px;letter-spacing:-.02em;margin-bottom:8px}
|
||||||
|
.sub{color:var(--dim)}
|
||||||
|
h2{font-size:22px;margin:44px 0 14px;padding-top:18px;border-top:2px solid var(--line)}
|
||||||
|
p,li{margin-bottom:10px}
|
||||||
|
ul{padding-left:22px}
|
||||||
|
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin:16px 0}
|
||||||
|
.formula{font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--card);border-left:4px solid var(--signal);padding:12px 16px;margin:14px 0;border-radius:0 8px 8px 0}
|
||||||
|
table{width:100%;border-collapse:collapse;margin:16px 0}
|
||||||
|
th,td{border:1px solid var(--line);padding:10px 12px;text-align:left}
|
||||||
|
th{background:var(--card)}
|
||||||
|
footer{margin-top:56px;color:var(--dim);font-size:14px;border-top:2px solid var(--line);padding-top:16px}
|
||||||
|
a{color:var(--signal)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<h1>42 Labs — условия студии</h1>
|
||||||
|
<p class="sub">Участие · Инвестиционная программа · Управление. Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h2>1. Как устроено участие</h2>
|
||||||
|
<p>Набор в студию — <strong>раз в квартал</strong>. Условия участия подробно: <a href="https://42.cil.by/">42.cil.by</a>, объяснение договора: <a href="https://cil.by/static/dogovor_obiascenie.html">cil.by/static/dogovor_obiascenie.html</a>.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня.</li>
|
||||||
|
<li>Платишь большее из двух и только его: месячный взнос <strong>$999</strong> или <strong>[X]% от выручки проекта</strong>. Месяца, где платишь и то и другое, не существует.</li>
|
||||||
|
<li>Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko):</li>
|
||||||
|
</ul>
|
||||||
|
<div class="formula">Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей</div>
|
||||||
|
<ul>
|
||||||
|
<li>Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>2. Инвестиционная программа (SAFE MFN)</h2>
|
||||||
|
<p>Инвесторы вкладываются по <strong>SAFE с MFN</strong> (most favored nation). Голос инвестора определяется на момент инвестиции:</p>
|
||||||
|
<div class="formula">Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения</div>
|
||||||
|
<p>Голоса фиксируются в момент входа и дальше не переоцениваются — растёт или падает биткоин после инвестиции, число голосов не меняется.</p>
|
||||||
|
|
||||||
|
<h2>3. Квартальные уровни инвестиций студии</h2>
|
||||||
|
<p>Раз в квартал студия распределяет инвестиции по уровням:</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>Уровень</th><th>Размер инвестиции</th></tr>
|
||||||
|
<tr><td>1</td><td>$1 000</td></tr>
|
||||||
|
<tr><td>2</td><td>$10 000</td></tr>
|
||||||
|
<tr><td>3</td><td>$100 000</td></tr>
|
||||||
|
<tr><td>4</td><td>$1 000 000</td></tr>
|
||||||
|
</table>
|
||||||
|
<ul>
|
||||||
|
<li>Подаваться нужно <strong>на текущий уровень</strong> — перепрыгивать уровни нельзя.</li>
|
||||||
|
<li>Количество победителей на уровне регулирует правление. Сейчас — <strong>по 1 победителю на уровень</strong>.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Ежедневные звонки: 3 через 3</h2>
|
||||||
|
<p>В рабочие дни звонки идут по схеме «3 через 3». Время распределяется <strong>пропорционально текущим голосам инвесторов</strong>. Если у инвестора ноль голосов — ему отводится <strong>3 минуты</strong>.</p>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · <a href="https://42.cil.by/">42.cil.by</a><br>
|
||||||
|
Эта страница объясняет условия, но не заменяет договор. При расхождении действует договор.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
artifacts/studio-conditions-2026-10.md
Normal file
45
artifacts/studio-conditions-2026-10.md
Normal file
|
|
@ -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
|
||||||
43
docs/digests/hour-2026-10-06-0400.md
Normal file
43
docs/digests/hour-2026-10-06-0400.md
Normal file
|
|
@ -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-симуляции): агенты больше
|
||||||
|
не ходят вслепую, а фикс гонки гарантирует, что фича-бэклог не теряет
|
||||||
|
запросы — прямой вклад в долгосрочную базу.
|
||||||
43
docs/digests/hour-2026-10-06-0830.md
Normal file
43
docs/digests/hour-2026-10-06-0830.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Дайджест hour 2026-10-06 ~08:30 UTC
|
||||||
|
|
||||||
|
## Батч задач (1/1 закрыта)
|
||||||
|
|
||||||
|
1. **t-261006072315-18199d** [L] — Доска фича-реквестов на странице
|
||||||
|
проекта (Alex). На портале cil.by `/project/<id>` добавлен раздел
|
||||||
|
«Фича-реквесты»: агрегирует все входящие MCP `feature_request` с их
|
||||||
|
статусами. Сделано:
|
||||||
|
- миграция: `agent_feature_requests.project_id` + индекс (init_db,
|
||||||
|
применена на проде);
|
||||||
|
- API в `domains/boards.py`: `GET /api/projects/<pid>/feature-requests`
|
||||||
|
(FR проекта + общий бэклог портала `project_id=''`, голоса, can_manage)
|
||||||
|
и `PATCH .../<id>/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-базу.
|
||||||
38
docs/digests/hour-2026-10-06-1810.md
Normal file
38
docs/digests/hour-2026-10-06-1810.md
Normal file
|
|
@ -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-инцидент закрыт без потери данных.
|
||||||
|
|
@ -28,6 +28,10 @@ from harness import commands, es # noqa: E402
|
||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
_INDEX = os.path.join(_HERE, "dashboard.html")
|
_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")
|
_ADDR = os.environ.get("SIMBIOT_DASH_ADDR", "0.0.0.0")
|
||||||
_PORT = int(os.environ.get("SIMBIOT_DASH_PORT", "8765"))
|
_PORT = int(os.environ.get("SIMBIOT_DASH_PORT", "8765"))
|
||||||
|
|
@ -541,6 +545,142 @@ class _Handler(BaseHTTPRequestHandler):
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
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 ──
|
# ── routes ──
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
route = self.path.partition("?")[0]
|
route = self.path.partition("?")[0]
|
||||||
|
|
@ -572,6 +712,22 @@ class _Handler(BaseHTTPRequestHandler):
|
||||||
return self._json(build_agent(lvl))
|
return self._json(build_agent(lvl))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self._json({"error": str(e)[:500]}, code=503)
|
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":
|
if route == "/api/health":
|
||||||
return self._json({"ok": True, "ts": _utcnow()})
|
return self._json({"ok": True, "ts": _utcnow()})
|
||||||
if route in ("/", "/index.html"):
|
if route in ("/", "/index.html"):
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,7 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict)
|
||||||
version = (row["v"] or 0) + 1
|
version = (row["v"] or 0) + 1
|
||||||
ts = datetime.now(timezone.utc).isoformat()
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
data_str = json.dumps(data, ensure_ascii=False)
|
data_str = json.dumps(data, ensure_ascii=False)
|
||||||
|
sig_f = {}
|
||||||
try:
|
try:
|
||||||
from . import audit as _audit
|
from . import audit as _audit
|
||||||
prev = db.execute(
|
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,
|
sig_f = _audit.sign_event_row(aggregate, aggregate_id, event_type,
|
||||||
data_str, ts, version,
|
data_str, ts, version,
|
||||||
prev["sig"] if prev else None)
|
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:
|
except Exception:
|
||||||
sig_f = {} # без подписи (legacy) — аудит отразит, харнес не падает
|
sig_f = {} # без подписи (legacy) — аудит отразит, харнес не падает
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
83
harness/roy.html
Normal file
83
harness/roy.html
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simbiot · Roy — Claude Code (ttyd)</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
||||||
|
<style>
|
||||||
|
html,body{margin:0;height:100%;background:#161b22;color:#e6edf3;font-family:system-ui,sans-serif}
|
||||||
|
#bar{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d}
|
||||||
|
#bar b{font-size:14px}
|
||||||
|
#st{margin-left:auto;font-size:12px;color:#8b949e}
|
||||||
|
#st.ok{color:#3fb950}#st.err{color:#f85149}
|
||||||
|
#term{padding:6px;height:calc(100% - 42px);box-sizing:border-box}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="bar"><b>Roy · 178.104.52.234 · Claude Code (ttyd)</b>
|
||||||
|
<span id="st">подключение…</span></div>
|
||||||
|
<div id="term"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
const term = new Terminal({cursorBlink:true, fontSize:14, theme:{background:'#161b22'}});
|
||||||
|
const fit = new FitAddon.FitAddon();
|
||||||
|
term.loadAddon(fit);
|
||||||
|
term.open(document.getElementById('term'));
|
||||||
|
fit.fit();
|
||||||
|
addEventListener('resize', () => fit.fit());
|
||||||
|
|
||||||
|
// токен — как у дашборда: из cookie или query этой страницы
|
||||||
|
let token = '';
|
||||||
|
const q = new URLSearchParams(location.search).get('token');
|
||||||
|
if (q) { token = q; document.cookie = 'simbiot_token=' + encodeURIComponent(q); }
|
||||||
|
else { const m = document.cookie.match(/(?:^|;\s*)simbiot_token=([^;]+)/); if (m) token = decodeURIComponent(m[1]); }
|
||||||
|
const wsUrl = (location.protocol==='https:'?'wss://':'ws://') + location.host
|
||||||
|
+ '/roy/ws' + (token ? '?token=' + encodeURIComponent(token) : '');
|
||||||
|
const st = document.getElementById('st');
|
||||||
|
|
||||||
|
// ttyd-протокол: первый JSON-фрейм {AuthToken:'', Columns:, Rows:}
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
let initSent = false;
|
||||||
|
ws.onopen = () => {
|
||||||
|
ws.send(JSON.stringify({AuthToken:'', Columns:term.cols, Rows:term.rows}));
|
||||||
|
initSent = true;
|
||||||
|
st.textContent = 'онлайн'; st.className = 'ok';
|
||||||
|
term.focus();
|
||||||
|
};
|
||||||
|
ws.onmessage = ev => {
|
||||||
|
const d = new Uint8Array(ev.data instanceof ArrayBuffer ? ev.data : new TextEncoder().encode(ev.data));
|
||||||
|
if (!d.length) return;
|
||||||
|
if (d[0] === 0x00) { // OUTPUT: печатаем как есть
|
||||||
|
term.write(new Uint8Array(d.buffer, 1));
|
||||||
|
} else if (d[0] === 0x01) { // PING
|
||||||
|
const p = new Uint8Array(2); p[0] = 0x02; ws.send(p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
st.textContent = initSent ? 'связь потеряна — обновите страницу' : 'нет доступа (токен?)';
|
||||||
|
st.className = 'err';
|
||||||
|
};
|
||||||
|
term.onData(data => {
|
||||||
|
// INPUT: 0x00 + utf-8
|
||||||
|
const p = new Uint8Array(1 + data.length);
|
||||||
|
p[0] = 0x00;
|
||||||
|
for (let i = 0; i < data.length; i++) p[1+i] = data.charCodeAt(i) & 0xFF;
|
||||||
|
ws.send(p);
|
||||||
|
});
|
||||||
|
// resize: WINDOW_RESIZE 0x02 + json {Columns,Rows}
|
||||||
|
term.onResize(({cols, rows}) => {
|
||||||
|
if (ws.readyState !== 1) return;
|
||||||
|
const j = JSON.stringify({Columns:cols, Rows:rows});
|
||||||
|
const p = new Uint8Array(1 + j.length);
|
||||||
|
p[0] = 0x02;
|
||||||
|
for (let i = 0; i < j.length; i++) p[1+i] = j.charCodeAt(i);
|
||||||
|
ws.send(p);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
out/dashboard.log
Normal file
5
out/dashboard.log
Normal file
|
|
@ -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 -
|
||||||
289
scripts/deploy-claude-code.sh
Executable file
289
scripts/deploy-claude-code.sh
Executable file
|
|
@ -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='<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=... при развёртке)"
|
||||||
Loading…
Reference in a new issue