Add shared nginx-proxy role; deploy Hermes in Docker; rename Paperclip

nginx-proxy (new role):
- Shared nginx + certbot under /opt/nginx, replacing the copies that
  lived inside the Paperclip compose project
- Apps publish via tasks_from: add_vhost (cert marker -> issue cert ->
  write vhost -> nginx -t -> reload); a vhost that fails nginx -t is
  rolled back
- Certs issued one per domain via Cloudflare DNS challenge; domains
  registered as marker files in cert-domains/; optional LE staging
- Stock nginx.conf plus conf.d (directory mounts, not single-file
  mounts); catch-all server drops unknown Host/SNI
- Containers renamed to nginx and certbot

hermes_docker (new role):
- Official Hermes image, host networking, gateway and dashboard bound
  to loopback and published through nginx-proxy
- Dashboard login via basic-auth provider, secrets from the vault
- Main model set from hermes_model (fantasmic endpoint, key from vault);
  config.yaml overrides are merged from a 0600 staged file that is
  removed afterwards, so the key never appears in a command line
- Data dir owner-only (HERMES_HOME_MODE=0700)

paperclip-docker:
- Remove nginx, certbot and docker-compose download tasks; deploy with
  community.docker.docker_compose_v2 (idempotent, remove_orphans)
- Stop passing the Cloudflare token and certbot email to the Paperclip
  container via env_file
- Bind Postgres to 127.0.0.1 (host networking exposed it on all
  interfaces); stop chowning pgdata, which the postgres image owns
- Handler recreates the paperclip service so .env changes apply
- Domain m-v.wdw-labs.cloud -> cranium-command.wdw-labs.cloud

Playbooks:
- Add day1_deploy_nginx_proxy.yml and day1_deploy_hermes_docker.yml
- deploy_paperclip_stack.yml: run nginx-proxy before paperclip-docker,
  CLI-based container checks (no Docker SDK dependency)
- Remove deploy-hermes.yml

Host vars: add nginx_proxy_certbot_email and nginx_proxy_certbot_staging
(staging stays on until the switch to real certs).
This commit is contained in:
rblundon committed 2026-09-30 23:12:40 -05:00
1 parent 5b48536c32
commit 881e2e24a8
37 files changed
+1683 -755

No files matched your search

+394 -377
View File
@@ -1,378 +1,395 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
64333032613165306433376335663162366661653234633037376532636563613831366365363533 63373230343939633131333966383630303363656234366536633839376631323762656431323263
3566386139613261313236313831396138663733613462390a383931656539636132373135343031 3938623536303533306430373064316238633132323063380a646236643461373162316336366135
62396234326534363466396365333265353030393064383036373662623730616634333236363437 33343834383962313838646461333935303834613162613633613034623563653333373431343562
6338623363663661610a306332393733633837636330653938643230613937353535303033643033 3565613135316434360a393239643065376566656332333562393737316432356633303032653131
35616632353337626561623064366263326339386335343833366437386230383163386666303939 34346263653264623566333561363339346232373432373733346137376638643261663739623138
63626530383463326261353034613862373861643664356439306233303430366264626231373536 38346634306362343761306530636138343530663563636135336431346135653464633564306564
36303862356364626164646432356563353335386162623539646632386638393234643638353536 61663732353231313637333161616466613663363037313930646334346162626332643332623461
37656438343634356530626336653532343762666436383461646132353561353437343963323662 35333638313261343435323638646565653236636231346230383136356538653865356563643661
64353435643761396336353338653066363566663632383466393362643861336534646632383064 34313066306634396133376165323761333835323365663031653864373538353535643965366631
62303430653732303666663264333439633462656431636361386637376237343831353662613538 62363831616664323735333236323734663361386365646636373030363639646666343764376638
38396138386631656431363761616234306664326265633432396638333938646164306334376636 64623633383833653339636639666162303038313564653831303362306338623831303737343539
66623764613361336338646262636434633038353134373166306433323361306563323834356330 39376630383937666531636530383137393264636464383331623563373764313232653839373938
39313030663764313137626535366561663365346536663034346163323932386232393364383939 64373865646665646363636339613536616437373234626630636261653038663936643631383733
37613964323462313565663062346131323762323163626338346638346331616638386633636138 36666137616631323338316338396364303965366639336562383133343131346162396362393030
36313938663939663734323433643261363130623362663838666564653663643263343139646361 36363436373462643665343561336363653764393733303834643935616662633238343136646337
34303031646432323361366165616463646637663664356565636230326531653233333365383339 36656233636362653863643335316462636138383665333932663164366362386432333933633664
33653735316132613236383131323032353934626265633338663131393638396335376236353836 33346638323831663266636334333530356562383463316163303639363233353031626366346231
37366534336635366661373731313433353561363835613631383033656163373063306139323137 32393865336265613562386166646437346630666432393237616565373563633131656562343266
37653766376230393434346137396537613733336134363031383332313930373839313235393663 39373265393235646462333663656263633862323036373334323831633762373831333536323836
65326562383966313737626466663461326532636163363331306265633038313539303434316532 64653035366163396138653866363765633865653637313336303064656131633531303331363432
65333065636531303261343164303862333031386361626431326363363261313635333430383530 31613532336336393232613437666235633964376535636135656662353730353031353662613463
37616632643566373831656532373633343332653431353465333066613063396562393961363962 65313163316137353831636633313962313631373733343839303036656338323738396364373565
39636232333532313836613930346334656563656532323562633563643766306139333034343436 64353264396465616435333138353531383934363564646238633935623035653830366163373063
66623832373936396236333063666339303934303262326166616339613663346166666531306135 36643733383536346665336339396135336333336235393961663565393166613563343235313365
33323163316131373231663935663331356362626364613965613235343139353563363234393537 34363763353939613730643137333562353130616565343536396630666265303961363638323165
32396131646234393465623832333436623731363537313235633334326530393338313933613839 65356138363864646136336665333261373333333234336237616138346332313138613233616331
31636364343961633564386561643263383031396632633436323736356233643261653637626165 31346132376237313863336464313662396164303733636162626130393236393633663630633733
63613764653931626338613033376436363565666632643130303136326464326237643564616631 35343866356230363262326436313738386334353565363265633261303031333662666564616261
38313766386362616364346236663138376439336530313132383235393532663666646663636166 38623639623431376232376566306362363734316238653630353564623961326331653464653264
32616461663935363432353536303435326338373566666637626231393335323864656331663637 38653437333830346461636630363538376137313030326132313665353065396464616135626265
34633835663338323763663835363466386237353833363061623639366537373465363464396239 31613433313866333931636535373063353865356435366132383538316265316635326233646439
31306134666266386237343363613431623864333463353330623238613636373062653833323064 33346439643262613562363861313364396537366231373464396164666635396435333132393363
36346562643662313533363134363964623565616162333331643433643863656232613935333938 62326630303135643264653935316330396664653063303934613335613133333931623262626138
61306635636633333230623265356535656431373032363237343262623438393235653734313835 63376438356539363533363134343535386165656364306233353364353364613665643436386163
35626637306463613437396235386338373763323634656665363864613361633365333738663034 37396164663630633233343130333036323730633532336136613862376134356534373237343739
37656563326131326166386232323639373365363865353763613364646635303362393564396163 34636162633436323465643165396162376364376365616236303361333330396238386461626362
64616161313864383634346437396635613431353838376663656165633438343430306230323964 31363931663834363464343866613862663037383135646130653939323566616334313461353730
30373230613839626264643562633033643236623664336234333535626363646131336330323838 61373935643962633138383832656634346563666637613434633130663665393637353230336633
32656266313066396130313865616531336461333361323436346166303432323030666366363735 37643534386136623731316665663630303634363132663636393862386666633239363937386163
38663530343833396261646230333939666435346331356437366135336437393438353930333338 61313361663862343438613835356539373462663230356166633231613064626638653532383537
38343935623466616562363431653663643465613763313230336237316265326165346238323834 66663331306639363932323530343164343666323237383631313033323461373432653063373665
62613430313965326664386636323562613262323838386438356439326531313938613031393863 65353531653565373165666666396236653038386435653666376661323164383537653936373432
38323936666335646461373936653065363139323431626662333361343039376534363433323465 32643234663161316564636333333265643661633634616235346132356138363966653430326634
61663763623266636635346666623063666537396535386135666236653336666633333130383334 62343237383936656336343139373135313830613466373837356534373666303233336564323466
36303439613565306663346164646162343764636438336138316261376130383331653266393039 37313266316461353062366536323237383165396434663861653032383033623964643965366266
64393764366437643264373361373730663262396664333964393062663433653735623637336433 31633238363865623237626131633334313139393437383163313762303264366433656562393461
36623533356361653465383737633238613234346464363565663934613632336638626238376664 65333039663437366131366137316230366261613131343866396131626464336165306431373931
36373238336432363061633730356265636639343239653436396163363061613237373333666466 36343134393238323165343631373032356332663337366464666530653665313864373463373031
34666435366634316462623662393131393138613961393030363831353832396436613966633731 65393164613637653435366164613336386138643463373433636637383433323434623539666138
37383164366661323739313361656139373662663631643835303831363938313362326337353966 65333335343033356237373439303636313639623231316638393533333838636364336235643437
37383139333532666631633035346137353432346532383866393938323936383834336231373830 61353234633938333263373866636161363166623733356533393665303632643635653131306137
64376261653132336131323562383734653366643137613433666262666337366362333335633465 65353531656465373830336163633564353665306566636232323430653363343930636131633735
31323733363332313561666438613930373862643332323931663438653666336433303034636666 35343335613531323032346263323561353636613166626533326663633133363465306431323039
36636232386462373666356461616434643861663739303032383638393739633239376664396163 30326561363764663435666262636664326438373536663464383232313636373466306661643537
36643130663963616433383235316539336133656562666134616438656532306438663739633333 30643232373636666462313730396232653865366235313237373362333135393435636632623638
39646165313566386330376166366534323236373131616332623763623131323466663365646565 61353735386430376133373333323464386463303534316333333438656635336636343130393337
30646435643737396665333066313035653361363462666639383538343833336363663264326130 33313137626239373731396438636434326133326332353965346138303934326231393066346439
66613563336266316363353131613331386533316665303034393230323937396531373336373430 35373535626134353037316362306539396234613736643132623234393962376439643134316163
66306632656163306532653131363061306362646539353837663634323662356536363366373138 65373331336364393337393339343433663136396162633263383535663239613132663862306363
38333664663938393232353235343163376634373738313661613861316432666539653938613666 63646662396635653862646137613964616662396630656366326563636135613337653661636639
35343161366638666439333633613632623562306164663934333737336362373366393335396563 36353561373035386565326666633366323334626663643366346261316365353863383037303836
61643839663738323837613736316661383332633965333464653066303065653764393034396234 35356562323966323739396335313736663763313463663465396430633066343930363961376561
31326466393135643765616165333235396466306366343934396439303337633535653033393761 61343962353239336434636634323535303534323739326239633965303065343936623433393462
64336435343634633761373733623835393136623864323861396534336634633532636162376263 37646135653239373165333435343531343166613864323238343664363137613964396130316439
66363439313032646462336664356636373563353137646334623939363231333639363838623432 63363963643330633363313631356535613761646461313632333366626237333732316537653136
61616230636265303765313436623766656635633264316438373834343737363836616466393633 30633532623132393764666338306262393562366266346134356134663661333030353333333465
37633232653266616163393531303034353932376464643265353832356466636534396239336138 36613333353932396439613030393038626333356230636638666663613634333365376561376439
61333933366662333732366437353164363761643761303032333235306365653464383639316637 39303264636335346630303032346233363238323032386362323433373439666635313861396263
66353231336139373435323264663863306135353961613437613736343364383463333031393030 62356239663734626665353061323131623032646365316338373235383139633334636466653430
37613336313933343630646562303866363130383831313835386231373462326465623466396137 66633662633430376331373233393532666663636636386432396263366238343663613136303263
33346238303433306235393538663532613662383764643264633038653934646236363338356234 64393431623535626161666165316235656233616337636663363762343665653236666636323063
34343131383465306131316538326439656338353531326539626366323161313132666337376632 36373364353566356438663764383662623331303063363866386134303930656638346463336238
30313262373261633161303737663162646130373164383030656437386363663534666639663663 33356630356536373062643533626563376331386338636638616234656535353038323065333930
35356331383831386662386535393236636639646562383534393633363836636631396364336634 34613764343832313063323934356462623065303465373739626533336465613836343738396463
35393734343338636161373864643666613530303832326561336138393536646565663230653936 32316561343236353365353163323938656364663634613235633165343461393232663335643865
63373866646533346365393261363637343839623933356466366535363034363338353130376565 34346130336539663339633266333166643937313761636364346563363263656661353639306639
32623530643032626261663864303362626461336233306164306536383565626139633164383834 36336334313732333362326432323832626235623331323734323539333631373535363964343037
62646335653234366563636262643833616165663062316363393135346361623332663164616138 34326532353636346566636139333464616466623234346336363530396136636139303830613165
64323831616265356332393535323666376233326333383139353837333762393437316265616366 38653539383039363233316437373463623130303038633233373565343632613239653630326434
39313637393236306331313364333864333530323436303539346235333430653762666633643563 30393564373462343361376636313037313961383065353864636532643337353162393661626630
63376264326432353639366165303537386535366236663537303932376533326561346533363535 37333363313730373132353330323533303635653763616263663964333763666538643864303231
30396632626137303838333263313062313261366634653134626536636538623037646238313135 37333662313837353131363962383735366531333730393534396263396130343336643138646234
66343338616337346434663735353138636132663535656534623432303661303130346230663930 33333434623966653664316663653337643266303234623837613962356635393865373331666237
66626339646463643562303432313461636632313464663833353663393166323031653364613563 64633430386534663162303363333861613338373835643837656664333364373361636338633761
36376439633731646332666539353465616230383433653765653835303566636664646162356464 64333034623063393166313336303431383261666564336132326334366639326363393832353865
62363165623639626232376231666263353838386164336464393832353265363336666531653037 39633736326238663634316663346239363935396432313336663037396536363561323538666364
34623738363037343461633035373135313630316639636231353835313534626635613566393965 63396230333066626463626331653438373365656435333761343565343064326361343537306238
37323762636164383931373163316532633065386535303538616663663536663766646363356262 66316663663963643266653132656439376665306261626665393061333962373133323264623330
34303036666631636365396637653561656237303135396136616464356561633738376339333834 62343033646463383637636463613633353465633738636465346135643738663535356238656433
36366365353832343633333435303565386566393366363831643461316134356138666131663938 34376339613566393764663264373034393065636536653837646466616563656362636231633064
38336139626562303837366235333038353739393863353161383635353339373032626238386435 36393736626462303039623932613462636635616233336337363135326564393439663262343363
64363536306266306165333135373732336434383639613865333130343532616261376363323536 64326630613563623164366337303430323233326237646634336335393664323231373237373137
38346563623161306165383962316436383631343763623336306435346636626664353434646530 39633962366331373531343065323563306662646530653535636438626538393537333033343462
32626361343635343230373834373262306164313263396366626164653235663362613765376536 33303332313135653135616464383539333261333730373362666163363335633830376131343161
62613531393866656530613165343565323839393861646337636234633964306130356263626533 62353161383664346337303464646463326464316131393266366464613333653266326161663539
61383932333866663436653638643931626262386631386438333764663134656230356630396132 32663037366433393864666266653137373331616339396634626262376462373533393761326337
64303332346366643130383664643438386664376138626136313163663961616439313165333866 66333666376563353839333432336563326462323461313635343231313964386333656636353266
63336464653531366135636434313034646230616365313063613162633435346235656565636661 35323062373131383661346431333665336237313036393837343933333631626236646438346136
34343033346261623966633262653762323037373339303962323735646366326131346334373736 63613231393266323739356566663264316536383632353134393561336266306361626532663966
35626564623330653161396232323265643966343531633732383236316666343938343730316361 38666666303632346163333066383934303765323839663839326531356363643465343363303165
32376536633331306631333865376662323966326139333130383162386564346162643833316365 39393366366237343565653831303335363162613461623063356433353565643233343134376435
35373861383033333030656261393635386535643131643237343363346133306231343531373265 35663739383135313130646663383662353561323930323366313962386333343433316236636462
32663833356234373038383634653438333864656330656531383632623439613763663935383436 63396563653161323565336433616265623462393761653935663438373235626535343066346331
62363034623263613164306463343161663937613865363134323362313165363335616364393833 30636366366434306563663036316436663465313330386231633736303562313239353736393733
62616139666337343632616565636332343064316666306366643931313639376133393735333161 35343330636539386132303464383134623063643563313435386563343261316635376135363832
31663538353662656536623038623164656630343265636466616366646334623037393636393161 33633737333930306439656563646366623461636366653366663933396163306138393765666337
30383432633433313737646565386337353063623761633931346535393662306534303732666332 35396332653563633330363037383734353965623763653731306532626238633736313361386466
65323731396262303361353163636565303531643933353535323239353136313165343763336533 38326362613161633465343437326337656363666463326164336137356435623365363034666335
39626631663238326663653530653965366662326537353038643665383361323030333532333161 63303938343631313439666538633731323732363530356634383131373766376536306665303132
61393665616661376631616533346361663266363938363964333963383633363466326166393732 30386137373137653733633866346439633530616438343638663665323132643335623563393633
35323762303137373566653433396265616431623032643734386136633137643732313930343536 65623236363333626361316533643134333864616364376664646264383436623264633462366161
37666237386665363037326236623837626331333434623731366439373963316563643564303864 33653131626335326437663537393935303937633932306636333264376137353262313237326538
36626635623162326164633233346363343333626262393531616461356333313733613165616439 31366166646536303931373464633365636666306137306666396231613465326235343162383331
35623338336335356331633034623535643762356537313861303930383130316366363061383233 33656665326439396231306538373232613535396233323361396164633233616234623339346433
37663136333133656564663962356535613333373932646239633235623239393539353137306138 61653034333437376434323034393637396563336530323961663862383334383563343033343863
63333063393066616536656630613434653264313562393464323436663435313733366631623739 66376537386330636139343831303235663962336332383365333439646136363632383838343839
63313236366136393365626634386438623062663338396539343438373162666132313034333362 33386265383336316231663665366638383138653765323666303832326537376139303835323933
65633363643966333436303636376162316363643933316131336233636562363163643062356237 66393638383335336463633235633731326139323266353636636239313335323430363066633430
35373061313139356230653230326366613665393036623634353563313530633036396461373132 37663535663837316338316562353835353231333063616635353733633939643535666236343137
39323431353239363037356264366330393066653631656163346262616334653137653232643066 31303765616435643765623837653436613733663135393630326264353464356366303866623564
61646132363331643531386463346234343038316565633536393036363564343332623238373037 32666533656466643063663165306266643164616562326432663564613733386631396664306263
33386436313437313931343438623766663863343030633465356266373661313737663965656331 33353433633461623061376232643963326463663165613065393063666437653236313836383361
65656562626236353866663038373535333262343031626663313439383137666131623833646363 39303063323065376239623736616566656532666533363630373961653662386538323130323331
30316333363939633761383933666531306337303764343061313732316436626430313866633632 62656433303166333464383065636334316138373362323838383132303237363632376131313036
36626136396362373866613436663639353532393361366230386231363530316566386261626261 35643231313338656562303034626563373130313136643463396236633337323137306435386262
30626463313139336532346166303838313333393666656432306632316636323736653639366433 31326362356133313263646230636132306536666562306364366337666163303334653065616134
34646432666266363366366561646462616365306335666437623639306539376663373138313061 32303437663730643836356461333436643239333262363733353338353831323566323438303162
63306532653666336463313731633966366630613665353533383932663730643033653465336265 34366138383730393433333662626564313538643838633630393465396565313339613864396536
62636139653631393335323238353537616664333962633632613233393634373263626333656663 61366534366530376361393930343236636630386261343839663031386532356533383433383738
30643935663432633238646335336436313738323464656535643237313866386335643932653935 37313133393130373664623737323631616138636665653431383435376338323439613737326535
32653434326365623635356134656131303663396462633962613337653837633066323865326632 34323462303639653162313431313737623239393037303431373039653136386539653138376464
30626163313731326166313035396238343239653233646461353034653562343838663433646131 33613461643132666230653932363739613930303333323739343033623936636536656139316435
34653837646431623661663364633732303934383537636133316230356136656631356166353566 33623964613833383030333937613037333331643230653633633135303561393337663435393035
35303233386535366231306535323662303761666536646664396138373265643533373161616237 37373135663461623663623634623930663937366162626134383862336336636633336138656334
39363263363065313832393237333831353836666335366365353939326363316530343665636264 31323565396531323633393534326538616338313030303230363765313637656232626665313532
35373666356262306332323361333939636438323932373566373035333337393036386532633463 33333662363062313530636661646536383531643335366561366537303263613534336537663764
34326135323165613263626632303664363333333262333466663030383364656666623466343038 37376238646464383466346130323232373736616636333939356466353830623861653664363235
35306539636536353964633365623362323662613936386463643033666234613562386366386533 33366561623531343462303462636537613939646465323965366638653936643363353164613030
65323238396532383965353663333565303134346432306135323331636636393036613031383362 38646164613330313035396466653931336237326464646532636133326234646133303161386632
61616665616264326135656338656334353064346665346464316637343731303839363165313133 37656438666663383430363834323966383633333438336138626166353339323466386566343461
63363366376234646631306665636237386133643863306330333238353366646665306262336462 62346566326638323730343839363730333736333465613933303536626132366561313231383434
61663162383165306236393238643161383963336164333134376333653462356263316366386432 31643732356530646634386230336566663461623462643036316433343334623035353036383234
30613163616336303234663835613938623162303764306335636264623230623863306637393537 62636133666431303163646133633966383030666664363630343730333634663463663034363264
34646166353538386330616235306131646531323930363664333239323533363263653562656533 63316265653034363239353434666263656463323136663132663263373130303536636266383533
65643162613539386238363564663638636132303638303436323137643164346531356632626634 66353234306430306161656164313131376163646332646435336664613764356333316666323832
30363635346435393531323165316364383261333335623262336530623231363238643964393232 39373932396530613430373130646464346463376330333566623935373433393265386663376237
30363836326461323730363534376437636161353061393030636334313430383134326563616539 37613935653933643435363932623139343738366236346434666638323437383562626133353231
30363362316237653933373339393035636634373434636535383334393263623430643535663862 66373166363138353239633134356338333032613338326633356139353232326232653963353864
34306266323630646464663266383861663533306563366539313361623532306437303865333863 33346236623333626635316239346236313765373863343830363434333738633532343730303462
39613631306437373132313930363434646339303763356366663132623565303563363239623662 63613930376566316463383936333933396566626436383062323834636362396361383738633336
38356165376362383735306439366630653962366465396265646130303735313836626332326161 63333564623363623738393835316264313630383631333638333066613963343761383338306566
39303739383837636366613963326632633764393564623162343734663131633165636530633836 32623336383933646265653538336566323430393735373063636438396539643365366134623864
39383430376666363231643538343638336336373833303534363163353630393032616666636530 66616263616663633039303861316235633934313334613333336163386364306632626232613834
30373361373761346436316132323236643236396137653030656431333863373438386566363639 65376265323437346635313330396439643536373564333231313634323837613066373964316334
35636230666138633335336266626530373762656439666133363262323932323665623434643732 64643634363138613465363037386534346164323134333138666261623234386336653664313562
64343138633539636434373363356635396238333962353433653632316230313537393336396636 34303331663939623833646662343063303865346266323430353232373339356336383338363933
35616239353838303165306232303066656131643532656433313766333066393162613635633734 32363739323330333937346334643031306166363132326363626261306436343065303339623963
30336263363439373761313961366234323432323131303539333237313764313631646462366230 33626636643865636461383261366533646139333138396232376339623066313635333963343532
31313763386262653434323734306463626364643966353064643534633031623962393533326436 37306463643263333632396334373739323665316338383763643830653965396539366432373164
66616631333833363339623664626365313465666234363739636264616538336263366237373832 37373335313137383664396563323831666535313635313737613365626639623864376163383436
34613431326433336330623637326335346330613962336263313032366234353938303238663236 36643061386661383939666630393133636134316535663934333766313837653332303531623437
36663462383566363933666532653735666566353534353330356161383166323830356336366363 39363161313936393361343265393132393636376538636338646230376564393330356534663263
63356330383538396133393162623736386135633537643037643238393166323431313239636137 30646334343961366561613661623036303535313562303666376635643431323237313132373931
33306563383838633938363130633539363731653436643131646137393661663836343830343662 32316333333237636330376166326434343438316362333464343232313232343130376533353235
64633365633862396362656565623439383139626433306331316665666534383063633932376337 64363761643136653563353232653239646238366330313939613436363465353061303033663464
33313839616166663335346331613737666135613531363464376231353835613831613233373932 35383039626166363937633232613836616164663832663938613237393666383336366231313438
35373731323334623836366435616231346630313862393861613162376161333138643931353164 34613930663536336263646637666361366235633066613236383463383464616432616563383831
35383334623966356632373665663234363830333162396431353364653431383533633664343031 39396265303364353363316132313264393735663034616561363062626530383463393761363931
66646664666565333763333266313164613362333532616130636561326164366634383137653531 38656261616635643836313264396130336538643865643636316462336663643066393835663339
38636434326561383336386134656366393163623930666430326164393430366265376639356562 32313263616138666461636431316337386530356139666139333236613964323734386566636666
36646338313137383436336665383238393034646364656462333430376538366265613736663439 64616661303562633063343464363766363132636135633463633235356161626330386364646336
39636631633339643130656433303637383161323331353939323662356337626231323937613636 30643165356237353130623534616239336632353237353263393837656664646537633766646531
66303935666235336466653131316563356536333336326132626438316338376137376337373963 36633433626336386334323039303039623062306637633539656339333233303262633033313639
35333665383661333161333563613131343863323337366633383334336332383066666238396565 62623062656431613238383737353239326633386237653438613166386333653735326661333838
38353332343761376165363363363763313431343031636561396534656262613761363665616163 65666239643435313237323032356261376563333265386133383237353034613537623131363038
36653766343539363031303238313030663637666161303036353466306536303032306639663562 36363066346135346364336234303539336638333962656165306264343438646166313962643935
36393331316434356666353762316234626364376333333966333933306636393132326464386465 39623764643034663965383865353939306136663532353932333664396165643830333030373730
32393764393064656262623935326438373039643537313562646437346232353265333733653132 30613462346536656661656534383864633664303735393035623964366632306233343165663038
37636138333237386265616234333361613835393433363631623534633533363634656165383931 64386364643630653737666666376565643639616639393462373233333765343961323631336436
61666533313562663831353639663734626630323361656237383662666637353861613862306164 32333634613136663734663763353162356337326537653766633664353163363634396131373166
65376565376336386163316666656564643333383731633562633834623464323731363562653033 66343933343862323032643537333464303836623939316130623961323037623839663639393966
33643861636533613634343938363131656636656265633365633731653538326638343165313865 32623439333836383234313135306135363434656366653964336135383765383163643962663465
38353738653830383737353433333830636339386339623435343330656530653766643732353131 36653130626362636561646661376139356565356563373435626537663930643837376530326662
32303539356533373731383564616163643637323834333133363431666538393338393235636333 61356336656235396333663362326563643039346231633261323930653162323938343562626235
36303062303138373636396335616335623862366364336365616633373966363563616439303738 36663662366534383932396630326338626333666437656264343764316339306132353265613063
31636231343463623162613734363332323330343466633561333032663435643764616164366366 62393339653630623965636331623331633733653530653164643863363637616131343036633432
36323336613136386662346164326331323034386462383862616166336361623937333638316264 33633161396331333264386431663565636334376533363735666466623830383164356366666136
65643765353734383934343435636331346561636135346434623730386566323231343262393065 63353061623564386630616134613866623532663237333432613962366537643834306364316632
30653364346633626165323434333532646664663333376362336165663431653434383865323938 34613465343532653261303664326362663938613638656532613638303566333635363263653935
30643932656633633631303136323135323562306139643838393464333563363934323233343136 62363363393539326638333965643130383139313935376436363831653837303831326634656639
33613163323135313536376132653464333730386462623131666662316139326463666433383637 31343166613030616166393638643333343264626466303363313938323966623832333330336436
66313133643464636130616333363331663165376438333636366364346539346334623534303362 35633538636437313761383530613862343033613334643465393164386431303138323239313436
30616165373836393038373430336539373732323631663666616232396666653364313138313836 38653161643261326438383132306663333235336362353439306566306563643664323335376265
33306365366564313165646230326135633039323063333231316232316132623939386563396439 32653365383863323165343366666639393538363434353061623939633262316131643465316462
30636661306531373139373061633031343165333339363035316338376165643364313862306132 30383239613439343333636663363366616263393866636466343835333335633030633062323039
32356530343166326631306234356336623464303233613064393030646362643061363338393532 62656564613065303336323064616230356564313235393030316239336162633831363336623261
34346233393739653732663336396230633066363663653064633132323235353030333539623235 31633163363937346534316136306532636464353463326166383239383736363962363465643162
38623835303931616231316639396431633634656161326431643766306566396138653364346665 33383539396364303062326666636437373761646462663338666436656362346231666136653232
64333666363739643233326566663764383362623263613538613134303461353563306435393064 39353538313663373066623935623935616461646266616231343338623462616663373837616162
65323836373434313864373331656132363839393232303963346562623630646466316635633164 36663436363532346565323363383263376231356138646563333433323561343533333566303431
64653830393361386631393336653066653032366263663337613039623935633938373232323137 36633432633039396363666634333862613835303838323733656332653031346637316233313963
38663535653963376633623133343933333739363233373734323637376433633234663439313665 65326662313564643564616535383264323137333136646662396462373134316238323537323538
36643038666536303539626436636535623762623230336130356231646665393965393930643037 39613734633631346131653336333562333831333030333639656231303261643034353430613933
61636335643531383837653336323631333961353937623334633833653431353566336366343632 34636164376165386431636334313831323733313266323666656365333062353063333839363937
36346439366535613532363630656462356435663032323931343930313235613137376465333863 65386331643732653430313138343639366437613466316438383762376164383031636232386162
30336466366135613363633136383461363336336231656531373931313134633438633439656231 39353064353466373664653437626461316563613161383461626331393363653332643062626331
64356333323737633965633633613939333064383765636465626265326338366532363562376237 31656334623061346337303963323630613065326364343834316361373364623566316630643036
64663937633433326664313331356537353365663761373032363161323334383766356339343365 30323334636137653439656235396266396434663737396466366363646664393162303563626632
37643662363266616535636365343261363537313765633031613263643561393663303339316433 35666437353665343065396536353731353532323664363430303934343933623566363031366435
66383863323633643864383134386630333763633138353562363863653666366435316364643564 65326434656465366364383932353131353938326535366662633234653038363164323563633833
35636230336437326561396536663235643130343063666165373530323732663761656131326239 66326530343339646161353766376565336366363364363231613031663966396632373539383130
32633435336233333231633133633165333961346437343833633863373839316461663661373565 63616333333736333563343232656162373861666433393462623232393535633235653237666233
39313033383562363063313464336631356630643461633065366439376636653339303362336363 38373831303862323735623461393365303063353235326166613163323162613164613162373761
66353732623761343666666230646437636230333733303961666663366163313665353132333462 31663031653835336231303432633735653538303062303030323964616439393538356138636539
38383661363431323330336564333166396331643664383937666232343962343635366432643031 61656136613835373965383837616366643931633933633733626136346536646630393537626633
65313666383962656166636433323233353634313533643232333037303566646163393865656130 64616563363034616532303936343136616662363965353739666233313936613039613235333836
33316630343535386663356665616132356137326666343134353934303336643635353463623635 63623031323430383039353939333934346638366239346364383366383966393364393165316633
31623936353433626237643136313131656666623236643362636131373636346164366562393565 32343566303766633464303164323339313130373261306564393737623465393631363231343138
34323065626565303035643463386262373164383431383865663536633362663137663437336565 30386132636232623765666462653638383330373438323834326563666330346262623366636463
35616562353363653230633534393235336363363038356139366438316664353535333761363930 39626633613436316164353861303333326564393864393662383263343662616633386565396137
66643266623862366461303735336465393039643866383764633937353835336137613865386466 64393337333634646434326264373335383762356630383633313539323737643736363262346464
63613833393636346664613737323436333666343535666436323835386561373336636135346663 35313230323132316464616332646333306239613762346439346234653437376164316335656561
35376335306266356564656234623534636561346235663137376532633439336231363563383234 38396264303936626338313232323636336430643761363432626266626233633438303535656136
36386333353133623538636464613033386238363532643837626434346235353630396636343434 62323232393438343661396536396261643466346339613866353037303732316538656633626130
61643764623133333861373636646531613633336334636261613562363766343138366563646564 35323164653337316636613163316130373137653134393633613936623530306430663435383530
37323933653639343962393430623265613033396436383432353436353338383434373137373536 65653461323833656466623938356335386235643236353662653864643564303262363937623363
66666162373166333937643735336562373637346130353262376134393233323337383765623632 30373430343933616238323434373039356439316531623562653862303735383233346134363338
64313339323866333239323763303931393766366537323833653162656139353230376361333539 32653433623162366361303565613537336462646338616337656437616363333936633433356434
37636530396136383030383235383664393566313363613461376262383865396466376264393561 62336664343063303035313233613765336539666437623635396366306434363231333633626561
34316336323264633664353132363061303538313934383439346666653438393532376563383861 32623731663465653237326232346263653163616132623035313632306630663239626339643530
62663932393431386237373237623063336237653665326664386433303538363735336565633938 36366438326261353364353433323630303631306336363564623237386130323663616334633063
34613535313364666539363133376235383765643339316131323936323064363831353236643038 33643665306138393434396135656534306639373133323866353261323262343532356439376435
36383431616333656639666365626665303631393133356563333335626535323534346539393431 38316239353865366239656432393830353563396565663838323432363333323533363232643666
30363537353362343064313364636438383939376533613765333930653264353364356234626234 35376634313338616436303131383838396166303864333165316262373538313731323563316536
32636330623737653832346362386537373032306238363466633232366364326139376234616636 65626230656339653538343838313631336131393533626265373731326536613861336335316665
34396465383338366536363337313635613531343431623038336531343634616635313664333230 37373238353837633838633136393164663937626262613035646263383639383365313865613365
61383561353664303931306631333164613637393632343337393562663864363962363966633332 37313537393332623535303038666432383432613363633462316466363436353231333830653137
62363331653835353731663835323432333434373464663063306239323765323265316231663636 66346633666132313136323965613865633031346134666335356237353261363937613637656333
38656137666664323163363639386366323736623863333131306539636436643361613137323738 65653537303965383133623161343332663962373462326232346334313064373530646331643838
66636664326661376634303136366139373235666661306130383330613662313133393632663235 63333538636561336335323665366439313936623136626465303630373962316130363461633565
64653363396431616338653938343531613530613364643861343465646132636630373366323034 30653430633733376336323537633036383537336665353735376466373762393761336535653836
31323063393437393934616634646465306438313530656465323532356537316131626131313863 33323935613065336535323034343535323531633537386337326665343935323136636464386636
32653536303066353764616235346434663063366234646266323164623962623861313865656133 37316561626664386634346435613331613363383461626333653738323130633633366462333465
33366633636433393732346465616438303730333939333162356335383732626231353062613731 36383237616363306631623066616534333866383138396435373839366565653738663039333266
66396436663639333262386235633936316431393333623861653836386164636438346565366531 66626466663834363766313165396132396334666266303665616666316236306433376536313765
35393735353235623736613936383637663931333930373036396337383335633165613464336432 65656530656539383833646430393762663066396430663838303566326465633231653039613134
62356532646439666361306534386239623634636532343434316336306663303339393537373834 62613030663037363062393161363034633831346631656262623330333032613030303138636161
63666436613034343061303166326332643061346633363136656535613931623936313339393263 62366366613038303766373530333039376266303137366365306535393238323565623633633962
30316133316431393661643336336463643932653034313637633762646338383965323032316434 61653634366633393630373834326331386230313136393966303964373964623561336431623433
32366236373634316639303339363032663135383065376239613762336238313634386535666366 30626333363338643039626661653437336536636331396132383834613264343965666134653733
38303063353062396663396130636131656333326630623339353836613432393663613261316633 30303030383832336463616164633866376332653735306436343136376131653231313733363065
36376137633036653538323337663932333831323836356336376135326635346166336534373766 61396639323539363763386236373433393032326266313534663566363064643765306134356562
36646630346363326237323135363832306566303462306365613863393735323862306537346436 35346237626439326464333330663166666466383037303135663336613139653330663039353538
35623730323465356630303536373061666337326565356630646665613430636436333663333564 63316536656162656238363235626637313835643062626163373839333462353239633634666161
65646435663238396436373161366631376137393533623131666561353037663135383165666231 35363430373464336635656361316266333132396564333331306261323531306561356537653837
33663563363639346336343666386639323833326333313137663562623236643932633030306564 33653365643864626639393864393665363739333239326639666132613334643364663431353961
62643836616533303665376566363034363334646261363565363139353031366362386433323030 34393363353234616166323536386365316462386339333334663036313938633364626236613633
61663635653961366665666666393564366531633863393632393734383137376463363933393862 34666232613061316661663132343032623435663435366539663532653262313564613938626565
64316335616162623436613365393135363664313266623237323830303632373139653763353539 63656430343165666435336461323966393565373931373837626464313261303263396161616565
34313936366666303733666566363565363933316434333231653732663335363961386634373263 65393366303739623061616633353664326362376666346166613462626536336363323164613835
31623862326462363364326334353038366538626237633138303362343532616232613837663733 33326133666166333036326162366233653664383961363239343865313231326334383837393039
65393339643831323734646439356566393033373332386333633766373539616436623464373963 32633634383430333963633661613563326566306433653233313332393235666536633734323762
64646535613436633733326335323664386262626437633862643664376131663965376237303133 33326135363231303166326632323435363233336132353666336235666136363531353865646566
38383730373738613034643637633934636536343936373932656365613339616534373263333965 30303330656665396634303330313463306566653863663335626337343562373430326633653961
37633466393734323431356239373038306131626463326636623063323930326339346239316235 30646366633330333431356566616336323732326435356336653164323238373863396465626235
66643262303936306430306465366437346333386364663266346433616136343863383236343764 65386338636132383632373764633139396265376566323037353538313835366232356366393233
65383331393739333131383763323762303766356433313334346137643963396466343466373637 32313236633831326136346232623630373039393862613435316563343966353663666364383733
66336165356564366133623664373165666235306163666666633562623437343865306665313134 65336639656438303835393135343431316130326438373237616666366163613863306264376438
33323133643066663134356337333763346162626437363763366331373865666539656462343139 37323339333731666265323138376664613065353533363638386139346131613065353663306334
33333066646363663962303936353734663930333261373331353766633335633033653038333935 66353932616139376330366364323564396664373539396366653239383538343563353832633039
31303631343663363837383962326137323432656230326439336662616233643766313366613366 31633330343235343138373365363161303966316639623961306433383037343735623932376430
30663737363466333362303338653238313739316433343336366435333366393064616462363063 64393932326431323938306536663239303130366265323965653164326532326265313761323337
62623466326133343130313836396233313236343461623634623936393562373261616237343966 35626232663834393066646263376563333635643337356639373036306538383536626363396533
64623233653835613762363334363736303332656363326263323562626430656564616563346639 63626361333861643339626634356430323537343865643535643238336366393835623130383337
63616562653964656262623134626233316234323065316464626565643561623863646464633663 63353139306338353961353732343461323964623532326161636162303038623661633436616134
32363763316130393736636662353663316466663965353233636532653364633736633033356264 62333966613432356430613961343837326661623130356232623633363865303039653832643638
63333264653162313438313531626335343938353361336563656639376365333932303563333930 31643035613561316663383934343262653661636333323963306330373133303431373433353934
31316237343836386137363635373231326563626438663764383562666336626233643934336438 34376333373166626561616532356266316666646536323963326662363335653131626661666436
36326365313033653432633430396265313834336633366161376537363235633732623036306130 66636562656431663537623339616137383161373862323362323363373634353861353035303935
38303331343230306437386466643566326561616562623038363034633139313634623564313530 33373963346164363038336164343733336265623638396239373263326435363662343232366664
64323033643965316439613938666166326638353166303163313163373635613435333036306536 62383039646463646263373463343531333938393730383531653163653930313236373337333638
36363263326666323261633831626562343537646139303031336261666539396161383235623230 33393632663439643364353734623530386239663430383263646266323833633632323238623762
31656263313130653637386332396432303534643036333761653965643632643164373637316438 63383464323230333039323838366237376334663163653463313033353664316565313263323861
39353565356166653037373237643731653735333465313831393435616139663262616135383631 39356430623364376530356233623762643163356234353764343461356238353337333634353939
37656234313838353832343035303732313663363034316536656263626265373632646438373732 66356137633464326432363433383335313938306530326635356333353732376661616465373661
36666462626537663963623361313166376365643963383339353165616337313830343136386561 30666462643339656664356431336661643164393933646531613266656134393163323534343166
62666637333765373663646534383466663830313366316133633364376537336264393062323830 33373736653333336132663038663561313563363562316363626535383834356564383535343962
65396262303937366130333232323432626561353761666662393737353765663533646633343431 63303734373735393162326336343338336238613038626231653936383264626238613535646164
33353030323365663936343433353866306235316637666262313663376430366534613364623238 65353366383134363239663738363433636238633038363735633638363837613635616337656534
66383262313731396330623564343132393962356664376565626466613038653932313534306430 38623438396430336563643266643434336130663337383835383939343461373962633731623339
62396564623664356633613030343439316235646665653034383464363736646433643463656230 65663636326534653533613436373635373864616165393362663033323361343735633863353837
34393231386464343463333238663436376435343766343162303337313763623836663533393031 66343037303437346461386531346363393965633165353062623866626663383838613861653332
30333735303531383231633839383037613834363265373436613439303266656636316139633165 62343766663237626266643166376239343334363536393166366138616363663766663136346235
32393036343861353064663163333164336539383165313431386538636439333166333266383763 34623362373364316230386465336264353666643934313165393632313337393164393165633661
38633539333766326136383962633536353432656663396638363439313137653037643135323438 30333638633432306164646163633561373564653436656330343439653062646630376536653362
37323739666366353165346235383863633765346330303263656365353962396565323966653764 39356362386639353865383730396261383364613964383665363766626639666630396237623733
39366461383563383762376637383130333738343564326231636536353331653365376463333335 33323939376633613036353066333534396163633737363864623634646366323032393635343434
35336538396335313135376138613736313264663362393333383832373766326661343765643831 38613263366331656363373239633764353230306338316635373965373837386136303237633334
38373438623530313962343065656339316131656363643564353238356639666566313264366463 33386237366166313766626133313466323635336437313363393061643533393866353336613961
39373237363539313530666461653966303633383834316463353136613265346436323133623563 31343561633562656431313131326464613439636666313537336533333935323338623538336230
39336665613764346335623133363935323434303138643033643737396535653261643261663330 39396534663065643232363339396234346335623531613437666136323531636639623739303434
31353835353232356431633930653538316331623231363364313732343061333332646439353731 34653063623139373065363438653461333239376164363462343737366330363934336665306338
34316133353564643534663464616239313836613133353938663164656661343763613433343366 38613863326362646631636536633561623034613939306434306431646261343563346664333132
63383265303533383530396138363230646539643833633439366165343630636138643439393165 62623431303537313164633333373030623630313733313138366538326361346534373634363663
66383633666433636130616364643731626461316331303563343865336566353134643466303638 65643536616637623734373863663466363066323465633862333538613935666337376530386339
31666130656666663434306136323236613162386137353233336363316566323364623135653130 32356531326661656461623434366666386134363336363766653462643031626238653063343864
34336431626139303634346439623362616464343963323335343030623835333964613863623766 30393232303534343365656161356162383735653532636538616265333062373036326164306634
65396565343438366632346162663463353230306535643461373333613833613430343937626563 30333035663434386462326230303231366430623261353439383330373566363238303666653637
65343631353035353731353766633630343936336161343066393862663536613630653261616435 61666562346631393762383565333030343263613139353565613238393337613338666364656538
61643566306633653433623538313264333565303431343938386136616230356238363164613833 66336131356333303835366539386239376332393636363339323763646138333737616433663230
32653738313465653435363138663061303965643262373135616663316166353961313135376363 34386439356236656330656234613963336536633766316232626632626338656363383638313964
63353037643266636330386230636530636633356231313636303233633665363961303163646234 33633465383833366230623563653332616233666335396530313764336662313436363764636434
32323664316465386339653730333332333631613631323634386139386230393465373063343036 33326566396538333961363836383566366231613839386338363037313934306566613235303930
37393566636135353034366138643434613463653936386162316137343230353765316433626362 65656130343733343963343930616136306632656539663665613334613931383031643661646334
64363362373465393433336361663934663965383031346162656234323630313338636331326435 63326530623639653732326463313966363231393733323936616565323462373034336334303166
32656266393035616130636438393535626263653638643432633665323565616661363339343531 34313965366231643261643161356361616431623931366133386332633735356266316530326537
62643365633532633262343966316530353966633263393432353061383134386432356665366236 36323539346330363866353764663636386435323063386535316139366634313331613366393237
36616565373166303562396464356136353938363535663638663965376662336362653263326565 33653965663737363262396135326632616536373331396431306664373962633862633666616237
30303538356530333237323935363930626664386264373937646336376532656532383234643639 35353439393863313132323533623431333564343565613633306461373738313066373836643461
39666438313064366231336235373666616231386366623638666266386238343065363034303663 38343731653433643937636337636436353237663765313735646431303834353662326431303163
32633830663662643136636536343536363566666333646331656530636635626534363964353263 37613762396639353066633636613836396135376563643064623039346434393138383166646264
39353239383539363131313331626637626661306563643930356230353137653961363566663031 62656461633366633431363831656366373934333838336463616264646636653630396334643132
39643431626164613964626231346638636132323462343933656236333964663962656534313236 63306238373230613734356536346439353738303132366265346561326534303239303039343832
61636238386335633230353565613062363834633634336331393239373932633738383131323066 63636361653837383766353136313465313164653534663862353966333339633864616239613937
61653038663631383539616438386530336462613063363137643533303837346264656630353933 61363062653833316630303536313765323232313633356363376436643230623034323264313065
62313336333137343039353166393966323838333665653834636138346630353434643138373366 66643061336536353965633038393231323965376637653131383264323463393238636365623061
30386339386439383339623934353462336166323961666432373136373863386165366364656334 33316432336134633933343566373561333339333764316633393762653238393630343462373433
33666363633763643538383030383333616465303465333630383739376635613538376564303136 31643863323430626432383536326437613639653565373665326133666666326237343039363163
35393564353739626631363932306131386535313063303638656638353338666335646131393131 33616162383764613039326438393962353563343166373330343862633366356232363765666336
63643139643935356661613735373461633239356463666331343531643264386664663531336561 36376634393034356130326634373961363730303531666666353532386433373932656332333136
61633132323637396434316535303638663336313162353738363931623461376136373438376666 37663364386262363565346566663161396661616664353035386537323230656235633866613432
63646661653063616664616635383838333038356438313066383866343966343635303565633836 61633161666364623033626438396534313931353432663435353036376433356665646266306138
36353538653438363931386261633535613066623462303262646136343461316465373961393433 36306262303934653231303631363364336632353739326338616638616335303332396664313862
33653636383062386366353837336262393465333638663633316331656632653539626363393030 34613363626438313337316632313965336331383362643266626336353264346164343036616265
61346663326233353864383063626461383966666331663438333739393065633938656236616161 65373462363732353534623239306230346532333366636530356531656232366239643335646164
62636439663363353736343431333166393161633466313230393261376133373838336333616338 30353266663833646464646165316664616564313734323131386236353535336339336635653462
32373039313135343864643661353263316335336537323436303437623637336336656664636236 38303430366538356163373331373633336634343238393638623434316663666336316163383365
63626437343864313338313336316332613464633564653837333363373632616564663161663237 65646462653765633961303438626665306139646462316630663635336630376363343337393034
31313466343163353936343631316637356339303563363261646366323333333237356139613932 35363836386365633161383463323062613663373431336139656361616333366534666666653735
64346533383138633336363134663032333565363064353864376630366235666433363532356137 38653931343961313565626233633433636632333835643936333739626365633938636463316637
63383965613636306534653836653164616638626164376166643136373566363531346437363364 34343965393235646564393762353430396133656136646565323632313035623736353730663761
33363630353535626339663234303764373565616466636263343933376532316432393030636462 61653539613837323862383136333734646134383865666465633938376661616137346564653262
36396437666331376661376261626232366337366665326537393166343630356462363766616264 34346365356663626634333032383763663434656464373764353061326265646330646238333137
35313661633162363137666434643466353961633536623762383764656336666436646162343662 64323331623838346432633964393433383239633731366365643234643832376161376430653837
37663032613935363261653165356632353730626232373934343061393633653564613133383838 31346138366466393839346231353937626431323065383837343062636664626266333734373430
66623461393163623430303139613136336161636438366337373263306564653662656639616365 37623461363062343563313930316131666165633463616438623263383764653137316461643738
38356664323437663264363431653562643136316362653235376431663836303833643737353361 64333431663439383163343438386637653537303835393930326230363166306136636663393161
33643662303336636231616233393166393638353133306263333561336261303336343639353330 32353136393663363639346338323933383032383031346237336437613061626638373330363639
35636132383734356235343237316230373766393563656663343131663561373630636438633634 31636361353233653034613366366339636564353639313038643832613261383133393134303136
36373835363437386462373466386531663231363738616564313839333864326463373036343364 66326638623238303761396262663862373161323665396266383738343330366431343562313530
34633632633832353363363162333536616332646630633433653663313332313531393363356132 31333730393634623466356532386138663939623230323531373566356431613031363236396234
62663637333765633665616466643130363234626563636563643866343033316330623862303039 65306164336537643636626461333234303266306433646566363361623234313735653930343932
39326561626331383935393662393436336239633330386261616462646631633964353862643361 63346339633864656538323631363231343930323631353231646534646339323130323237373835
63613831323831336461313936356233666532333535393963353633366431343330626233616636 30313834613334303539326338623735396537316162363861626566326662636638373931343134
64396464353563623037653163616535346437313433653332313862303363353638366539613531 61643530363465653934366639393364636161363633336636383136333363303936333333303261
66616366386265376231376436366566333137646633323730303737653834616662666538663037 34323231396365376232653733386464333364623634353361323635373935366266323130666633
64373139376332356436653733386631366165316563393536353564626633646365366562626633 36313530356236636534623661343335306330303961336138306335613836653938303966616231
36373835623937386366356136613061306630626661653764653665313462373632666236323664 37646234306530346234393439323435623164306538396237343138313131386239613834323664
33323435333062333831653434383331306333376432623330643062666130333461336630393561 31356663616630363536613037633666636163333034663132643865626434633863653463333739
39383462626632316437343166396231386138616637306563343765626533653531636665393264 66656536643731323839376239333665353935343563363436323266343863346466363663613336
38633966333633316339643265376361643366336334336633393430316432393165363239373332 62373064633264643932376265653162336661313133326438303931646135666161383165626262
38326263343637663661663963303464636561623332333438306337343430643162626366643936 65613132613737626462613038383234643838653462646364373166323064383366333933643933
66363166626665363031306161626263666531313733373833633433643130613039336539653466 65653734343638303235336561366131336630353761373866396437323864386235646665656334
32323436323439366265656236623163366432306433323839343061376464633330376538626334 63666264636238633938653535376431616539376465303964396334323435636564613562643462
32626436363463643534376634626562396437373037653962356262653939316562393233383735 33396239383132626366333466393135313834383038383361383631616336613166663062656634
38643832386238316235366439646132336136363131326665653465306265353966623761366632 65616538303165616637356464353039333765643736313761393237643933643736653436366462
63373332313766343366663137653131336339663032616435366362633638373033663762316437 63363732363864336664363637643663383732386537396231303565633036303534383464336533
66353732356166376435316135646635636361626361653338343534383562383836343763363630 63613334336138353632613764663962346263323536343532643563666332626531653464333931
61383331623534346334393065636136306339353433326436633965376363313030303661333935 38333734303230343533646238323865396434346232656664376465383938343139396534303838
38363431363430323461353339643130393164326161356662343766616631326539346131666635 65333361346234663133383837646632393939636365383966386364616433636666313466633138
35613462643731376336646338636464333935666666323532633730333162373865363333393638 38393835323263393536336439646237613033306665336133373836323539303033383461666333
35316534643330613962656665303363353434313664663932666466616630653636316666646332 38616631656430643333313634343061623934633166623262656133666135646162343363366539
38376431373363303931336437323432363432393539363461373135633839363334393232343334 35386434343931393432313061386632613632633733646437666265653130363935666239393735
64353431363838313564613839623866653964383762313632366232393131323466656431343035 35666633303132636134626365376633386531313036653536616462373164303734303836303936
66663635616131373665323166386364323637663061653734393039656232303434613865313538 66333338613861323266633339383539303238626230613663306438386333373831316135303939
35336430333739356632303837323133376664643433303361396336636130636666373639626230 39316264616663323761323130313930373834666161663836333735306338633232313637313361
34373761643564306663643037623531373163336131303733653739356362323539 66666338626338343434653761363735613166356632396663643332616631353433306566306662
63363538346634376338313538376333376537316532386531633537396638663732666665353462
61353339356461323532313630663135626532306132336161373235663033633033656461336235
62613434623631353336376230656232363730643637646131626430393962616537306262386565
31303839323263343439623935313666303330353539623130653838623161323130373933383039
38336631343761633861383338666365343562613835313265633165313832613665613230303533
37646432623461313236373137363536613565353164333935346337376635336264623965346337
61646462653032333362353738666238323861323835306664303735326162356338613162353763
38333035323661313661346136663437613931386432376165643830613066643061623934336130
64396431646533646130636333363866396138323131383037373937383230353662646634376132
30643530336665663865623165653135363961373838666134346463343239353733326262396630
34373562646634373566373238353836326562313162663764386239396364333162326630626630
31373962643765663364333766373031646263626236613461343533396164663962633235343136
62373936646332303161326265383165353362643234653831613539613231623563353566613633
64316461616236313137353136336365313833643333376665306232636537333734343633613461
66373066393639623037323535333661656664383265623130656563383537633131336562306237
64366135643537653231396238313836353439303465343732323533356437353935396139376462
3839656237373031313431326364373839613265303330383236
@@ -1,6 +1,6 @@
--- ---
# Paperclip Docker deployment # Paperclip Docker deployment
paperclip_domain: muppet-vision.wdw-labs.cloud paperclip_domain: cranium-command.wdw-labs.cloud
paperclip_base_path: /opt/paperclip paperclip_base_path: /opt/paperclip
paperclip_port: 3100 paperclip_port: 3100
certbot_email: ryan.blundon@protonmail.com certbot_email: ryan.blundon@protonmail.com
@@ -8,6 +8,12 @@ certbot_email: ryan.blundon@protonmail.com
# vLLM inference endpoint (local) # vLLM inference endpoint (local)
vllm_endpoint: "http://127.0.0.1:8000" vllm_endpoint: "http://127.0.0.1:8000"
hermes_model:
provider: custom
default: Gemma-4-26B-A4B-Hermes-VLM-MLX # from http://fantasmic.wdw-labs.cloud:8000/v1/models
base_url: http://fantasmic.wdw-labs.cloud:8000/v1
api_key: "{{ vault_fantasmic_api_key }}"
# # DNS entries (if using your existing dns-manager role) # # DNS entries (if using your existing dns-manager role)
# dns_entries: # dns_entries:
# - name: illuminations # - name: illuminations
+10 -6
View File
@@ -1,7 +1,7 @@
--- ---
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# FILE: ansible/host_vars/astro_orbiter/vars.yml # FILE: ansible/host_vars/illuminations/vars.yml
# HOST: astro-orbiter (10.1.71.130) # HOST: illuminations (10.1.71.130)
# ROLE: llama.cpp LLM inference host — Ryzen 7 5800XT / RTX 3090 (ATX rebuild, # ROLE: llama.cpp LLM inference host — Ryzen 7 5800XT / RTX 3090 (ATX rebuild,
# 2026-08-04). Superseded the prior AMD RX 5700 / Ollama config below; # 2026-08-04). Superseded the prior AMD RX 5700 / Ollama config below;
# drive was transplanted into new hardware, not reinstalled. # drive was transplanted into new hardware, not reinstalled.
@@ -234,7 +234,11 @@ vllm_service_enabled: true
vllm_service_state: started vllm_service_state: started
# Paperclip Docker deployment # Paperclip Docker deployment
paperclip_domain: muppet-vision.wdw-labs.cloud nginx_proxy_certbot_email: ryan.blundon@protonmail.com
paperclip_base_path: /opt/paperclip nginx_proxy_certbot_staging: true # remove after first successful real issuance
paperclip_port: 3100
certbot_email: ryan.blundon@protonmail.com hermes_model:
provider: custom
default: Gemma-4-26B-A4B-Hermes-VLM-MLX # from http://fantasmic.wdw-labs.cloud:8000/v1/models
base_url: http://fantasmic.wdw-labs.cloud:8000/v1
api_key: "{{ vault_fantasmic_api_key }}"
@@ -0,0 +1,7 @@
---
# Run after day1_deploy_nginx_proxy.yml (the role publishes its vhost through it).
- name: Deploy Hermes Agent (Docker)
hosts: illuminations
become: true
roles:
- hermes_docker
@@ -0,0 +1,6 @@
---
- name: Deploy shared nginx reverse proxy and certbot
hosts: illuminations
become: true
roles:
- nginx-proxy
+27 -29
View File
@@ -1,5 +1,5 @@
--- ---
- name: Deploy Paperclip stack with nginx and certbot - name: Deploy Paperclip stack (behind the shared nginx-proxy)
hosts: illuminations hosts: illuminations
become: true become: true
vars_files: vars_files:
@@ -8,77 +8,75 @@
roles: roles:
- docker-host - docker-host
- nginx-proxy # must come before paperclip-docker (it publishes its vhost through it)
- paperclip-docker - paperclip-docker
post_tasks: post_tasks:
- name: Verify Paperclip stack is running # CLI-based on purpose: community.docker.docker_container_info needs the
community.docker.docker_container_info: # Docker SDK for Python, which the paperclip-docker role no longer installs.
name: "{{ item }}" - name: Verify stack containers are running
ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ item }}"
register: container_status register: container_status
loop: loop:
- paperclip-db
- paperclip - paperclip
- paperclip-nginx - nginx
- paperclip-certbot - certbot
changed_when: false
- name: Fail if any container is not running
fail:
msg: "Container {{ item.item }} is not running or does not exist"
when: not item.exists or item.container.State.Running == false
loop: "{{ container_status.results }}"
changed_when: false changed_when: false
failed_when: container_status.rc != 0 or container_status.stdout | trim != 'true'
loop_control:
label: "{{ item }}"
- name: Verify Docker is available - name: Verify Docker is available
command: docker --version ansible.builtin.command: docker --version
register: docker_version register: docker_version
changed_when: false changed_when: false
- name: Show Docker version - name: Show Docker version
debug: ansible.builtin.debug:
msg: "Docker version: {{ docker_version.stdout }}" msg: "Docker version: {{ docker_version.stdout }}"
- name: Verify docker-compose plugin is available - name: Verify docker-compose plugin is available
command: docker compose version ansible.builtin.command: docker compose version
register: compose_version register: compose_version
changed_when: false changed_when: false
ignore_errors: true ignore_errors: true
- name: Show docker-compose version - name: Show docker-compose version
debug: ansible.builtin.debug:
msg: "Docker Compose version: {{ compose_version.stdout | default('docker-compose plugin not found') }}" msg: "Docker Compose version: {{ compose_version.stdout | default('docker-compose plugin not found') }}"
- name: Display deployment summary - name: Display deployment summary
debug: ansible.builtin.debug:
msg: | msg: |
Paperclip stack deployed successfully! Paperclip stack deployed successfully!
Access points: Access points:
- HTTPS: https://{{ paperclip_domain }} - HTTPS: https://{{ paperclip_domain }}
- HTTP (redirects to HTTPS): http://{{ paperclip_domain }} - HTTP (redirects to HTTPS): http://{{ paperclip_domain }}
Services: Services:
- Paperclip: {{ paperclip_host }}:{{ paperclip_port }} - Paperclip: {{ paperclip_host }}:{{ paperclip_port }}
- nginx (reverse proxy): 0.0.0.0:443 / 0.0.0.0:80 - nginx (shared reverse proxy): 0.0.0.0:443 / 0.0.0.0:80, config in {{ nginx_proxy_conf_dir }}
- certbot: Running certificate renewal checks - certbot: Running certificate renewal checks
Data location: Data location:
- {{ paperclip_base_path }}/data - {{ paperclip_base_path }}/data
Certificates: Certificates:
- {{ paperclip_certs_dir }}/live/{{ paperclip_domain }}/ - {{ nginx_proxy_certs_dir }}/live/{{ paperclip_domain }}/
vLLM endpoint: vLLM endpoint:
- {{ vllm_endpoint }} - {{ vllm_endpoint }}
OpenCode configuration: OpenCode configuration:
- Config: {{ paperclip_base_path }}/.config/opencode/opencode.jsonc - Config: {{ paperclip_base_path }}/.config/opencode/opencode.jsonc
- Model: Gemma-4-26B (AWQ) - Model: Gemma-4-26B (AWQ)
- Endpoint: {{ vllm_endpoint }}/v1 - Endpoint: {{ vllm_endpoint }}/v1
Next steps: Next steps:
1. Verify DNS resolution for {{ paperclip_domain }} 1. Verify DNS resolution for {{ paperclip_domain }}
2. Visit https://{{ paperclip_domain }} to access Paperclip 2. Visit https://{{ paperclip_domain }} to access Paperclip
3. OpenCode should auto-initialize with vLLM at {{ vllm_endpoint }} 3. OpenCode should auto-initialize with vLLM at {{ vllm_endpoint }}
4. Set up Hermes Gateway on Mac Studio 4. Run day1_deploy_hermes_docker.yml (publishes its dashboard through the same nginx)
5. Configure agents and skills 5. Configure agents and skills
@@ -0,0 +1,91 @@
---
hermes_image: nousresearch/hermes-agent:latest
# missing = only pull if absent (no surprise upgrades on redeploy).
# Set to "always" when you actually want to upgrade the image.
hermes_pull_policy: missing
hermes_container_name: hermes
hermes_project_dir: /opt/hermes
hermes_data_dir: "{{ hermes_project_dir }}/data"
# The container's hermes user. The image's start-up hook chowns the data
# volume to it; the role only needs the UID to stage files it can read.
hermes_uid: 10000
# config.yaml, .env and sessions live here and can hold API keys, so keep it
# owner-only. Passed to the container as HERMES_HOME_MODE (see compose).
hermes_data_dir_mode: "0700"
hermes_secrets_env_file: "{{ hermes_project_dir }}/hermes-secrets.env"
# Same network namespace as Paperclip and nginx
hermes_network_mode: host
hermes_memory_limit: 8G
hermes_cpu_limit: "4.0"
# Playwright/Chromium needs shared memory (upstream troubleshooting note)
hermes_shm_size: 1g
# --- Gateway / API server (loopback only; Paperclip reaches it locally) ---
hermes_gateway_port: 8642
hermes_api_server_enabled: true
hermes_api_server_host: 127.0.0.1
# Leave empty to run keyless on loopback. If you set one, Paperclip (or any
# other client) must send it as a bearer token.
# e.g. hermes_api_server_key: "{{ vault_hermes_api_server_key }}"
hermes_api_server_key: ""
# --- Dashboard (bound to loopback, published through nginx) ---
hermes_dashboard_enabled: true
hermes_dashboard_host: 127.0.0.1
hermes_dashboard_port: 9119
hermes_domain: haunted-mansion.wdw-labs.cloud
# Publish the dashboard through the nginx-proxy role (cert + vhost + reload).
# Requires day1_deploy_nginx_proxy.yml to have run first.
hermes_publish_vhost: true
hermes_public_url: "https://{{ hermes_domain }}"
hermes_dashboard_username: admin
# Vault variables this role requires (see README block at bottom of file)
hermes_required_vault_vars:
- vault_hermes_dashboard_password_hash
- vault_hermes_dashboard_secret
# --- Preflight ---
hermes_preflight_ports:
- "{{ hermes_gateway_port }}"
- "{{ hermes_dashboard_port }}"
hermes_wait_timeout: 120
# --- config.yaml overrides (deep-merged, only the keys listed here) ---
# nginx runs with host networking, so it reaches the dashboard from loopback.
# Model settings are NOT managed by default (your wizard-written config stays
# untouched). To manage them, add a model block, for example:
# model:
# provider: custom
# model: <served-model-name>
# base_url: http://10.1.71.130:8000/v1
# api_key: "{{ vault_vllm_api_key }}"
hermes_config_overrides:
dashboard:
trusted_proxies:
- "127.0.0.1"
# Main model (written to config.yaml under model:). Leave empty to manage it
# by hand with `hermes model` or the dashboard. Example:
# hermes_model:
# provider: custom
# default: <served model name from the endpoint's /v1/models>
# base_url: http://fantasmic.wdw-labs.cloud:8000/v1
# api_key: "{{ vault_fantasmic_api_key }}"
# # context_length: 65536 # only if Hermes complains about the context window
# The api_key lands in config.yaml in plain text (owner-only, see hermes_data_dir_mode).
hermes_model: {}
# What the merge task actually applies: base overrides + the model block.
hermes_effective_overrides: >-
{{ hermes_config_overrides
| combine({'model': hermes_model} if (hermes_model | length > 0) else {}, recursive=True) }}
# Required vault vars:
# vault_hermes_dashboard_password_hash - output of hash_password() (see below)
# vault_hermes_dashboard_secret - openssl rand -base64 32
# Generate the hash with:
# docker exec -it -w /opt/hermes hermes /opt/hermes/.venv/bin/python -c \
# "from plugins.dashboard_auth.basic import hash_password; print(hash_password('PW'))"
@@ -0,0 +1,40 @@
"""Deep-merge overrides (a JSON file, path in argv[1]) into config.yaml.
Run inside the hermes container as the hermes user. Only keys present in the
overrides are touched. Prints CHANGED when the file was rewritten.
"""
import json
import os
import sys
import yaml
PATH = "/opt/data/config.yaml"
with open(sys.argv[1]) as fh:
overrides = json.load(fh)
if os.path.exists(PATH):
with open(PATH) as fh:
current = yaml.safe_load(fh) or {}
else:
current = {}
def merge(dst, src):
changed = False
for key, val in src.items():
if isinstance(val, dict) and isinstance(dst.get(key), dict):
changed |= merge(dst[key], val)
elif dst.get(key) != val:
dst[key] = val
changed = True
return changed
if merge(current, overrides):
with open(PATH, "w") as fh:
yaml.safe_dump(current, fh, sort_keys=False)
print("CHANGED")
else:
print("UNCHANGED")
sys.exit(0)
@@ -0,0 +1,5 @@
---
# Config.yaml is read at start, so a plain restart is enough.
# Compose/env changes are handled by docker compose itself (it recreates).
- name: Restart hermes
ansible.builtin.command: "docker restart {{ hermes_container_name }}"
+148
View File
@@ -0,0 +1,148 @@
---
- name: Preflight | Required vault variables are defined
ansible.builtin.assert:
that: "{{ item }} is defined and ({{ item }} | length) > 0"
fail_msg: "{{ item }} is not defined. Add it to your ansible vault."
quiet: true
loop: "{{ hermes_required_vault_vars }}"
tags: [hermes, preflight]
- name: Preflight | Confirm docker CLI is available
ansible.builtin.command: docker --version
changed_when: false
tags: [hermes, preflight]
- name: Preflight | Get existing container state
ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ hermes_container_name }}"
register: hermes_existing
failed_when: false
changed_when: false
tags: [hermes, preflight]
# Only enforce free ports when our container is not already running,
# so redeploys stay idempotent.
- name: Preflight | Ensure required ports are free
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ item }}"
state: stopped
timeout: 3
loop: "{{ hermes_preflight_ports }}"
when: hermes_existing.stdout | default('') | trim != 'true'
tags: [hermes, preflight]
- name: Create project directory
ansible.builtin.file:
path: "{{ hermes_project_dir }}"
state: directory
mode: "0755"
tags: [hermes]
# No owner here: the image's start-up hook chowns the volume to the hermes user.
- name: Create data directory (owner-only)
ansible.builtin.file:
path: "{{ hermes_data_dir }}"
state: directory
mode: "{{ hermes_data_dir_mode }}"
tags: [hermes]
# Compose reads this as root on the host; the container never sees the file.
- name: Template secrets env file
ansible.builtin.template:
src: hermes-secrets.env.j2
dest: "{{ hermes_secrets_env_file }}"
owner: root
group: root
mode: "0600"
no_log: true
tags: [hermes]
- name: Template docker compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ hermes_project_dir }}/docker-compose.yml"
mode: "0644"
tags: [hermes]
- name: Deploy hermes
community.docker.docker_compose_v2:
project_src: "{{ hermes_project_dir }}"
pull: "{{ hermes_pull_policy }}"
state: present
tags: [hermes]
- name: Verify gateway health
ansible.builtin.uri:
url: "http://127.0.0.1:{{ hermes_gateway_port }}/health"
status_code: 200
register: hermes_health
until: hermes_health.status == 200
retries: "{{ (hermes_wait_timeout | int) // 5 }}"
delay: 5
when: hermes_api_server_enabled | bool
tags: [hermes, verify]
- name: Wait for dashboard port
ansible.builtin.wait_for:
host: "{{ hermes_dashboard_host }}"
port: "{{ hermes_dashboard_port }}"
timeout: "{{ hermes_wait_timeout }}"
when: hermes_dashboard_enabled | bool
tags: [hermes, verify]
# Overrides can contain secrets (the model api_key), so they are staged in a
# 0600 file owned by the container user instead of travelling in a command
# line (visible in `ps`), and removed again afterwards. The merge script runs
# inside the container with the image's own venv (PyYAML is available there),
# as the hermes user, so config.yaml stays owned by it.
- name: Apply config.yaml overrides
when: hermes_effective_overrides | length > 0
tags: [hermes, config]
block:
- name: Stage overrides file
ansible.builtin.copy:
content: "{{ hermes_effective_overrides | to_json }}"
dest: "{{ hermes_data_dir }}/.ansible-overrides.json"
owner: "{{ hermes_uid }}"
group: "{{ hermes_uid }}"
mode: "0600"
changed_when: false # transient file; only the merge result should count as a change
no_log: true
- name: Merge into config.yaml
ansible.builtin.command:
argv:
- docker
- exec
- -i
- --user
- hermes
- "{{ hermes_container_name }}"
- /opt/hermes/.venv/bin/python
- "-"
- /opt/data/.ansible-overrides.json
stdin: "{{ lookup('ansible.builtin.file', 'merge_config.py') }}"
register: hermes_config_merge
changed_when: "'CHANGED' in hermes_config_merge.stdout and 'UNCHANGED' not in hermes_config_merge.stdout"
notify: Restart hermes
always:
- name: Remove staged overrides file
ansible.builtin.file:
path: "{{ hermes_data_dir }}/.ansible-overrides.json"
state: absent
changed_when: false
- name: Publish dashboard through nginx-proxy
ansible.builtin.include_role:
name: nginx-proxy
tasks_from: add_vhost
vars:
vhost_name: hermes
vhost_domain: "{{ hermes_domain }}"
vhost_upstream_port: "{{ hermes_dashboard_port }}"
vhost_proxy_timeout: 3600s # long-lived dashboard WebSockets
when:
- hermes_publish_vhost | bool
- hermes_dashboard_enabled | bool
tags: [hermes, nginx]
@@ -0,0 +1,31 @@
# Managed by Ansible (hermes_docker role)
services:
hermes:
image: {{ hermes_image }}
container_name: {{ hermes_container_name }}
restart: unless-stopped
network_mode: {{ hermes_network_mode }}
command: gateway run
shm_size: "{{ hermes_shm_size }}"
env_file:
- {{ hermes_secrets_env_file }}
environment:
HERMES_HOME_MODE: "{{ hermes_data_dir_mode }}"
{% if hermes_dashboard_enabled | bool %}
HERMES_DASHBOARD: "1"
HERMES_DASHBOARD_HOST: "{{ hermes_dashboard_host }}"
HERMES_DASHBOARD_PORT: "{{ hermes_dashboard_port }}"
HERMES_DASHBOARD_PUBLIC_URL: "{{ hermes_public_url }}"
{% endif %}
{% if hermes_api_server_enabled | bool %}
API_SERVER_ENABLED: "true"
API_SERVER_HOST: "{{ hermes_api_server_host }}"
API_SERVER_PORT: "{{ hermes_gateway_port }}"
{% endif %}
volumes:
- {{ hermes_data_dir }}:/opt/data
deploy:
resources:
limits:
memory: {{ hermes_memory_limit }}
cpus: "{{ hermes_cpu_limit }}"
@@ -0,0 +1,8 @@
# Managed by Ansible (hermes_docker role) - do not edit
# Single quotes keep compose from interpolating '$' inside hashes/secrets.
HERMES_DASHBOARD_BASIC_AUTH_USERNAME={{ hermes_dashboard_username }}
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH='{{ vault_hermes_dashboard_password_hash }}'
HERMES_DASHBOARD_BASIC_AUTH_SECRET='{{ vault_hermes_dashboard_secret }}'
{% if hermes_api_server_key | length > 0 %}
API_SERVER_KEY='{{ hermes_api_server_key }}'
{% endif %}
@@ -0,0 +1,66 @@
---
hermes_image: nousresearch/hermes-agent:latest
# missing = only pull if absent (no surprise upgrades on redeploy).
# Set to "always" when you actually want to upgrade the image.
hermes_pull_policy: missing
hermes_container_name: hermes
hermes_project_dir: /opt/hermes
hermes_data_dir: "{{ hermes_project_dir }}/data"
hermes_secrets_env_file: "{{ hermes_project_dir }}/hermes-secrets.env"
# Same network namespace as Paperclip and nginx
hermes_network_mode: host
hermes_memory_limit: 8G
hermes_cpu_limit: "4.0"
# Playwright/Chromium needs shared memory (upstream troubleshooting note)
hermes_shm_size: 1g
# --- Gateway / API server (loopback only; Paperclip reaches it locally) ---
hermes_gateway_port: 8642
hermes_api_server_enabled: true
hermes_api_server_host: 127.0.0.1
# Leave empty to run keyless on loopback. If you set one, Paperclip (or any
# other client) must send it as a bearer token.
# e.g. hermes_api_server_key: "{{ vault_hermes_api_server_key }}"
hermes_api_server_key: "{{ vault_hermes_api_server_key }}"
# --- Dashboard (bound to loopback, published through nginx) ---
hermes_dashboard_enabled: true
hermes_dashboard_host: 127.0.0.1
hermes_dashboard_port: 9119
hermes_domain: haunted-mansion.wdw-labs.cloud
hermes_public_url: "https://{{ hermes_domain }}"
hermes_dashboard_username: admin
# Vault variables this role requires (see README block at bottom of file)
hermes_required_vault_vars:
- vault_hermes_dashboard_password_hash
- vault_hermes_dashboard_secret
# --- Preflight ---
hermes_preflight_ports:
- "{{ hermes_gateway_port }}"
- "{{ hermes_dashboard_port }}"
hermes_wait_timeout: 120
# --- config.yaml overrides (deep-merged, only the keys listed here) ---
# nginx runs with host networking, so it reaches the dashboard from loopback.
# Model settings are NOT managed by default (your wizard-written config stays
# untouched). To manage them, add a model block, for example:
# model:
# provider: custom
# model: <served-model-name>
# base_url: http://10.1.71.130:8000/v1
# api_key: "{{ vault_vllm_api_key }}"
hermes_config_overrides:
dashboard:
trusted_proxies:
- "127.0.0.1"
# Required vault vars:
# vault_hermes_dashboard_password_hash - output of hash_password() (see below)
# vault_hermes_dashboard_secret - openssl rand -base64 32
# Generate the hash with:
# docker exec -it -w /opt/hermes hermes /opt/hermes/.venv/bin/python -c \
# "from plugins.dashboard_auth.basic import hash_password; print(hash_password('PW'))"
@@ -0,0 +1,39 @@
"""Deep-merge overrides (JSON in $HERMES_CONFIG_OVERRIDES) into config.yaml.
Run inside the hermes container as the hermes user. Only keys present in the
overrides are touched. Prints CHANGED when the file was rewritten.
"""
import json
import os
import sys
import yaml
PATH = "/opt/data/config.yaml"
overrides = json.loads(os.environ["HERMES_CONFIG_OVERRIDES"])
if os.path.exists(PATH):
with open(PATH) as fh:
current = yaml.safe_load(fh) or {}
else:
current = {}
def merge(dst, src):
changed = False
for key, val in src.items():
if isinstance(val, dict) and isinstance(dst.get(key), dict):
changed |= merge(dst[key], val)
elif dst.get(key) != val:
dst[key] = val
changed = True
return changed
if merge(current, overrides):
with open(PATH, "w") as fh:
yaml.safe_dump(current, fh, sort_keys=False)
print("CHANGED")
else:
print("UNCHANGED")
sys.exit(0)
@@ -0,0 +1,5 @@
---
# Config.yaml is read at start, so a plain restart is enough.
# Compose/env changes are handled by docker compose itself (it recreates).
- name: Restart hermes
ansible.builtin.command: "docker restart {{ hermes_container_name }}"
@@ -0,0 +1,49 @@
# ---- Hermes dashboard: paste inside the http { } block of nginx.conf.j2 ----
# Redirect HTTP to HTTPS
server {
listen 80;
server_name {{ hermes_domain }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ hermes_domain }};
ssl_certificate /etc/nginx/certs/live/{{ hermes_domain }}/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/{{ hermes_domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# No auth_basic here: the dashboard has its own login (basic auth provider).
location / {
proxy_pass http://127.0.0.1:{{ hermes_dashboard_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
}
}
@@ -0,0 +1,108 @@
---
- name: Preflight | Required vault variables are defined
ansible.builtin.assert:
that: "{{ item }} is defined and ({{ item }} | length) > 0"
fail_msg: "{{ item }} is not defined. Add it to your ansible vault."
quiet: true
loop: "{{ hermes_required_vault_vars }}"
tags: [hermes, preflight]
- name: Preflight | Confirm docker CLI is available
ansible.builtin.command: docker --version
changed_when: false
tags: [hermes, preflight]
- name: Preflight | Get existing container state
community.docker.docker_container_info:
name: "{{ hermes_container_name }}"
register: hermes_existing
tags: [hermes, preflight]
# Only enforce free ports when our container is not already running,
# so redeploys stay idempotent.
- name: Preflight | Ensure required ports are free
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ item }}"
state: stopped
timeout: 3
loop: "{{ hermes_preflight_ports }}"
when: not (hermes_existing.container.State.Running | default(false))
tags: [hermes, preflight]
- name: Create project and data directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ hermes_project_dir }}"
- "{{ hermes_data_dir }}"
tags: [hermes]
# Compose reads this as root on the host; the container never sees the file.
- name: Template secrets env file
ansible.builtin.template:
src: hermes-secrets.env.j2
dest: "{{ hermes_secrets_env_file }}"
owner: root
group: root
mode: "0600"
no_log: true
tags: [hermes]
- name: Template docker compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ hermes_project_dir }}/docker-compose.yml"
mode: "0644"
tags: [hermes]
- name: Deploy hermes
community.docker.docker_compose_v2:
project_src: "{{ hermes_project_dir }}"
pull: "{{ hermes_pull_policy }}"
state: present
tags: [hermes]
- name: Verify gateway health
ansible.builtin.uri:
url: "http://127.0.0.1:{{ hermes_gateway_port }}/health"
status_code: 200
register: hermes_health
until: hermes_health.status == 200
retries: "{{ (hermes_wait_timeout | int) // 5 }}"
delay: 5
when: hermes_api_server_enabled | bool
tags: [hermes, verify]
- name: Wait for dashboard port
ansible.builtin.wait_for:
host: "{{ hermes_dashboard_host }}"
port: "{{ hermes_dashboard_port }}"
timeout: "{{ hermes_wait_timeout }}"
when: hermes_dashboard_enabled | bool
tags: [hermes, verify]
# Runs inside the container with the image's own venv (PyYAML is available
# there) as the hermes user, so config.yaml stays owned by UID 10000.
- name: Merge config.yaml overrides
ansible.builtin.command:
argv:
- docker
- exec
- -i
- --user
- hermes
- -e
- "HERMES_CONFIG_OVERRIDES={{ hermes_config_overrides | to_json }}"
- "{{ hermes_container_name }}"
- /opt/hermes/.venv/bin/python
- "-"
stdin: "{{ lookup('ansible.builtin.file', 'merge_config.py') }}"
register: hermes_config_merge
changed_when: "'CHANGED' in hermes_config_merge.stdout and 'UNCHANGED' not in hermes_config_merge.stdout"
no_log: true
when: hermes_config_overrides | length > 0
notify: Restart hermes
tags: [hermes, config]
@@ -0,0 +1,30 @@
# Managed by Ansible (hermes_docker role)
services:
hermes:
image: {{ hermes_image }}
container_name: {{ hermes_container_name }}
restart: unless-stopped
network_mode: {{ hermes_network_mode }}
command: gateway run
shm_size: "{{ hermes_shm_size }}"
env_file:
- {{ hermes_secrets_env_file }}
environment:
{% if hermes_dashboard_enabled | bool %}
HERMES_DASHBOARD: "1"
HERMES_DASHBOARD_HOST: "{{ hermes_dashboard_host }}"
HERMES_DASHBOARD_PORT: "{{ hermes_dashboard_port }}"
HERMES_DASHBOARD_PUBLIC_URL: "{{ hermes_public_url }}"
{% endif %}
{% if hermes_api_server_enabled | bool %}
API_SERVER_ENABLED: "true"
API_SERVER_HOST: "{{ hermes_api_server_host }}"
API_SERVER_PORT: "{{ hermes_gateway_port }}"
{% endif %}
volumes:
- {{ hermes_data_dir }}:/opt/data
deploy:
resources:
limits:
memory: {{ hermes_memory_limit }}
cpus: "{{ hermes_cpu_limit }}"
@@ -0,0 +1,8 @@
# Managed by Ansible (hermes_docker role) - do not edit
# Single quotes keep compose from interpolating '$' inside hashes/secrets.
HERMES_DASHBOARD_BASIC_AUTH_USERNAME={{ hermes_dashboard_username }}
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH='{{ vault_hermes_dashboard_password_hash }}'
HERMES_DASHBOARD_BASIC_AUTH_SECRET='{{ vault_hermes_dashboard_secret }}'
{% if hermes_api_server_key | length > 0 %}
API_SERVER_KEY='{{ hermes_api_server_key }}'
{% endif %}
+28
View File
@@ -0,0 +1,28 @@
# nginx-proxy
Shared nginx + certbot (Cloudflare DNS challenge) under /opt/nginx. Apps publish
themselves with `tasks_from: add_vhost`; see the header of tasks/add_vhost.yml.
Layout on the host:
/opt/nginx/
docker-compose.yml .env (0600)
conf.d/ 00-global.conf + one <app>.conf per vhost -> /etc/nginx/conf.d
certs/ certbot state (live/, archive/, renewal/) -> /etc/letsencrypt, /etc/nginx/certs
cert-domains/ one empty file per domain (filename = domain)
scripts/ certbot-init.sh
Required variables: `nginx_proxy_certbot_email`, and `vault_cloudflare_api_token`
(or `nginx_proxy_cloudflare_api_token`).
Prerequisite (manual): a DNS record for each app domain pointing at this host.
Staging: set `nginx_proxy_certbot_staging: true` to test the DNS challenge without
using Let's Encrypt rate limits. Staging certs are skipped-as-existing afterwards, so
to move a domain to a real cert: `docker exec certbot certbot delete --cert-name <domain>`
and re-run its play.
Retire a domain: delete its cert-domains/ marker and conf.d/<app>.conf, reload nginx,
then `docker exec certbot certbot delete --cert-name <domain>`.
Pin nginx older than 1.25.1? Set `nginx_proxy_http2: false` (the template uses `http2 on;`).
@@ -0,0 +1,27 @@
---
nginx_proxy_base_path: /opt/nginx
nginx_proxy_certs_dir: "{{ nginx_proxy_base_path }}/certs"
nginx_proxy_conf_dir: "{{ nginx_proxy_base_path }}/conf.d"
nginx_proxy_domains_dir: "{{ nginx_proxy_base_path }}/cert-domains"
nginx_proxy_scripts_dir: "{{ nginx_proxy_base_path }}/scripts"
nginx_proxy_nginx_container: nginx
nginx_proxy_certbot_container: certbot
nginx_proxy_nginx_image: nginx:latest
nginx_proxy_certbot_image: certbot/dns-cloudflare:latest
# Required: set these in group_vars/host_vars (token comes from the vault)
nginx_proxy_certbot_email: ""
nginx_proxy_cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
# true = Let's Encrypt staging (untrusted certs, no rate-limit risk). Use while iterating.
nginx_proxy_certbot_staging: false
# nginx reloads this often so renewed certs get picked up
nginx_proxy_reload_interval: 6h
nginx_proxy_client_max_body_size: 100M
# "http2 on;" needs nginx >= 1.25.1 (fine for nginx:latest). Set false if you pin older.
nginx_proxy_http2: true
nginx_proxy_pull_policy: missing
@@ -0,0 +1,92 @@
#!/bin/sh
# Issues certificates (one per domain) via the Cloudflare DNS challenge.
#
# certbot-init.sh -> every domain with a marker file in $DOMAINS_DIR
# certbot-init.sh a.example.com -> only the domains given as arguments
#
# Idempotent: domains that already have a certificate are skipped.
# Renewal is NOT handled here; `certbot renew` covers every existing lineage.
set -eu
DOMAINS_DIR="${DOMAINS_DIR:-/etc/cert-domains.d}"
EMAIL="${CERTBOT_EMAIL:-}"
TOKEN="${CLOUDFLARE_API_TOKEN:-}"
STAGING_ARG=""
if [ "${CERTBOT_STAGING:-0}" = "1" ]; then
STAGING_ARG="--server https://acme-staging-v02.api.letsencrypt.org/directory"
echo "NOTE: using the Let's Encrypt STAGING server (certificates will not be trusted)"
fi
if [ "$#" -gt 0 ]; then
DOMAINS="$*"
elif [ -d "$DOMAINS_DIR" ]; then
DOMAINS=$(ls -1 "$DOMAINS_DIR" | grep -E '^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$' || true)
else
DOMAINS=""
fi
DOMAINS=$(printf '%s\n' "$DOMAINS" | tr ', \t' '\n\n\n' | grep -v '^$' | sort -u || true)
if [ -z "$DOMAINS" ]; then
echo "No domains registered in $DOMAINS_DIR; nothing to do"
exit 0
fi
if [ -z "$EMAIL" ]; then
echo "ERROR: CERTBOT_EMAIL not set"
exit 1
fi
if [ -z "$TOKEN" ]; then
echo "ERROR: CLOUDFLARE_API_TOKEN not set"
exit 1
fi
# Temporary credentials file, shared by all requests and always cleaned up
CREDS_FILE=$(mktemp /tmp/cloudflare.XXXXXX)
trap 'rm -f "$CREDS_FILE"' EXIT
chmod 600 "$CREDS_FILE"
printf 'dns_cloudflare_api_token = %s\n' "$TOKEN" > "$CREDS_FILE"
FAILED=""
ISSUED=0
SKIPPED=0
for DOMAIN in $DOMAINS; do
CERT_PATH="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem"
if [ -f "$CERT_PATH" ]; then
echo "Certificate for $DOMAIN already exists at $CERT_PATH"
SKIPPED=$((SKIPPED + 1))
continue
fi
echo "Requesting certificate for $DOMAIN via Cloudflare DNS challenge..."
# --cert-name pins the lineage directory to the domain name so the
# live/<domain>/ path nginx expects never gets a -0001 suffix.
# STAGING_ARG is intentionally unquoted (it expands to two words).
# shellcheck disable=SC2086
if certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials "$CREDS_FILE" \
--agree-tos \
--no-eff-email \
--email "$EMAIL" \
--non-interactive \
--cert-name "$DOMAIN" \
$STAGING_ARG \
-d "$DOMAIN" && [ -f "$CERT_PATH" ]; then
echo "Certificate obtained and verified for $DOMAIN at $CERT_PATH"
ISSUED=$((ISSUED + 1))
else
echo "ERROR: Failed to obtain certificate for $DOMAIN"
FAILED="$FAILED $DOMAIN"
fi
done
echo "Summary: $ISSUED issued, $SKIPPED already present"
if [ -n "$FAILED" ]; then
echo "ERROR: certificate request failed for:$FAILED"
exit 1
fi
@@ -0,0 +1,6 @@
---
- name: Reload nginx
ansible.builtin.shell: >
docker exec {{ nginx_proxy_nginx_container }} nginx -t &&
docker exec {{ nginx_proxy_nginx_container }} nginx -s reload
changed_when: true
@@ -0,0 +1,118 @@
---
# Publish an app through nginx-proxy. Usage:
#
# - ansible.builtin.include_role:
# name: nginx-proxy
# tasks_from: add_vhost
# vars:
# vhost_name: paperclip # required, becomes conf.d/<name>.conf
# vhost_domain: "{{ paperclip_domain }}" # required
# vhost_upstream_port: 3100 # required
# vhost_upstream_host: 127.0.0.1 # optional
# vhost_proxy_timeout: 60s # optional (use 3600s for WebSockets)
# vhost_health_path: /health # optional, proxied without access log
#
# Order: marker -> certificate -> vhost file -> nginx -t -> reload.
# The vhost is only written once the certificate exists, and a vhost that
# fails nginx -t is rolled back, so one bad app cannot take the others down.
- name: Validate vhost parameters
ansible.builtin.assert:
that:
- vhost_name is defined
- vhost_domain is defined
- vhost_upstream_port is defined
fail_msg: "add_vhost needs vhost_name, vhost_domain and vhost_upstream_port"
quiet: true
- name: Check that the nginx-proxy containers are running
ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ item }}"
register: nginx_proxy_running
failed_when: nginx_proxy_running.rc != 0 or nginx_proxy_running.stdout | trim != 'true'
changed_when: false
loop:
- "{{ nginx_proxy_nginx_container }}"
- "{{ nginx_proxy_certbot_container }}"
loop_control:
label: "{{ item }}"
- name: "Register cert marker for {{ vhost_domain }}"
ansible.builtin.file:
path: "{{ nginx_proxy_domains_dir }}/{{ vhost_domain }}"
state: touch
modification_time: preserve
access_time: preserve
mode: "0644"
- name: "Check for existing certificate for {{ vhost_domain }}"
ansible.builtin.stat:
path: "{{ nginx_proxy_certs_dir }}/live/{{ vhost_domain }}/fullchain.pem"
register: nginx_proxy_cert_before
# Targeted run: only this domain, so another app's failed issuance
# cannot fail this play.
- name: "Issue certificate for {{ vhost_domain }}"
ansible.builtin.command: >-
docker exec {{ nginx_proxy_certbot_container }}
/scripts/certbot-init.sh {{ vhost_domain }}
register: nginx_proxy_cert_issue
changed_when: "'Certificate obtained' in nginx_proxy_cert_issue.stdout"
when: not nginx_proxy_cert_before.stat.exists
- name: "Confirm certificate exists for {{ vhost_domain }}"
ansible.builtin.stat:
path: "{{ nginx_proxy_certs_dir }}/live/{{ vhost_domain }}/fullchain.pem"
register: nginx_proxy_cert_after
failed_when: not nginx_proxy_cert_after.stat.exists
- name: "Write {{ vhost_name }} vhost, test, and reload"
block:
- name: "Template conf.d/{{ vhost_name }}.conf"
ansible.builtin.template:
src: vhost.conf.j2
dest: "{{ nginx_proxy_conf_dir }}/{{ vhost_name }}.conf"
mode: "0644"
backup: true
register: nginx_proxy_vhost_file
- name: Test nginx config
ansible.builtin.command: "docker exec {{ nginx_proxy_nginx_container }} nginx -t"
changed_when: false
- name: Reload nginx
ansible.builtin.command: "docker exec {{ nginx_proxy_nginx_container }} nginx -s reload"
when: nginx_proxy_vhost_file.changed
- name: Remove superseded backup
ansible.builtin.file:
path: "{{ nginx_proxy_vhost_file.backup_file }}"
state: absent
when: nginx_proxy_vhost_file.backup_file is defined
rescue:
- name: Restore previous vhost
ansible.builtin.copy:
src: "{{ nginx_proxy_vhost_file.backup_file }}"
dest: "{{ nginx_proxy_conf_dir }}/{{ vhost_name }}.conf"
remote_src: true
mode: "0644"
when: nginx_proxy_vhost_file.backup_file | default('') | length > 0
- name: Remove backup after restoring it
ansible.builtin.file:
path: "{{ nginx_proxy_vhost_file.backup_file }}"
state: absent
when: nginx_proxy_vhost_file.backup_file | default('') | length > 0
- name: Remove the new vhost (there was no previous one)
ansible.builtin.file:
path: "{{ nginx_proxy_conf_dir }}/{{ vhost_name }}.conf"
state: absent
when:
- nginx_proxy_vhost_file is defined
- nginx_proxy_vhost_file.backup_file | default('') | length == 0
- name: Fail with context
ansible.builtin.fail:
msg: >-
nginx rejected the {{ vhost_name }} vhost; it was rolled back and
the running config is unchanged.
+70
View File
@@ -0,0 +1,70 @@
---
- name: Assert required variables
ansible.builtin.assert:
that:
- nginx_proxy_certbot_email | length > 0
- nginx_proxy_cloudflare_api_token | length > 0
fail_msg: >-
Set nginx_proxy_certbot_email, and make sure vault_cloudflare_api_token
(or nginx_proxy_cloudflare_api_token) is defined.
quiet: true
tags: [nginx-proxy, preflight]
- name: Create directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ nginx_proxy_base_path }}"
- "{{ nginx_proxy_certs_dir }}"
- "{{ nginx_proxy_conf_dir }}"
- "{{ nginx_proxy_domains_dir }}"
- "{{ nginx_proxy_scripts_dir }}"
tags: [nginx-proxy]
- name: Install certbot script
ansible.builtin.copy:
src: certbot-init.sh
dest: "{{ nginx_proxy_scripts_dir }}/certbot-init.sh"
mode: "0755"
tags: [nginx-proxy]
- name: Template global nginx settings
ansible.builtin.template:
src: 00-global.conf.j2
dest: "{{ nginx_proxy_conf_dir }}/00-global.conf"
mode: "0644"
notify: Reload nginx
tags: [nginx-proxy]
- name: Template compose env file
ansible.builtin.template:
src: env.j2
dest: "{{ nginx_proxy_base_path }}/.env"
mode: "0600"
no_log: true
tags: [nginx-proxy]
- name: Template compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ nginx_proxy_base_path }}/docker-compose.yml"
mode: "0644"
tags: [nginx-proxy]
- name: Deploy nginx and certbot
community.docker.docker_compose_v2:
project_src: "{{ nginx_proxy_base_path }}"
pull: "{{ nginx_proxy_pull_policy }}"
state: present
tags: [nginx-proxy]
- name: Verify nginx config is valid and the container is serving
ansible.builtin.command: "docker exec {{ nginx_proxy_nginx_container }} nginx -t"
register: nginx_proxy_test
until: nginx_proxy_test.rc == 0
retries: 6
delay: 5
changed_when: false
tags: [nginx-proxy, verify]
@@ -0,0 +1,31 @@
# Managed by Ansible (nginx-proxy role)
# Included inside the stock nginx.conf http { } block, so these are http-level.
server_tokens off;
client_max_body_size {{ nginx_proxy_client_max_body_size }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Catch-all: unknown Host headers get nothing, and unknown SNI is rejected
# before any certificate is presented.
server {
listen 80 default_server;
server_name _;
access_log off;
location = /nginx-health {
return 200 "ok\n";
}
location / {
return 444;
}
}
server {
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
@@ -0,0 +1,46 @@
# Managed by Ansible (nginx-proxy role)
services:
certbot:
image: {{ nginx_proxy_certbot_image }}
container_name: {{ nginx_proxy_certbot_container }}
volumes:
- {{ nginx_proxy_certs_dir }}:/etc/letsencrypt
- {{ nginx_proxy_scripts_dir }}:/scripts:ro
- {{ nginx_proxy_domains_dir }}:/etc/cert-domains.d:ro
environment:
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
CERTBOT_STAGING: "{{ '1' if nginx_proxy_certbot_staging | bool else '0' }}"
entrypoint: /bin/sh
command:
- -c
- |
while true; do
/scripts/certbot-init.sh || echo "issuance failed; will retry next cycle"
certbot renew --quiet
sleep 12h
done
restart: unless-stopped
nginx:
image: {{ nginx_proxy_nginx_image }}
container_name: {{ nginx_proxy_nginx_container }}
network_mode: host
volumes:
# Directory mounts (not single files) so Ansible's atomic file
# replacement is visible inside the container.
- {{ nginx_proxy_conf_dir }}:/etc/nginx/conf.d:ro
- {{ nginx_proxy_certs_dir }}:/etc/nginx/certs:ro
command:
- /bin/sh
- -c
- |
( while :; do sleep {{ nginx_proxy_reload_interval }}; nginx -s reload; done ) &
exec nginx -g 'daemon off;'
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/nginx-health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
@@ -0,0 +1,3 @@
# Managed by Ansible (nginx-proxy role)
CLOUDFLARE_API_TOKEN={{ nginx_proxy_cloudflare_api_token }}
CERTBOT_EMAIL={{ nginx_proxy_certbot_email }}
@@ -0,0 +1,51 @@
# Managed by Ansible (nginx-proxy role) - vhost: {{ vhost_name }}
server {
listen 80;
server_name {{ vhost_domain }};
return 301 https://{{ vhost_domain }}$request_uri;
}
server {
listen 443 ssl;
{% if nginx_proxy_http2 | bool %}
http2 on;
{% endif %}
server_name {{ vhost_domain }};
ssl_certificate /etc/nginx/certs/live/{{ vhost_domain }}/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/{{ vhost_domain }}/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://{{ vhost_upstream_host | default('127.0.0.1') }}:{{ vhost_upstream_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout {{ vhost_proxy_timeout | default('60s') }};
proxy_read_timeout {{ vhost_proxy_timeout | default('60s') }};
proxy_buffering off;
proxy_request_buffering off;
}
{% if vhost_health_path | default('') | length > 0 %}
location {{ vhost_health_path }} {
proxy_pass http://{{ vhost_upstream_host | default('127.0.0.1') }}:{{ vhost_upstream_port }}{{ vhost_health_path }};
access_log off;
}
{% endif %}
}
@@ -2,31 +2,30 @@
# Paperclip deployment paths and configuration # Paperclip deployment paths and configuration
paperclip_base_path: /opt/paperclip paperclip_base_path: /opt/paperclip
paperclip_data_dir: "{{ paperclip_base_path }}/data" paperclip_data_dir: "{{ paperclip_base_path }}/data"
paperclip_certs_dir: "{{ paperclip_base_path }}/certs"
paperclip_port: 3100 paperclip_port: 3100
paperclip_domain: muppet-vision.wdw-labs.cloud paperclip_domain: cranium-command.wdw-labs.cloud
paperclip_host: muppet-vision.wdw-labs.cloud paperclip_host: cranium-command.wdw-labs.cloud
# paperclip_listen_host: 127.0.0.1 # Bootstrap local deployment
paperclip_listen_host: 0.0.0.0 # Switch to this host after CEO is functional on local model
# Paperclip allowed hostnames (space or comma-separated) # Paperclip allowed hostnames (space or comma-separated)
# Includes: domain name, localhost (for internal/local access), and client IP # Includes: domain name, localhost (for internal/local access), and client IP
paperclip_allowed_hostnames: "{{ paperclip_domain }} localhost 192.168.10.50" paperclip_allowed_hostnames: "{{ paperclip_domain }} localhost 192.168.10.50"
# Deployment mode: authenticated (requires login) or local_trusted (no auth, trusted network) # Deployment mode: authenticated (requires login) or local_trusted (no auth, trusted network)
paperclip_deployment_mode: "authenticated" # paperclip_deployment_mode: "local_trusted" # Bootstrap local deployment
paperclip_deployment_mode: "authenticated" # Switch to this model after CEO is functional on local model
# Reverse proxy configuration # TLS / reverse proxy are handled by the nginx-proxy role (/opt/nginx).
nginx_port: 443 # Set nginx_proxy_certbot_email in host_vars; run day1_deploy_nginx_proxy.yml first.
nginx_http_port: 80
# HTTPS/SSL configuration # Inference endpoints (vLLM or remote OpenAI-compatible)
certbot_email: ryan.blundon@protonmail.com illuminations_endpoint: "http://illuminations:8000"
certbot_provider: cloudflare
# Inference endpoint (vLLM or remote OpenAI-compatible)
vllm_endpoint: "http://127.0.0.1:8000"
fantasmic_endpoint: "http://fantasmic.wdw-labs.cloud:8000" fantasmic_endpoint: "http://fantasmic.wdw-labs.cloud:8000"
wishes_endpoint: "http://fantasmic.wdw-labs.cloud:8000" wishes_endpoint: "http://wishes.wdw-labs.cloud:8000"
# NOTE: Authentication secrets (BETTER_AUTH_SECRET, PAPERCLIP_AGENT_JWT_SECRET) # NOTE: Secrets come from the ansible vault and are written to .env (mode 0600):
# are auto-generated during deployment and stored in .env (mode 0600) # vault_better_auth_secret, vault_paperclip_agent_jwt_secret,
# No need to specify them here — they're created with: openssl rand -hex 24 # vault_illuminations_api_key, vault_fantasmic_api_key
# Everything in .env is passed into the Paperclip container (env_file), so only
# put values here that Paperclip itself should be able to read.
@@ -1,19 +1,12 @@
--- ---
# Recreate (not just restart) so .env / env_file changes are actually applied.
# Skipped when the deploy task already created or recreated containers this run.
- name: Restart paperclip stack - name: Restart paperclip stack
shell: | community.docker.docker_compose_v2:
cd {{ paperclip_base_path }} project_src: "{{ paperclip_base_path }}"
/usr/local/bin/docker-compose restart services:
become: yes - paperclip
state: present
- name: Reload nginx recreate: always
shell: | become: true
docker exec paperclip-nginx nginx -s reload when: not (paperclip_deploy.changed | default(false))
become: yes
ignore_errors: yes
- name: Reload certbot
shell: |
docker restart paperclip-certbot
become: yes
ignore_errors: yes
+97 -110
View File
@@ -1,27 +1,6 @@
--- ---
- name: Install Docker SDK for Python
apt:
name: python3-docker
state: present
become: true
- name: Download latest docker-compose from GitHub
shell: |
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
become: true
- name: Verify docker-compose installation
shell: /usr/local/bin/docker-compose --version
register: compose_version
become: true
- name: Show docker-compose version
debug:
msg: "{{ compose_version.stdout }}"
- name: Create paperclip directories - name: Create paperclip directories
file: ansible.builtin.file:
path: "{{ item }}" path: "{{ item }}"
state: directory state: directory
mode: '0755' mode: '0755'
@@ -29,97 +8,77 @@
group: root group: root
loop: loop:
- "{{ paperclip_base_path }}" - "{{ paperclip_base_path }}"
- "{{ paperclip_data_dir }}" # The postgres image chowns its own data dir on start; do not chown it here.
- "{{ paperclip_certs_dir }}"
- "{{ paperclip_base_path }}/pgdata" - "{{ paperclip_base_path }}/pgdata"
become: true become: true
tags: [paperclip, files]
- name: Create OpenCode config directory # Container runs as UID 1000. Deliberately NOT recursive: recurse + mode would
file: # chmod every file the app writes under these paths on each run. A fresh
path: "{{ paperclip_base_path }}/.config/opencode" # deploy has no root-owned content to repair; if you ever need to, run a
# one-off `chown -R 1000:1000 <dir>` by hand.
- name: Create container-owned directories (UID 1000)
ansible.builtin.file:
path: "{{ item }}"
state: directory state: directory
mode: '0755' mode: '0755'
owner: 1000 owner: 1000
group: 1000 group: 1000
loop:
- "{{ paperclip_data_dir }}"
- "{{ paperclip_base_path }}/.config/opencode"
- "{{ paperclip_base_path }}/.local/share/opencode"
become: true become: true
tags: [paperclip, files]
- name: Create OpenCode .local/share directory # - name: Deploy OpenCode auth.json with vLLM credentials
file: # template:
path: "{{ paperclip_base_path }}/.local/share/opencode" # src: opencode-auth.json.j2
state: directory # dest: "{{ paperclip_base_path }}/.local/share/opencode/auth.json"
mode: '0755' # mode: '0600'
owner: 1000 # owner: 1000
group: 1000 # group: 1000
become: yes # become: true
# no_log: true
- name: Deploy OpenCode auth.json with vLLM credentials
template:
src: opencode-auth.json.j2
dest: "{{ paperclip_base_path }}/.local/share/opencode/auth.json"
mode: '0600'
owner: 1000
group: 1000
become: true
no_log: true
- name: Fix directory ownership for container user (UID 1000)
shell: |
chown -R 1000:1000 {{ paperclip_data_dir }}
chown -R 1000:1000 {{ paperclip_base_path }}/pgdata
chown -R 1000:1000 {{ paperclip_base_path }}/.config
become: true
# This file is both the compose interpolation source and the container's
# env_file, so everything here is visible inside the Paperclip container.
# Infrastructure secrets (Cloudflare token, certbot email) live in
# /opt/nginx/.env instead.
- name: Write secrets and configuration to .env - name: Write secrets and configuration to .env
copy: ansible.builtin.copy:
content: | content: |
# Authentication secrets (from vault) # Authentication secrets (from vault)
BETTER_AUTH_SECRET={{ vault_better_auth_secret }} BETTER_AUTH_SECRET={{ vault_better_auth_secret }}
PAPERCLIP_AGENT_JWT_SECRET={{ vault_paperclip_agent_jwt_secret }} PAPERCLIP_AGENT_JWT_SECRET={{ vault_paperclip_agent_jwt_secret }}
# Infrastructure configuration # Inference endpoints
CLOUDFLARE_API_TOKEN={{ vault_cloudflare_api_token }} ILLUMINATIONS_ENDPOINT={{ illuminations_endpoint }}
CERTBOT_EMAIL={{ certbot_email }} ILLUMINATIONS_API_KEY={{ vault_illuminations_api_key }}
PAPERCLIP_DOMAIN={{ paperclip_domain }} FANTASMIC_ENDPOINT={{ fantasmic_endpoint }}
VLLM_ENDPOINT={{ vllm_endpoint }} FANTASMIC_API_KEY={{ vault_fantasmic_api_key }}
VLLM_API_KEY={{ vault_vllm_api_key }}
dest: "{{ paperclip_base_path }}/.env" dest: "{{ paperclip_base_path }}/.env"
mode: '0600' mode: '0600'
owner: root owner: root
group: root group: root
become: true become: true
no_log: true no_log: true
notify: Restart paperclip stack
tags: [paperclip, files]
# No notify: the deploy task below recreates whatever the compose file changed.
- name: Template docker-compose.yml - name: Template docker-compose.yml
template: ansible.builtin.template:
src: docker-compose.yml.j2 src: docker-compose.yml.j2
dest: "{{ paperclip_base_path }}/docker-compose.yml" dest: "{{ paperclip_base_path }}/docker-compose.yml"
mode: '0644' mode: '0644'
owner: root owner: root
group: root group: root
become: true become: true
notify: Restart paperclip stack tags: [paperclip, files]
- name: Template nginx config
template:
src: nginx.conf.j2
dest: "{{ paperclip_base_path }}/nginx.conf"
mode: '0644'
owner: root
group: root
become: true
notify: Reload nginx
- name: Template certbot init script
template:
src: certbot-init.sh.j2
dest: "{{ paperclip_base_path }}/certbot-init.sh"
mode: '0755'
owner: root
group: root
become: true
- name: Template OpenCode config - name: Template OpenCode config
template: ansible.builtin.template:
src: opencode.jsonc.j2 src: opencode.jsonc.j2
dest: "{{ paperclip_base_path }}/.config/opencode/opencode.jsonc" dest: "{{ paperclip_base_path }}/.config/opencode/opencode.jsonc"
mode: '0644' mode: '0644'
@@ -127,43 +86,71 @@
group: 1000 group: 1000
become: true become: true
notify: Restart paperclip stack notify: Restart paperclip stack
tags: [paperclip, files]
- name: Stop and remove old containers # Idempotent: only changed services are recreated. remove_orphans also cleans
shell: | # up the old paperclip-nginx / paperclip-certbot containers if they still exist.
cd {{ paperclip_base_path }} - name: Deploy paperclip stack
/usr/local/bin/docker-compose down --remove-orphans 2>/dev/null || true community.docker.docker_compose_v2:
project_src: "{{ paperclip_base_path }}"
pull: missing
remove_orphans: true
state: present
register: paperclip_deploy
become: true become: true
ignore_errors: true tags: [paperclip, deploy]
- name: Deploy docker compose stack
shell: |
cd {{ paperclip_base_path }}
/usr/local/bin/docker-compose up -d
become: true
register: compose_result
- name: Wait for Paperclip to be ready - name: Wait for Paperclip to be ready
uri: ansible.builtin.uri:
url: "http://localhost:{{ paperclip_port }}/health" url: "http://localhost:{{ paperclip_port }}/health"
status_code: 200 status_code: 200
register: result register: paperclip_health
until: result.status == 200 until: paperclip_health.status == 200
retries: 30 retries: 30
delay: 5 delay: 5
ignore_errors: true ignore_errors: true
tags: [paperclip, deploy, verify]
- name: Wait for nginx to be ready - name: Warn if Paperclip never became healthy
uri: ansible.builtin.debug:
url: "https://{{ paperclip_domain }}/" msg: "WARNING: Paperclip did not report healthy on port {{ paperclip_port }}. Check: docker logs paperclip"
validate_certs: no when: paperclip_health.status | default(0) != 200
status_code: [200, 301, 302] tags: [paperclip, verify]
register: result
until: result.status in [200, 301, 302] # Requires day1_deploy_nginx_proxy.yml to have run first.
retries: 30 - name: Publish Paperclip through nginx-proxy
ansible.builtin.include_role:
name: nginx-proxy
tasks_from: add_vhost
apply:
become: true
tags: [paperclip, nginx]
vars:
vhost_name: paperclip
vhost_domain: "{{ paperclip_domain }}"
vhost_upstream_port: "{{ paperclip_port }}"
vhost_health_path: /health
tags: [paperclip, nginx]
# --resolve keeps this independent of DNS: it tests nginx, the cert and the
# upstream. DNS is a separate (manual) prerequisite.
- name: Verify HTTPS through nginx
ansible.builtin.command: >-
curl -sk -o /dev/null -w '%{http_code}'
--resolve {{ paperclip_domain }}:443:127.0.0.1
https://{{ paperclip_domain }}/
register: paperclip_https
until: paperclip_https.stdout in ['200', '301', '302']
retries: 6
delay: 5 delay: 5
ignore_errors: true changed_when: false
failed_when: false
tags: [paperclip, verify]
- name: Verify Paperclip is accessible via https - name: Report HTTPS result
debug: ansible.builtin.debug:
msg: "Paperclip is accessible at https://{{ paperclip_domain }}" msg: >-
when: result.status is defined and result.status in [200, 301, 302] {{ 'Paperclip is serving at https://' ~ paperclip_domain
if paperclip_https.stdout in ['200', '301', '302']
else 'WARNING: https://' ~ paperclip_domain ~ ' returned ' ~ (paperclip_https.stdout | default('no response')) ~ ' (502 usually means Paperclip is not up yet)' }}
tags: [paperclip, verify]
@@ -1,65 +0,0 @@
#!/bin/sh
set -e
DOMAIN="${PAPERCLIP_DOMAIN}"
CERT_PATH="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem"
EMAIL="${CERTBOT_EMAIL}"
TOKEN="${CLOUDFLARE_API_TOKEN}"
# Verify environment variables
if [ -z "$DOMAIN" ]; then
echo "ERROR: PAPERCLIP_DOMAIN not set"
exit 1
fi
if [ -z "$EMAIL" ]; then
echo "ERROR: CERTBOT_EMAIL not set"
exit 1
fi
if [ -z "$TOKEN" ]; then
echo "ERROR: CLOUDFLARE_API_TOKEN not set"
exit 1
fi
# Check if certificate already exists
if [ -f "$CERT_PATH" ]; then
echo "Certificate for $DOMAIN already exists at $CERT_PATH"
exit 0
fi
echo "Requesting certificate for $DOMAIN via Cloudflare DNS challenge..."
# Create a temporary credentials file (more reliable than env var with certbot)
CREDS_FILE="/tmp/cloudflare.ini"
cat > "$CREDS_FILE" << EOF
dns_cloudflare_api_token = ${TOKEN}
EOF
chmod 600 "$CREDS_FILE"
# Request certificate with Cloudflare DNS plugin
if certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials "$CREDS_FILE" \
--agree-tos \
--no-eff-email \
--email "$EMAIL" \
--non-interactive \
-d "$DOMAIN"; then
echo "Certificate obtained successfully for $DOMAIN"
else
echo "ERROR: Failed to obtain certificate for $DOMAIN"
rm -f "$CREDS_FILE"
exit 1
fi
# Clean up credentials file
rm -f "$CREDS_FILE"
# Verify certificate
if [ -f "$CERT_PATH" ]; then
echo "Certificate verified at $CERT_PATH"
else
echo "ERROR: Certificate not found at $CERT_PATH after successful certbot run"
exit 1
fi
@@ -3,6 +3,8 @@ services:
image: postgres:16-alpine image: postgres:16-alpine
container_name: paperclip-db container_name: paperclip-db
network_mode: host network_mode: host
# network_mode: host would otherwise expose Postgres on every interface
command: ["postgres", "-c", "listen_addresses=127.0.0.1"]
environment: environment:
POSTGRES_USER: paperclip POSTGRES_USER: paperclip
POSTGRES_PASSWORD: paperclip POSTGRES_PASSWORD: paperclip
@@ -23,12 +25,12 @@ services:
env_file: env_file:
- {{ paperclip_base_path }}/.env - {{ paperclip_base_path }}/.env
environment: environment:
HOST: "127.0.0.1" HOST: {{ paperclip_listen_host }}
PAPERCLIP_HOME: /paperclip PAPERCLIP_HOME: /paperclip
PAPERCLIP_PORT: {{ paperclip_port }} PAPERCLIP_PORT: {{ paperclip_port }}
PAPERCLIP_PUBLIC_URL: "https://{{ paperclip_domain }}" PAPERCLIP_PUBLIC_URL: "https://{{ paperclip_domain }}"
PAPERCLIP_DEPLOYMENT_MODE: "{{ paperclip_deployment_mode }}" PAPERCLIP_DEPLOYMENT_MODE: "{{ paperclip_deployment_mode }}"
PAPERCLIP_DEPLOYMENT_EXPOSURE: "private" # PAPERCLIP_DEPLOYMENT_EXPOSURE: "private"
PAPERCLIP_ALLOWED_HOSTNAMES: "{{ paperclip_allowed_hostnames }}" PAPERCLIP_ALLOWED_HOSTNAMES: "{{ paperclip_allowed_hostnames }}"
DATABASE_URL: "postgresql://paperclip:paperclip@localhost:5432/paperclip" DATABASE_URL: "postgresql://paperclip:paperclip@localhost:5432/paperclip"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
@@ -50,43 +52,3 @@ services:
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 60s start_period: 60s
certbot:
image: certbot/dns-cloudflare:latest
container_name: paperclip-certbot
volumes:
- {{ paperclip_certs_dir }}:/etc/letsencrypt
- {{ paperclip_base_path }}/certbot-init.sh:/certbot-init.sh:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
PAPERCLIP_DOMAIN: ${PAPERCLIP_DOMAIN}
entrypoint: /bin/sh
command:
- -c
- |
/certbot-init.sh && while true; do certbot renew --quiet; sleep 12h; done
restart: unless-stopped
nginx:
image: nginx:latest
container_name: paperclip-nginx
network_mode: host
volumes:
- {{ paperclip_base_path }}/nginx.conf:/etc/nginx/nginx.conf:ro
- {{ paperclip_certs_dir }}:/etc/nginx/certs:ro
depends_on:
- certbot
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
paperclip_data:
driver: local
@@ -1,86 +0,0 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100M;
# Redirect HTTP to HTTPS
server {
listen 80;
server_name {{ paperclip_domain }};
return 301 https://$server_name$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name {{ paperclip_domain }};
# SSL certificates from certbot
ssl_certificate /etc/nginx/certs/live/{{ paperclip_domain }}/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/{{ paperclip_domain }}/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://127.0.0.1:{{ paperclip_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering off;
proxy_request_buffering off;
}
# Health check endpoint (no auth)
location /health {
proxy_pass http://127.0.0.1:{{ paperclip_port }}/health;
access_log off;
}
}
}
@@ -2,7 +2,7 @@
"vllm": { "vllm": {
"type": "api", "type": "api",
"key": "{{ vault_vllm_api_key }}" "key": "{{ vault_vllm_api_key }}"
} },
"fantasmic": { "fantasmic": {
"type": "api", "type": "api",
"key": "{{ vault_fantasmic_api_key }}" "key": "{{ vault_fantasmic_api_key }}"
@@ -1,9 +1,9 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"provider": { "provider": {
"vllm": { "illuminations": {
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"name": "vLLM (Local)", "name": "illuminations",
"models": { "models": {
"gemma-4-26b": { "gemma-4-26b": {
"id": "Gemma-4-26B-A4B-it-AWQ", "id": "Gemma-4-26B-A4B-it-AWQ",
@@ -11,9 +11,9 @@
} }
}, },
"options": { "options": {
"baseURL": "{{ vllm_endpoint }}/v1", "baseURL": "{{ illuminations_endpoint }}/v1",
"headers": { "headers": {
"Authorization": "Bearer {{ vault_vllm_api_key }}" "Authorization": "Bearer {env:ILLUMINATIONS_API_KEY}"
} }
} }
} }