[{"data":1,"prerenderedAt":5860},["ShallowReactive",2],{"update-\u002Fde\u002Fupdates\u002Fmobile-performance":3,"updates-index-de":318},{"id":4,"title":5,"body":6,"date":308,"description":309,"extension":310,"meta":311,"navigation":312,"path":313,"seo":314,"stem":315,"tag":316,"__hash__":317},"updates_de\u002Fde\u002Fupdates\u002Fmobile-performance.md","Von 5 Sekunden auf unter 0,5",{"type":7,"value":8,"toc":289},"minimark",[9,18,23,26,33,41,45,71,74,106,109,113,122,125,135,138,169,181,185,188,200,204,207,213,216,219,223,232,257,261,264,276,282,286],[10,11,12,13,17],"p",{},"Am Desktop hat die Landingpage sofort geladen. Am Handy war sie furchtbar langsam, über fünf Sekunden bis sie da war. Schuld war am Ende ein einziger CSS-Effekt: ",[14,15,16],"code",{},"blur()",". Hier der ganze Weg von der Diagnose bis zu dem, was unter der Haube wirklich passiert.",[19,20,22],"h2",{"id":21},"das-problem","Das Problem",[10,24,25],{},"Am Handy war die Seite einfach langsam. Das Laden dauerte spürbar lang, am Desktop war es blitzschnell. Die üblichen Verdächtigen, zu große Bilder, zu viel JavaScript, langsamer Server, waren es nicht.",[10,27,28,29,32],{},"Am Ende war es der Hintergrund. Dort liegen ein paar große, weiche Lichtflecken (Glows), umgesetzt mit ",[14,30,31],{},"filter: blur()",". Ein Element war über 1000px breit, mit einem Blur-Radius von 100px.",[34,35,38],"callout",{"color":36,"icon":37},"primary","i-lucide-zap-off",[10,39,40],{},"Über fünf Sekunden bis zum ersten Frame ist für eine Landingpage tödlich. Genau dort entscheidet sich, ob jemand bleibt.",[19,42,44],{"id":43},"warum-blur-so-teuer-ist","Warum Blur so teuer ist",[10,46,47,49,50,57,58,62,63,66,67,70],{},[14,48,16],{}," ist eine ",[51,52,56],"a",{"href":53,"rel":54},"https:\u002F\u002Fdeveloper.chrome.com\u002Fblog\u002Fanimated-blur",[55],"nofollow","Convolution",": Für jeden Output-Pixel werden die umliegenden Input-Pixel gewichtet zusammengerechnet. Die Kosten skalieren mit Fläche ",[59,60,61],"strong",{},"und"," Radius, ein ",[14,64,65],{},"blur(100px)"," ist also drastisch teurer als ein ",[14,68,69],{},"blur(5px)",".",[10,72,73],{},"Im Detail läuft pro Frame ungefähr das ab:",[75,76,78,83,86,90,93,97],"steps",{"level":77},"3",[79,80,82],"h3",{"id":81},"render-to-texture","Render-to-Texture",[10,84,85],{},"Das Element wird zuerst in eine eigene Off-Screen-Textur gerendert. Allein das promotet es zu einer eigenen Compositing-Layer und kostet GPU-Speicher.",[79,87,89],{"id":88},"separable-gaussian","Separable Gaussian",[10,91,92],{},"Ein 2D-Gaussian lässt sich in zwei 1D-Passes zerlegen (horizontal, dann vertikal). Das senkt die Kosten von O(n²) auf O(n) pro Pixel, ist also schon die optimierte Variante.",[79,94,96],{"id":95},"downsampling","Downsampling",[10,98,99,100,105],{},"Bei großen Radien rechnet die Engine nicht in voller Auflösung. Skia (Chrome) ",[51,101,104],{"href":102,"rel":103},"https:\u002F\u002Fapi.skia.org\u002FclassSkImageFilters.html",[55],"skaliert ab einem Sigma > 4 herunter",", blurrt auf der kleineren Buffer-Größe und skaliert wieder hoch.",[10,107,108],{},"Trotz all dieser Optimierungen bleibt eine riesige Fläche übrig, die vor dem ersten Paint fertig sein muss. Auf dem Desktop fällt das nicht auf. Eine Mobile-GPU stemmt das deutlich langsamer, und genau das hat den ersten Frame um Sekunden verzögert.",[19,110,112],{"id":111},"warum-es-mobile-gpus-besonders-trifft","Warum es Mobile-GPUs besonders trifft",[10,114,115,116,121],{},"Mobile-GPUs arbeiten fast alle ",[51,117,120],{"href":118,"rel":119},"https:\u002F\u002Fdeveloper.samsung.com\u002Fgalaxy-gamedev\u002Fresources\u002Farticles\u002Fgpu-framebuffer.html",[55],"Tile-Based",": Der Framebuffer wird in kleine Tiles zerlegt, die im schnellen On-Chip-Speicher gerendert werden. Das spart enorm Memory-Bandwidth, funktioniert aber nur, solange ein Tile für sich allein berechenbar ist.",[10,123,124],{},"Ein Blur bricht genau diese Annahme. Die Convolution muss Pixel über die Tile-Grenze hinaus lesen. Die GPU kann den Effekt also nicht tile-lokal halten, sondern muss in den langsamen System-Memory schreiben und wieder lesen. Auf Hardware mit ohnehin knapper Fill-Rate und Bandwidth ist das der teuerste Fall.",[19,126,128,131,132],{"id":127},"filter-ist-nicht-backdrop-filter",[14,129,130],{},"filter"," ist nicht ",[14,133,134],{},"backdrop-filter",[10,136,137],{},"Im Header steckte noch ein zweiter Blur, und der ist eine ganz andere Baustelle. Die beiden werden oft verwechselt:",[139,140,141,153],"ul",{},[142,143,144,148,149,152],"li",{},[59,145,146],{},[14,147,31],{}," blurrt die ",[59,150,151],{},"eigenen Pixel"," des Elements. Inhalt dahinter ist egal.",[142,154,155,164,165,168],{},[59,156,157],{},[51,158,161],{"href":159,"rel":160},"https:\u002F\u002Fwww.w3tweaks.com\u002Fcss\u002Fcss-filter-backdrop-filter\u002F",[55],[14,162,163],{},"backdrop-filter: blur()"," blurrt ",[59,166,167],{},"alles, was dahinter liegt",", und liest diese Pixel bei jedem Frame neu.",[10,170,171,172,177,178,180],{},"Letzteres ist beim Scrollen brutal: Der Backdrop ändert sich ständig, also wird der Blur permanent neu berechnet. ",[51,173,176],{"href":174,"rel":175},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fvitepress\u002Fissues\u002F1049",[55],"Genau das ruckelt sichtbar",", bei älteren Firefox-Builds unter Linux. Ein fixer, halbtransparenter Header mit ",[14,179,134],{}," ist einer der häufigsten Jank-Verursacher überhaupt.",[19,182,184],{"id":183},"und-blur-animieren","Und Blur animieren?",[10,186,187],{},"Kurz: nicht den Radius animieren. Jeder Frame triggert die volle Convolution auf der GPU und sprengt das 16ms-Frame-Budget, das Ergebnis liegt weit unter 60fps.",[10,189,190,191,195,196,199],{},"Der ",[51,192,194],{"href":53,"rel":193},[55],"Trick von Chrome",": ein paar vorberechnete Kopien mit exponentiell steigendem Blur stapeln und per ",[14,197,198],{},"opacity"," zwischen ihnen cross-faden. Opacity ist compositor-only und damit billig. Man animiert also nicht den Blur, sondern täuscht ihn vor.",[19,201,203],{"id":202},"die-lösung","Die Lösung",[10,205,206],{},"Im konkreten Fall war sie fast enttäuschend simpel: den Blur ganz weglassen.",[10,208,209,210,212],{},"Statt einen scharfen Kreis teuer zu blurren, sind die Glows jetzt mehrstufige Radial-Gradients, die von Haus aus weich sind. Optisch praktisch identisch, aber ohne Filter, ohne extra Layer, ohne Convolution. Den ",[14,211,134],{}," im Header habe ich gleich mit rausgeworfen.",[10,214,215],{},"Andere valide Wege, je nach Fall: ein vorgeblurtes Bild als Asset ausliefern, den Radius klein halten, oder bewusst auf einem kleineren Element blurren.",[10,217,218],{},"Das Ergebnis: Statt über fünf Sekunden ist die Seite jetzt in unter einer halben Sekunde da. Zehnmal schneller, ohne dass man dem Design den Unterschied ansieht.",[19,220,222],{"id":221},"wie-man-so-etwas-findet","Wie man so etwas findet",[10,224,225,226,231],{},"In den Chrome DevTools führt der ",[51,227,230],{"href":228,"rel":229},"https:\u002F\u002Fdeveloper.chrome.com\u002Fdocs\u002Fdevtools\u002Frendering\u002Fperformance",[55],"Rendering-Tab"," schnell zur Ursache:",[139,233,234,240,250],{},[142,235,236,239],{},[59,237,238],{},"Paint Flashing"," zeigt grün, was neu gezeichnet wird. Ein Header, der beim Scrollen dauergrün blinkt, ist ein eindeutiges Signal.",[142,241,242,245,246,249],{},[59,243,244],{},"Layer Borders"," und der ",[59,247,248],{},"Layers-Tab"," zeigen, welche Elemente eine eigene Compositing-Layer bekommen, dort taucht jeder Blur auf.",[142,251,252,253,256],{},"Im ",[59,254,255],{},"Performance-Panel"," sieht man die langen Paint- und GPU-Tasks direkt im Flame-Chart.",[19,258,260],{"id":259},"was-ältere-browser-damit-zu-tun-haben","Was ältere Browser damit zu tun haben",[10,262,263],{},"Mein erster Reflex war: \"Blur wird doch überall unterstützt.\" Stimmt laut Caniuse, sagt aber nichts über Performance.",[10,265,266,269,270,275],{},[59,267,268],{},"GPU-Beschleunigung kann ganz wegfallen."," Firefox rendert über WebRender, fällt aber auf vielen Linux-Setups (alte Intel-GPUs, proprietäre Nvidia-Treiber) auf ",[51,271,274],{"href":272,"rel":273},"https:\u002F\u002Fwiki.archlinux.org\u002Ftitle\u002FFirefox\u002FTweaks",[55],"Software-Rendering"," zurück. Dann läuft die Blur-Convolution auf der CPU, und aus \"unmerklich\" wird \"ruckelt\".",[34,277,279],{"color":36,"icon":278},"i-lucide-lightbulb",[10,280,281],{},"Ein grüner Caniuse-Balken heißt \"der Browser kennt das Feature\", nicht \"er macht es schnell, auch ohne GPU-Beschleunigung\".",[19,283,285],{"id":284},"was-ich-mitnehme","Was ich mitnehme",[10,287,288],{},"Der schönste Effekt taugt nichts, wenn die Seite dadurch langsam wird. Blur ist teuer in der Berechnung, Layer-erzeugend, mit Nebenwirkungen auf Stacking und Positionierung, und besonders fragil auf Mobile und ohne GPU-Beschleunigung. Und immer auf echten, eher schwachen Geräten testen, nicht nur auf der dicken Entwickler-Maschine.",{"title":290,"searchDepth":291,"depth":291,"links":292},"",2,[293,294,300,301,303,304,305,306,307],{"id":21,"depth":291,"text":22},{"id":43,"depth":291,"text":44,"children":295},[296,298,299],{"id":81,"depth":297,"text":82},3,{"id":88,"depth":297,"text":89},{"id":95,"depth":297,"text":96},{"id":111,"depth":291,"text":112},{"id":127,"depth":291,"text":302},"filter ist nicht backdrop-filter",{"id":183,"depth":291,"text":184},{"id":202,"depth":291,"text":203},{"id":221,"depth":291,"text":222},{"id":259,"depth":291,"text":260},{"id":284,"depth":291,"text":285},"2026-06-12","Ein Deep Dive, warum CSS-Blur einer der teuersten Effekte im Browser ist und wie ein einziger Filter die Landingpage am Handy ausgebremst hat.","md",{},true,"\u002Fde\u002Fupdates\u002Fmobile-performance",{"title":5,"description":309},"de\u002Fupdates\u002Fmobile-performance","Performance","3aQVHjHahHgz4DIKYGtXZwOk6-3IiFDCydkaS8-HkUo",[319,2041,2938,3188,3570,3694,3943,4217,4376,4668,4874,5123,5403,5585,5671,5747],{"id":320,"title":321,"body":322,"date":2033,"description":2034,"extension":310,"meta":2035,"navigation":312,"path":2036,"seo":2037,"stem":2038,"tag":2039,"__hash__":2040},"updates_de\u002Fde\u002Fupdates\u002Fintegrations-rewrite.md","Eine Schnittstelle für alle Integrationen",{"type":7,"value":323,"toc":2013},[324,349,358,361,367,369,373,382,386,389,454,457,461,465,480,690,712,771,775,789,892,901,905,908,912,915,1146,1158,1162,1168,1354,1371,1375,1378,1571,1589,1593,1597,1606,1609,1812,1815,1819,1828,1971,1974,1983,1987,1990,2009],[325,326,327],"note",{},[10,328,329,330,334,335,339,340,344,345,70],{},"Dieser Post ist ein Blick unter die Haube. Er erklärt, wie die Integrationen im Server aufgebaut sind, nicht was man damit macht. Was Knecht macht, steht auf der ",[51,331,333],{"href":332},"\u002F","Startseite",", wie er auf GitHub und in Jira arbeitet, in ",[51,336,338],{"href":337},"\u002Fupdates\u002Fsessions-and-mentions","Knecht antwortet jetzt auf GitHub",". Die Einrichtung steht in den Docs zu ",[51,341,343],{"href":342},"\u002Fdocs\u002Fintegrations\u002Fgithub","GitHub"," und ",[51,346,348],{"href":347},"\u002Fdocs\u002Fintegrations\u002Fjira","Jira",[10,350,351,352,357],{},"Knecht hat mit einer Integration angefangen, GitHub. Dann kam Jira, und Jira funktioniert anders. Es gibt Tickets statt Pull Requests und ",[51,353,356],{"href":354,"rel":355},"https:\u002F\u002Fsupport.atlassian.com\u002Fjira-cloud-administration\u002Fdocs\u002Fwhat-are-issue-statuses-priorities-and-resolutions\u002F",[55],"Status-Kategorien"," statt offen und geschlossen. Ein Ticket kann keinen Pull Request anzeigen, also schreibt Knecht das Ergebnis als Kommentar zurück. Jira bekam deshalb eigenen Code. Jeder weitere Issue-Tracker hätte diesen Code noch einmal bekommen. Dieser Post erzählt, wie wir daraus eine gemeinsame Schnittstelle gemacht haben.",[10,359,360],{},"So sieht das Ergebnis in Jira aus. Ein Ticket bekommt ein Label, Knecht nimmt es sich, arbeitet in seiner Umgebung und meldet sich mit einem Pull Request und einem Kommentar zurück.",[362,363],"update-youtube",{"caption":364,"id":365,"title":366},"Ein Jira-Ticket bekommt ein Label, Knecht arbeitet und meldet sich mit PR und Kommentar zurück","9FFm1AlmRn4","Knecht Works on Jira",[19,368,22],{"id":21},[79,370,372],{"id":371},"kopierte-infrastruktur","Kopierte Infrastruktur",[10,374,375,376,381],{},"Jedes Tool braucht dieselben vier Dinge. Knecht muss Zugangsdaten speichern, die Verbindung in den Settings anzeigen, Labels und Status für das Trigger-Formular laden und Webhooks empfangen. Ein ",[51,377,380],{"href":378,"rel":379},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fwebhooks\u002Fabout-webhooks",[55],"Webhook"," ist die HTTP-Anfrage, mit der das Tool Knecht über eine Änderung informiert. Jira hatte dafür eigene API-Routen, eine eigene Datenbanktabelle und ein eigenes Settings-Panel. Ein zweiter Tracker hätte all das kopiert und nur Feldnamen und Texte ausgetauscht.",[79,383,385],{"id":384},"tool-namen-im-code","Tool-Namen im Code",[10,387,388],{},"Schlecht war, dass der Rest von Knecht die Tools beim Namen kannte. Überall, wo eine Integration eine Rolle spielte, stand eine Abfrage wie diese:",[390,391,395],"pre",{"className":392,"code":393,"language":394,"meta":290,"style":290},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","if (tool === 'jira') {\n  await postResultComment(ticket)\n}\n","ts",[14,396,397,430,449],{"__ignoreMap":290},[398,399,402,406,410,414,417,421,424,427],"span",{"class":400,"line":401},"line",1,[398,403,405],{"class":404},"s7zQu","if",[398,407,409],{"class":408},"sTEyZ"," (tool ",[398,411,413],{"class":412},"sMK4o","===",[398,415,416],{"class":412}," '",[398,418,420],{"class":419},"sfazB","jira",[398,422,423],{"class":412},"'",[398,425,426],{"class":408},") ",[398,428,429],{"class":412},"{\n",[398,431,432,435,439,443,446],{"class":400,"line":291},[398,433,434],{"class":404},"  await",[398,436,438],{"class":437},"s2Zo4"," postResultComment",[398,440,442],{"class":441},"swJcz","(",[398,444,445],{"class":408},"ticket",[398,447,448],{"class":441},")\n",[398,450,451],{"class":400,"line":297},[398,452,453],{"class":412},"}\n",[10,455,456],{},"Der Code, der einen Run abschließt, wusste, dass Jira einen Ergebnis-Kommentar braucht. Die Behandlung von Mentions kannte nur GitHub. Für jedes neue Tool hätten wir all diese Stellen finden und erweitern müssen.",[19,458,460],{"id":459},"die-schnittstelle","Die Schnittstelle",[79,462,464],{"id":463},"fähigkeiten-statt-namen","Fähigkeiten statt Namen",[10,466,467,468,473,474,479],{},"Jetzt ist eine Integration ein Objekt, das eine feste ",[51,469,472],{"href":470,"rel":471},"https:\u002F\u002Fwww.typescriptlang.org\u002Fdocs\u002Fhandbook\u002F2\u002Fobjects.html",[55],"TypeScript-Schnittstelle"," erfüllt. Der Rest von Knecht fragt nicht mehr, welches Tool er vor sich hat, sondern was es kann. Wer Design Patterns kennt, erkennt darin einen ",[51,475,478],{"href":476,"rel":477},"https:\u002F\u002Frefactoring.guru\u002Fdesign-patterns\u002Fadapter",[55],"Adapter",". Stark vereinfacht sieht die Schnittstelle so aus:",[390,481,483],{"className":392,"code":482,"language":394,"meta":290,"style":290},"interface Integration {\n  webhook: {\n    verify(request): boolean\n    parse(request): Delivery\n  }\n  comment(ticket, text): Promise\u003Cvoid>\n  labels?: { list(ticket), apply(ticket, names) }\n  statuses?: { list(ticket), move(ticket, status) }\n  assignee?: { take(ticket), handBack(ticket, person) }\n}\n",[14,484,485,498,508,525,540,546,576,616,650,685],{"__ignoreMap":290},[398,486,487,491,495],{"class":400,"line":401},[398,488,490],{"class":489},"spNyl","interface",[398,492,494],{"class":493},"sBMFI"," Integration",[398,496,497],{"class":412}," {\n",[398,499,500,503,506],{"class":400,"line":291},[398,501,502],{"class":441},"  webhook",[398,504,505],{"class":412},":",[398,507,497],{"class":412},[398,509,510,513,515,519,522],{"class":400,"line":297},[398,511,512],{"class":441},"    verify",[398,514,442],{"class":412},[398,516,518],{"class":517},"sHdIc","request",[398,520,521],{"class":412},"):",[398,523,524],{"class":493}," boolean\n",[398,526,528,531,533,535,537],{"class":400,"line":527},4,[398,529,530],{"class":441},"    parse",[398,532,442],{"class":412},[398,534,518],{"class":517},[398,536,521],{"class":412},[398,538,539],{"class":493}," Delivery\n",[398,541,543],{"class":400,"line":542},5,[398,544,545],{"class":412},"  }\n",[398,547,549,552,554,556,559,562,564,567,570,573],{"class":400,"line":548},6,[398,550,551],{"class":441},"  comment",[398,553,442],{"class":412},[398,555,445],{"class":517},[398,557,558],{"class":412},",",[398,560,561],{"class":517}," text",[398,563,521],{"class":412},[398,565,566],{"class":493}," Promise",[398,568,569],{"class":412},"\u003C",[398,571,572],{"class":493},"void",[398,574,575],{"class":412},">\n",[398,577,579,582,585,588,591,593,595,598,601,603,605,607,610,613],{"class":400,"line":578},7,[398,580,581],{"class":441},"  labels",[398,583,584],{"class":412},"?:",[398,586,587],{"class":412}," {",[398,589,590],{"class":441}," list",[398,592,442],{"class":412},[398,594,445],{"class":517},[398,596,597],{"class":412},"),",[398,599,600],{"class":441}," apply",[398,602,442],{"class":412},[398,604,445],{"class":517},[398,606,558],{"class":412},[398,608,609],{"class":517}," names",[398,611,612],{"class":412},")",[398,614,615],{"class":412}," }\n",[398,617,619,622,624,626,628,630,632,634,637,639,641,643,646,648],{"class":400,"line":618},8,[398,620,621],{"class":441},"  statuses",[398,623,584],{"class":412},[398,625,587],{"class":412},[398,627,590],{"class":441},[398,629,442],{"class":412},[398,631,445],{"class":517},[398,633,597],{"class":412},[398,635,636],{"class":441}," move",[398,638,442],{"class":412},[398,640,445],{"class":517},[398,642,558],{"class":412},[398,644,645],{"class":517}," status",[398,647,612],{"class":412},[398,649,615],{"class":412},[398,651,653,656,658,660,663,665,667,669,672,674,676,678,681,683],{"class":400,"line":652},9,[398,654,655],{"class":441},"  assignee",[398,657,584],{"class":412},[398,659,587],{"class":412},[398,661,662],{"class":441}," take",[398,664,442],{"class":412},[398,666,445],{"class":517},[398,668,597],{"class":412},[398,670,671],{"class":441}," handBack",[398,673,442],{"class":412},[398,675,445],{"class":517},[398,677,558],{"class":412},[398,679,680],{"class":517}," person",[398,682,612],{"class":412},[398,684,615],{"class":412},[398,686,688],{"class":400,"line":687},10,[398,689,453],{"class":412},[10,691,692,695,696,701,702,705,706,711],{},[14,693,694],{},"verify"," prüft die ",[51,697,700],{"href":698,"rel":699},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fwebhooks\u002Fusing-webhooks\u002Fvalidating-webhook-deliveries",[55],"Signatur"," eines Webhooks, also ob er wirklich vom Tool kommt. ",[14,703,704],{},"parse"," übersetzt ihn in eine Form, die Knecht versteht. Kommentieren kann jedes Tool, alles mit Fragezeichen ist ",[51,707,710],{"href":708,"rel":709},"https:\u002F\u002Fwww.typescriptlang.org\u002Fdocs\u002Fhandbook\u002F2\u002Fobjects.html#optional-properties",[55],"optional",". Aus der Abfrage oben wird damit:",[390,713,715],{"className":392,"code":714,"language":394,"meta":290,"style":290},"if (integration.labels) {\n  await integration.labels.apply(ticket, ['bug'])\n}\n",[14,716,717,731,767],{"__ignoreMap":290},[398,718,719,721,724,726,729],{"class":400,"line":401},[398,720,405],{"class":404},[398,722,723],{"class":408}," (integration",[398,725,70],{"class":412},[398,727,728],{"class":408},"labels) ",[398,730,429],{"class":412},[398,732,733,735,738,740,743,745,748,750,752,754,757,759,762,764],{"class":400,"line":291},[398,734,434],{"class":404},[398,736,737],{"class":408}," integration",[398,739,70],{"class":412},[398,741,742],{"class":408},"labels",[398,744,70],{"class":412},[398,746,747],{"class":437},"apply",[398,749,442],{"class":441},[398,751,445],{"class":408},[398,753,558],{"class":412},[398,755,756],{"class":441}," [",[398,758,423],{"class":412},[398,760,761],{"class":419},"bug",[398,763,423],{"class":412},[398,765,766],{"class":441},"])\n",[398,768,769],{"class":400,"line":297},[398,770,453],{"class":412},[79,772,774],{"id":773},"ein-weg-für-jeden-webhook","Ein Weg für jeden Webhook",[10,776,777,779,780,783,784,788],{},[14,778,704],{}," liefert für jedes Tool dieselbe Form zurück. Ein Webhook kann einen Kommentar enthalten, der Knecht direkt anspricht (eine ",[51,781,782],{"href":337},"Mention","), eine Statusänderung am Ticket oder ein Ereignis, auf das ein ",[51,785,787],{"href":786},"\u002Fdocs\u002Fusage\u002Ftriggers","Trigger"," reagiert. Ein Trigger ist die Regel, die einen Workflow startet. Oft steckt mehreres in einem Webhook, ein geschlossenes Ticket ist zum Beispiel eine Statusänderung und vielleicht auch ein Trigger-Ereignis. Was Knecht damit macht, steht an genau einer Stelle:",[390,790,792],{"className":392,"code":791,"language":394,"meta":290,"style":290},"const delivery = await integration.webhook.parse(request)\n\nif (delivery.comment) handleMention(delivery.comment)\nif (delivery.statusChange) syncTicketStatus(delivery.statusChange)\nif (delivery.event) startMatchingWorkflows(delivery.event)\n",[14,793,794,822,827,850,871],{"__ignoreMap":290},[398,795,796,799,802,805,808,810,812,815,817,819],{"class":400,"line":401},[398,797,798],{"class":489},"const",[398,800,801],{"class":408}," delivery ",[398,803,804],{"class":412},"=",[398,806,807],{"class":404}," await",[398,809,737],{"class":408},[398,811,70],{"class":412},[398,813,814],{"class":408},"webhook",[398,816,70],{"class":412},[398,818,704],{"class":437},[398,820,821],{"class":408},"(request)\n",[398,823,824],{"class":400,"line":291},[398,825,826],{"emptyLinePlaceholder":312},"\n",[398,828,829,831,834,836,839,842,845,847],{"class":400,"line":297},[398,830,405],{"class":404},[398,832,833],{"class":408}," (delivery",[398,835,70],{"class":412},[398,837,838],{"class":408},"comment) ",[398,840,841],{"class":437},"handleMention",[398,843,844],{"class":408},"(delivery",[398,846,70],{"class":412},[398,848,849],{"class":408},"comment)\n",[398,851,852,854,856,858,861,864,866,868],{"class":400,"line":527},[398,853,405],{"class":404},[398,855,833],{"class":408},[398,857,70],{"class":412},[398,859,860],{"class":408},"statusChange) ",[398,862,863],{"class":437},"syncTicketStatus",[398,865,844],{"class":408},[398,867,70],{"class":412},[398,869,870],{"class":408},"statusChange)\n",[398,872,873,875,877,879,882,885,887,889],{"class":400,"line":542},[398,874,405],{"class":404},[398,876,833],{"class":408},[398,878,70],{"class":412},[398,880,881],{"class":408},"event) ",[398,883,884],{"class":437},"startMatchingWorkflows",[398,886,844],{"class":408},[398,888,70],{"class":412},[398,890,891],{"class":408},"event)\n",[10,893,894,895,900],{},"Die Integration muss nur wissen, wie ihr Tool Daten schickt. Was daraus folgt, entscheidet Knecht für alle Tools gleich. Die Idee dahinter beschreibt Alexis King in ",[51,896,899],{"href":897,"rel":898},"https:\u002F\u002Flexi-lambda.github.io\u002Fblog\u002F2019\u002F11\u002F05\u002Fparse-don-t-validate\u002F",[55],"Parse, don't validate",": Fremde Daten werden einmal an der Grenze in einen eigenen Typ übersetzt, danach arbeitet der Code nur noch mit diesem Typ.",[19,902,904],{"id":903},"unterschiede-als-daten","Unterschiede als Daten",[10,906,907],{},"Was sich zwischen zwei Tools noch unterscheidet, sind vor allem Begriffe und Formate. Ein Ticket heißt anders, andere Status gelten als erledigt, der Titel steht an einer anderen Stelle im Payload. Das beschreibt jede Integration als Daten, die Logik dazu gibt es nur einmal.",[79,909,911],{"id":910},"der-tracker","Der Tracker",[10,913,914],{},"Ein Tracker ist ein Tool, in dem ein Team seine Tickets verwaltet, etwa Jira. Was Knecht über ihn wissen muss, passt in eine kurze Beschreibung:",[390,916,918],{"className":392,"code":917,"language":394,"meta":290,"style":290},"const jira = {\n  noun: 'ticket',\n  statusGroups: { new: 'To Do', indeterminate: 'In Progress', done: 'Done' },\n  closedGroups: ['done'],\n\n  toTicket: issue => ({\n    title: issue.fields.summary,\n    body: adfToMarkdown(issue.fields.description),\n    status: issue.fields.status.name,\n    labels: issue.fields.labels,\n  }),\n}\n",[14,919,920,931,947,999,1020,1024,1042,1063,1087,1112,1131,1141],{"__ignoreMap":290},[398,921,922,924,927,929],{"class":400,"line":401},[398,923,798],{"class":489},[398,925,926],{"class":408}," jira ",[398,928,804],{"class":412},[398,930,497],{"class":412},[398,932,933,936,938,940,942,944],{"class":400,"line":291},[398,934,935],{"class":441},"  noun",[398,937,505],{"class":412},[398,939,416],{"class":412},[398,941,445],{"class":419},[398,943,423],{"class":412},[398,945,946],{"class":412},",\n",[398,948,949,952,954,956,959,961,963,966,968,970,973,975,977,980,982,984,987,989,991,994,996],{"class":400,"line":297},[398,950,951],{"class":441},"  statusGroups",[398,953,505],{"class":412},[398,955,587],{"class":412},[398,957,958],{"class":441}," new",[398,960,505],{"class":412},[398,962,416],{"class":412},[398,964,965],{"class":419},"To Do",[398,967,423],{"class":412},[398,969,558],{"class":412},[398,971,972],{"class":441}," indeterminate",[398,974,505],{"class":412},[398,976,416],{"class":412},[398,978,979],{"class":419},"In Progress",[398,981,423],{"class":412},[398,983,558],{"class":412},[398,985,986],{"class":441}," done",[398,988,505],{"class":412},[398,990,416],{"class":412},[398,992,993],{"class":419},"Done",[398,995,423],{"class":412},[398,997,998],{"class":412}," },\n",[398,1000,1001,1004,1006,1008,1010,1013,1015,1018],{"class":400,"line":527},[398,1002,1003],{"class":441},"  closedGroups",[398,1005,505],{"class":412},[398,1007,756],{"class":408},[398,1009,423],{"class":412},[398,1011,1012],{"class":419},"done",[398,1014,423],{"class":412},[398,1016,1017],{"class":408},"]",[398,1019,946],{"class":412},[398,1021,1022],{"class":400,"line":542},[398,1023,826],{"emptyLinePlaceholder":312},[398,1025,1026,1029,1031,1034,1037,1040],{"class":400,"line":548},[398,1027,1028],{"class":437},"  toTicket",[398,1030,505],{"class":412},[398,1032,1033],{"class":517}," issue",[398,1035,1036],{"class":489}," =>",[398,1038,1039],{"class":408}," (",[398,1041,429],{"class":412},[398,1043,1044,1047,1049,1051,1053,1056,1058,1061],{"class":400,"line":578},[398,1045,1046],{"class":441},"    title",[398,1048,505],{"class":412},[398,1050,1033],{"class":408},[398,1052,70],{"class":412},[398,1054,1055],{"class":408},"fields",[398,1057,70],{"class":412},[398,1059,1060],{"class":408},"summary",[398,1062,946],{"class":412},[398,1064,1065,1068,1070,1073,1076,1078,1080,1082,1085],{"class":400,"line":618},[398,1066,1067],{"class":441},"    body",[398,1069,505],{"class":412},[398,1071,1072],{"class":437}," adfToMarkdown",[398,1074,1075],{"class":408},"(issue",[398,1077,70],{"class":412},[398,1079,1055],{"class":408},[398,1081,70],{"class":412},[398,1083,1084],{"class":408},"description)",[398,1086,946],{"class":412},[398,1088,1089,1092,1094,1096,1098,1100,1102,1105,1107,1110],{"class":400,"line":652},[398,1090,1091],{"class":441},"    status",[398,1093,505],{"class":412},[398,1095,1033],{"class":408},[398,1097,70],{"class":412},[398,1099,1055],{"class":408},[398,1101,70],{"class":412},[398,1103,1104],{"class":408},"status",[398,1106,70],{"class":412},[398,1108,1109],{"class":408},"name",[398,1111,946],{"class":412},[398,1113,1114,1117,1119,1121,1123,1125,1127,1129],{"class":400,"line":687},[398,1115,1116],{"class":441},"    labels",[398,1118,505],{"class":412},[398,1120,1033],{"class":408},[398,1122,70],{"class":412},[398,1124,1055],{"class":408},[398,1126,70],{"class":412},[398,1128,742],{"class":408},[398,1130,946],{"class":412},[398,1132,1134,1137,1139],{"class":400,"line":1133},11,[398,1135,1136],{"class":412},"  }",[398,1138,612],{"class":408},[398,1140,946],{"class":412},[398,1142,1144],{"class":400,"line":1143},12,[398,1145,453],{"class":412},[10,1147,1148,1149,1152,1153,1157],{},"Der obere Teil sind die Begriffe des Tools, also wie ein Ticket heißt, welche Status-Kategorien es gibt und welche davon als erledigt gelten. ",[14,1150,1151],{},"toTicket"," übersetzt die Daten aus dem Webhook in ein einheitliches Ticket. Ab da entscheidet für alle Tracker derselbe Code, ob ein Trigger startet, und auch der Text, den der ",[51,1154,1156],{"href":1155},"\u002Fdocs\u002Fusage\u002Fagent","Agent"," über das Ticket liest, entsteht daraus.",[79,1159,1161],{"id":1160},"das-trigger-formular","Das Trigger-Formular",[10,1163,252,1164,1167],{},[51,1165,1166],{"href":786},"Trigger-Dialog"," wählt man, bei welchem Ereignis ein Workflow startet. Welche Ereignisse es gibt, beschreibt jede Integration selbst:",[390,1169,1171],{"className":392,"code":1170,"language":394,"meta":290,"style":290},"trigger: {\n  form: {\n    events: [\n      { label: 'Label added', options: 'labels' },\n      { label: 'Status reached', options: 'statuses' },\n    ],\n  },\n  options: {\n    labels: () => jira.get('\u002Flabel'),\n    statuses: () => jira.get('\u002Fstatus'),\n  },\n}\n",[14,1172,1173,1182,1191,1201,1233,1263,1270,1275,1284,1316,1346,1350],{"__ignoreMap":290},[398,1174,1175,1178,1180],{"class":400,"line":401},[398,1176,1177],{"class":493},"trigger",[398,1179,505],{"class":412},[398,1181,497],{"class":412},[398,1183,1184,1187,1189],{"class":400,"line":291},[398,1185,1186],{"class":493},"  form",[398,1188,505],{"class":412},[398,1190,497],{"class":412},[398,1192,1193,1196,1198],{"class":400,"line":297},[398,1194,1195],{"class":493},"    events",[398,1197,505],{"class":412},[398,1199,1200],{"class":441}," [\n",[398,1202,1203,1206,1209,1211,1213,1216,1218,1220,1223,1225,1227,1229,1231],{"class":400,"line":527},[398,1204,1205],{"class":412},"      {",[398,1207,1208],{"class":441}," label",[398,1210,505],{"class":412},[398,1212,416],{"class":412},[398,1214,1215],{"class":419},"Label added",[398,1217,423],{"class":412},[398,1219,558],{"class":412},[398,1221,1222],{"class":441}," options",[398,1224,505],{"class":412},[398,1226,416],{"class":412},[398,1228,742],{"class":419},[398,1230,423],{"class":412},[398,1232,998],{"class":412},[398,1234,1235,1237,1239,1241,1243,1246,1248,1250,1252,1254,1256,1259,1261],{"class":400,"line":542},[398,1236,1205],{"class":412},[398,1238,1208],{"class":441},[398,1240,505],{"class":412},[398,1242,416],{"class":412},[398,1244,1245],{"class":419},"Status reached",[398,1247,423],{"class":412},[398,1249,558],{"class":412},[398,1251,1222],{"class":441},[398,1253,505],{"class":412},[398,1255,416],{"class":412},[398,1257,1258],{"class":419},"statuses",[398,1260,423],{"class":412},[398,1262,998],{"class":412},[398,1264,1265,1268],{"class":400,"line":548},[398,1266,1267],{"class":441},"    ]",[398,1269,946],{"class":412},[398,1271,1272],{"class":400,"line":578},[398,1273,1274],{"class":412},"  },\n",[398,1276,1277,1280,1282],{"class":400,"line":618},[398,1278,1279],{"class":493},"  options",[398,1281,505],{"class":412},[398,1283,497],{"class":412},[398,1285,1286,1288,1290,1293,1295,1298,1300,1303,1305,1307,1310,1312,1314],{"class":400,"line":652},[398,1287,1116],{"class":493},[398,1289,505],{"class":412},[398,1291,1292],{"class":412}," ()",[398,1294,1036],{"class":489},[398,1296,1297],{"class":408}," jira",[398,1299,70],{"class":412},[398,1301,1302],{"class":437},"get",[398,1304,442],{"class":441},[398,1306,423],{"class":412},[398,1308,1309],{"class":419},"\u002Flabel",[398,1311,423],{"class":412},[398,1313,612],{"class":441},[398,1315,946],{"class":412},[398,1317,1318,1321,1323,1325,1327,1329,1331,1333,1335,1337,1340,1342,1344],{"class":400,"line":687},[398,1319,1320],{"class":493},"    statuses",[398,1322,505],{"class":412},[398,1324,1292],{"class":412},[398,1326,1036],{"class":489},[398,1328,1297],{"class":408},[398,1330,70],{"class":412},[398,1332,1302],{"class":437},[398,1334,442],{"class":441},[398,1336,423],{"class":412},[398,1338,1339],{"class":419},"\u002Fstatus",[398,1341,423],{"class":412},[398,1343,612],{"class":441},[398,1345,946],{"class":412},[398,1347,1348],{"class":400,"line":1133},[398,1349,1274],{"class":412},[398,1351,1352],{"class":400,"line":1143},[398,1353,453],{"class":412},[10,1355,1356,1357,1360,1361,1364,1365,1367,1368,1370],{},"Aus ",[14,1358,1359],{},"form"," baut Knecht den Dialog. ",[14,1362,1363],{},"options"," füllt die Auswahlfelder mit den Labels und Status, die es in Jira wirklich gibt. ",[14,1366,1359],{}," sind reine Daten und gehen in den Browser, ",[14,1369,1363],{}," läuft auf dem Server, weil nur der die Zugangsdaten hat.",[79,1372,1374],{"id":1373},"die-verbindung","Die Verbindung",[10,1376,1377],{},"Die Verbindung sind die Zugangsdaten, mit denen Knecht im Tool liest und schreibt, bei Jira etwa die Site-URL, eine E-Mail und ein API-Token. Ein Admin trägt sie einmal in den Settings ein, danach gilt die Verbindung für die ganze Instanz. Auch dieses Formular ist eine Beschreibung. Die Integration sagt nur, welche Felder sie braucht und wie sie prüft, ob die Zugangsdaten stimmen:",[390,1379,1381],{"className":392,"code":1380,"language":394,"meta":290,"style":290},"connection: {\n  fields: [\n    { key: 'siteUrl', label: 'Site URL', type: 'url' },\n    { key: 'email', label: 'Email', type: 'email' },\n    { key: 'apiToken', label: 'API token', type: 'secret' },\n  ],\n  verify: values => jira.whoAmI(values),\n}\n",[14,1382,1383,1392,1401,1447,1489,1532,1539,1567],{"__ignoreMap":290},[398,1384,1385,1388,1390],{"class":400,"line":401},[398,1386,1387],{"class":493},"connection",[398,1389,505],{"class":412},[398,1391,497],{"class":412},[398,1393,1394,1397,1399],{"class":400,"line":291},[398,1395,1396],{"class":493},"  fields",[398,1398,505],{"class":412},[398,1400,1200],{"class":441},[398,1402,1403,1406,1409,1411,1413,1416,1418,1420,1422,1424,1426,1429,1431,1433,1436,1438,1440,1443,1445],{"class":400,"line":297},[398,1404,1405],{"class":412},"    {",[398,1407,1408],{"class":441}," key",[398,1410,505],{"class":412},[398,1412,416],{"class":412},[398,1414,1415],{"class":419},"siteUrl",[398,1417,423],{"class":412},[398,1419,558],{"class":412},[398,1421,1208],{"class":441},[398,1423,505],{"class":412},[398,1425,416],{"class":412},[398,1427,1428],{"class":419},"Site URL",[398,1430,423],{"class":412},[398,1432,558],{"class":412},[398,1434,1435],{"class":441}," type",[398,1437,505],{"class":412},[398,1439,416],{"class":412},[398,1441,1442],{"class":419},"url",[398,1444,423],{"class":412},[398,1446,998],{"class":412},[398,1448,1449,1451,1453,1455,1457,1460,1462,1464,1466,1468,1470,1473,1475,1477,1479,1481,1483,1485,1487],{"class":400,"line":527},[398,1450,1405],{"class":412},[398,1452,1408],{"class":441},[398,1454,505],{"class":412},[398,1456,416],{"class":412},[398,1458,1459],{"class":419},"email",[398,1461,423],{"class":412},[398,1463,558],{"class":412},[398,1465,1208],{"class":441},[398,1467,505],{"class":412},[398,1469,416],{"class":412},[398,1471,1472],{"class":419},"Email",[398,1474,423],{"class":412},[398,1476,558],{"class":412},[398,1478,1435],{"class":441},[398,1480,505],{"class":412},[398,1482,416],{"class":412},[398,1484,1459],{"class":419},[398,1486,423],{"class":412},[398,1488,998],{"class":412},[398,1490,1491,1493,1495,1497,1499,1502,1504,1506,1508,1510,1512,1515,1517,1519,1521,1523,1525,1528,1530],{"class":400,"line":542},[398,1492,1405],{"class":412},[398,1494,1408],{"class":441},[398,1496,505],{"class":412},[398,1498,416],{"class":412},[398,1500,1501],{"class":419},"apiToken",[398,1503,423],{"class":412},[398,1505,558],{"class":412},[398,1507,1208],{"class":441},[398,1509,505],{"class":412},[398,1511,416],{"class":412},[398,1513,1514],{"class":419},"API token",[398,1516,423],{"class":412},[398,1518,558],{"class":412},[398,1520,1435],{"class":441},[398,1522,505],{"class":412},[398,1524,416],{"class":412},[398,1526,1527],{"class":419},"secret",[398,1529,423],{"class":412},[398,1531,998],{"class":412},[398,1533,1534,1537],{"class":400,"line":548},[398,1535,1536],{"class":441},"  ]",[398,1538,946],{"class":412},[398,1540,1541,1544,1546,1549,1551,1553,1555,1558,1560,1563,1565],{"class":400,"line":578},[398,1542,1543],{"class":493},"  verify",[398,1545,505],{"class":412},[398,1547,1548],{"class":517}," values",[398,1550,1036],{"class":489},[398,1552,1297],{"class":408},[398,1554,70],{"class":412},[398,1556,1557],{"class":437},"whoAmI",[398,1559,442],{"class":441},[398,1561,1562],{"class":408},"values",[398,1564,612],{"class":441},[398,1566,946],{"class":412},[398,1568,1569],{"class":400,"line":618},[398,1570,453],{"class":412},[10,1572,1573,1574,1576,1577,1579,1580,1585,1586,1588],{},"Eine gemeinsame Vue-Komponente baut daraus das Formular, eine gemeinsame API-Route speichert es. Der Feldtyp bringt die Regeln mit. Eine ",[14,1575,1442],{}," muss mit https beginnen, ein ",[14,1578,1527],{}," wird ",[51,1581,1584],{"href":1582,"rel":1583},"https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FCryptographic_Storage_Cheat_Sheet.html",[55],"verschlüsselt gespeichert"," und nur als Vorschau angezeigt. Vor dem Speichern meldet sich ",[14,1587,694],{}," einmal beim Tool an, damit falsche Zugangsdaten gar nicht erst gespeichert werden.",[19,1590,1592],{"id":1591},"tests-gegen-die-schnittstelle","Tests gegen die Schnittstelle",[79,1594,1596],{"id":1595},"vom-durchklicken-zu-fixtures","Vom Durchklicken zu Fixtures",[10,1598,1599,1600,1605],{},"Am Anfang haben wir Trigger von Hand getestet. Die lokale Knecht-Instanz war über einen ",[51,1601,1604],{"href":1602,"rel":1603},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002F",[55],"Cloudflare Tunnel"," erreichbar, wir haben in Jira ein Ticket angelegt, ein Label gesetzt, den Status verschoben und im Log nachgesehen, ob der richtige Workflow startet. Das dauerte, und nach jeder Änderung an der Trigger-Logik ging es von vorne los.",[10,1607,1608],{},"Deshalb schreibt Knecht jetzt auf Wunsch jeden geprüften Webhook als JSON-Datei in einen Ordner. Eine Runde Durchklicken im Tool reicht, und die aufgezeichneten Webhooks werden zu Fixtures. Stand September 2026 sind es 89. Die Tests spielen sie erneut ab und legen für jeden Trigger fest, bei welchen Webhooks er feuern muss und bei welchen nicht:",[390,1610,1612],{"className":392,"code":1611,"language":394,"meta":290,"style":290},"const cases = [\n  { trigger: 'Label \"bug\" added', firesOn: ['ticket-labeled-bug'] },\n  { trigger: 'Status \"Done\" reached', firesOn: ['ticket-moved-to-done'] },\n]\n\nfor (const { trigger, firesOn } of cases) {\n  for (const webhook of recordedWebhooks) {\n    expect(fires(trigger, webhook)).toBe(firesOn.includes(webhook.name))\n  }\n}\n",[14,1613,1614,1625,1664,1698,1703,1707,1736,1757,1804,1808],{"__ignoreMap":290},[398,1615,1616,1618,1621,1623],{"class":400,"line":401},[398,1617,798],{"class":489},[398,1619,1620],{"class":408}," cases ",[398,1622,804],{"class":412},[398,1624,1200],{"class":408},[398,1626,1627,1630,1633,1635,1637,1640,1642,1644,1647,1649,1651,1653,1656,1658,1661],{"class":400,"line":291},[398,1628,1629],{"class":412},"  {",[398,1631,1632],{"class":441}," trigger",[398,1634,505],{"class":412},[398,1636,416],{"class":412},[398,1638,1639],{"class":419},"Label \"bug\" added",[398,1641,423],{"class":412},[398,1643,558],{"class":412},[398,1645,1646],{"class":441}," firesOn",[398,1648,505],{"class":412},[398,1650,756],{"class":408},[398,1652,423],{"class":412},[398,1654,1655],{"class":419},"ticket-labeled-bug",[398,1657,423],{"class":412},[398,1659,1660],{"class":408},"] ",[398,1662,1663],{"class":412},"},\n",[398,1665,1666,1668,1670,1672,1674,1677,1679,1681,1683,1685,1687,1689,1692,1694,1696],{"class":400,"line":297},[398,1667,1629],{"class":412},[398,1669,1632],{"class":441},[398,1671,505],{"class":412},[398,1673,416],{"class":412},[398,1675,1676],{"class":419},"Status \"Done\" reached",[398,1678,423],{"class":412},[398,1680,558],{"class":412},[398,1682,1646],{"class":441},[398,1684,505],{"class":412},[398,1686,756],{"class":408},[398,1688,423],{"class":412},[398,1690,1691],{"class":419},"ticket-moved-to-done",[398,1693,423],{"class":412},[398,1695,1660],{"class":408},[398,1697,1663],{"class":412},[398,1699,1700],{"class":400,"line":527},[398,1701,1702],{"class":408},"]\n",[398,1704,1705],{"class":400,"line":542},[398,1706,826],{"emptyLinePlaceholder":312},[398,1708,1709,1712,1714,1716,1718,1720,1722,1725,1728,1731,1734],{"class":400,"line":548},[398,1710,1711],{"class":404},"for",[398,1713,1039],{"class":408},[398,1715,798],{"class":489},[398,1717,587],{"class":412},[398,1719,1632],{"class":408},[398,1721,558],{"class":412},[398,1723,1724],{"class":408}," firesOn ",[398,1726,1727],{"class":412},"}",[398,1729,1730],{"class":412}," of",[398,1732,1733],{"class":408}," cases) ",[398,1735,429],{"class":412},[398,1737,1738,1741,1743,1745,1748,1750,1753,1755],{"class":400,"line":578},[398,1739,1740],{"class":404},"  for",[398,1742,1039],{"class":441},[398,1744,798],{"class":489},[398,1746,1747],{"class":408}," webhook",[398,1749,1730],{"class":412},[398,1751,1752],{"class":408}," recordedWebhooks",[398,1754,426],{"class":441},[398,1756,429],{"class":412},[398,1758,1759,1762,1764,1767,1769,1771,1773,1775,1778,1780,1783,1785,1788,1790,1793,1795,1797,1799,1801],{"class":400,"line":618},[398,1760,1761],{"class":437},"    expect",[398,1763,442],{"class":441},[398,1765,1766],{"class":437},"fires",[398,1768,442],{"class":441},[398,1770,1177],{"class":408},[398,1772,558],{"class":412},[398,1774,1747],{"class":408},[398,1776,1777],{"class":441},"))",[398,1779,70],{"class":412},[398,1781,1782],{"class":437},"toBe",[398,1784,442],{"class":441},[398,1786,1787],{"class":408},"firesOn",[398,1789,70],{"class":412},[398,1791,1792],{"class":437},"includes",[398,1794,442],{"class":441},[398,1796,814],{"class":408},[398,1798,70],{"class":412},[398,1800,1109],{"class":408},[398,1802,1803],{"class":441},"))\n",[398,1805,1806],{"class":400,"line":652},[398,1807,545],{"class":412},[398,1809,1810],{"class":400,"line":687},[398,1811,453],{"class":412},[10,1813,1814],{},"Steht die Verbindung zu einem Tool einmal und ist eine Runde Webhooks aufgezeichnet, wissen wir nach jeder Änderung in Sekunden, ob die Trigger noch stimmen, ohne das Tool zu öffnen. Ändert ein Tool sein Format, zeichnen wir neu auf und sehen im Test sofort, was sich geändert hat.",[79,1816,1818],{"id":1817},"eine-suite-für-alle","Eine Suite für alle",[10,1820,1821,1822,1827],{},"Die Fixtures prüfen, ob Trigger richtig entscheiden. Ob eine Integration die Schnittstelle überhaupt richtig erfüllt, prüft eine zweite Ebene. Weil alle Integrationen dieselbe Schnittstelle haben, laufen sie durch dieselbe Test-Suite in ",[51,1823,1826],{"href":1824,"rel":1825},"https:\u002F\u002Fvitest.dev\u002Fguide\u002F",[55],"Vitest",". Eine Integration liefert dafür nur Bausteine, die Webhooks in ihrem Format erzeugen:",[390,1829,1831],{"className":392,"code":1830,"language":394,"meta":290,"style":290},"webhookSuite({\n  integration: jira,\n  created: project => jiraWebhook('issue_created', project),\n  labeled: project => jiraWebhook('issue_updated', project, { labels: ['bug'] }),\n  mention: project => jiraComment(project, '@Knecht bitte anschauen'),\n})\n",[14,1832,1833,1842,1853,1884,1935,1965],{"__ignoreMap":290},[398,1834,1835,1838,1840],{"class":400,"line":401},[398,1836,1837],{"class":437},"webhookSuite",[398,1839,442],{"class":408},[398,1841,429],{"class":412},[398,1843,1844,1847,1849,1851],{"class":400,"line":291},[398,1845,1846],{"class":441},"  integration",[398,1848,505],{"class":412},[398,1850,1297],{"class":408},[398,1852,946],{"class":412},[398,1854,1855,1858,1860,1863,1865,1868,1870,1872,1875,1877,1879,1882],{"class":400,"line":297},[398,1856,1857],{"class":437},"  created",[398,1859,505],{"class":412},[398,1861,1862],{"class":517}," project",[398,1864,1036],{"class":489},[398,1866,1867],{"class":437}," jiraWebhook",[398,1869,442],{"class":408},[398,1871,423],{"class":412},[398,1873,1874],{"class":419},"issue_created",[398,1876,423],{"class":412},[398,1878,558],{"class":412},[398,1880,1881],{"class":408}," project)",[398,1883,946],{"class":412},[398,1885,1886,1889,1891,1893,1895,1897,1899,1901,1904,1906,1908,1910,1912,1914,1917,1919,1921,1923,1925,1927,1929,1931,1933],{"class":400,"line":527},[398,1887,1888],{"class":437},"  labeled",[398,1890,505],{"class":412},[398,1892,1862],{"class":517},[398,1894,1036],{"class":489},[398,1896,1867],{"class":437},[398,1898,442],{"class":408},[398,1900,423],{"class":412},[398,1902,1903],{"class":419},"issue_updated",[398,1905,423],{"class":412},[398,1907,558],{"class":412},[398,1909,1862],{"class":408},[398,1911,558],{"class":412},[398,1913,587],{"class":412},[398,1915,1916],{"class":441}," labels",[398,1918,505],{"class":412},[398,1920,756],{"class":408},[398,1922,423],{"class":412},[398,1924,761],{"class":419},[398,1926,423],{"class":412},[398,1928,1660],{"class":408},[398,1930,1727],{"class":412},[398,1932,612],{"class":408},[398,1934,946],{"class":412},[398,1936,1937,1940,1942,1944,1946,1949,1952,1954,1956,1959,1961,1963],{"class":400,"line":542},[398,1938,1939],{"class":437},"  mention",[398,1941,505],{"class":412},[398,1943,1862],{"class":517},[398,1945,1036],{"class":489},[398,1947,1948],{"class":437}," jiraComment",[398,1950,1951],{"class":408},"(project",[398,1953,558],{"class":412},[398,1955,416],{"class":412},[398,1957,1958],{"class":419},"@Knecht bitte anschauen",[398,1960,423],{"class":412},[398,1962,612],{"class":408},[398,1964,946],{"class":412},[398,1966,1967,1969],{"class":400,"line":548},[398,1968,1727],{"class":412},[398,1970,448],{"class":408},[10,1972,1973],{},"Die Suite schickt diese Webhooks durch die echte Route und erwartet von jeder Integration dasselbe Verhalten.",[10,1975,1976,1977,1982],{},"Martin Fowler nennt so etwas einen ",[51,1978,1981],{"href":1979,"rel":1980},"https:\u002F\u002Fmartinfowler.com\u002Fbliki\u002FContractTest.html",[55],"Contract Test",". Für eine neue Integration heißt das, dass wir nicht überlegen müssen, was zu testen ist. Sobald sie die Bausteine liefert und die Suite grün ist, verhält sie sich wie die anderen.",[19,1984,1986],{"id":1985},"was-wir-mitnehmen","Was wir mitnehmen",[10,1988,1989],{},"Eine neue Integration besteht jetzt aus der Beschreibung des Trackers, den API-Aufrufen und dem Verbindungsformular. Trigger, Mentions, Kommentare, Labels, Status und das Settings-Panel kommen mit der Schnittstelle. Wer so etwas selbst baut, kann sich an vier Punkten orientieren:",[139,1991,1992,1995,2003,2006],{},[142,1993,1994],{},"Der Rest der Anwendung fragt nach Fähigkeiten, nie nach dem Namen des Tools.",[142,1996,1997,1998,70],{},"Eingehende Daten werden an der Grenze in eine gemeinsame Form übersetzt. Danach gibt es keinen Tool-spezifischen Code mehr. Microsoft beschreibt das Muster als ",[51,1999,2002],{"href":2000,"rel":2001},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fazure\u002Farchitecture\u002Fpatterns\u002Fanti-corruption-layer",[55],"Anti-Corruption Layer",[142,2004,2005],{},"Was sich nur in Daten unterscheidet, etwa Formulare oder Begriffe, wird als Daten beschrieben und nicht als Code.",[142,2007,2008],{},"Getestet wird gegen die Schnittstelle, damit jede Integration dieselben Tests bestehen muss.",[2010,2011,2012],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":290,"searchDepth":291,"depth":291,"links":2014},[2015,2019,2023,2028,2032],{"id":21,"depth":291,"text":22,"children":2016},[2017,2018],{"id":371,"depth":297,"text":372},{"id":384,"depth":297,"text":385},{"id":459,"depth":291,"text":460,"children":2020},[2021,2022],{"id":463,"depth":297,"text":464},{"id":773,"depth":297,"text":774},{"id":903,"depth":291,"text":904,"children":2024},[2025,2026,2027],{"id":910,"depth":297,"text":911},{"id":1160,"depth":297,"text":1161},{"id":1373,"depth":297,"text":1374},{"id":1591,"depth":291,"text":1592,"children":2029},[2030,2031],{"id":1595,"depth":297,"text":1596},{"id":1817,"depth":297,"text":1818},{"id":1985,"depth":291,"text":1986},"2026-09-24","Wie wir die Integrationen von Knecht hinter eine gemeinsame Schnittstelle gebracht haben, damit ein neues Tool nur noch mitbringt, was an ihm wirklich anders ist.",{},"\u002Fde\u002Fupdates\u002Fintegrations-rewrite",{"title":321,"description":2034},"de\u002Fupdates\u002Fintegrations-rewrite","Architektur","jBdEI5nc1U8FKKBq42DHLe49BlM3jou7Gh4Jq28_W2I",{"id":2042,"title":2043,"body":2044,"date":2930,"description":2931,"extension":310,"meta":2932,"navigation":312,"path":2933,"seo":2934,"stem":2935,"tag":2936,"__hash__":2937},"updates_de\u002Fde\u002Fupdates\u002Frepos-without-ddev-config.md","Jedes Repo bootet, auch ohne DDEV-Config",{"type":7,"value":2045,"toc":2909},[2046,2076,2080,2083,2162,2168,2172,2181,2299,2314,2317,2336,2339,2351,2365,2368,2372,2414,2418,2440,2444,2451,2454,2458,2469,2474,2477,2480,2649,2653,2670,2676,2680,2683,2687,2717,2740,2744,2761,2765,2776,2784,2791,2795,2835,2839,2871,2875,2881,2892,2895,2906],[10,2047,2048,2049,2052,2053,2058,2059,2064,2065,344,2070,2075],{},"Bisher brauchte ein Repo eine ",[14,2050,2051],{},".ddev\u002Fconfig.yaml",", sonst konnte Knecht es nicht booten. Seit Version 0.10 baut Knecht die Umgebung aus den Dateien im Repo, wenn keine Config da ist. Ein Dev-Server wird dabei zur Live-Preview, ",[51,2054,2057],{"href":2055,"rel":2056},"https:\u002F\u002Fvite.dev\u002Fguide\u002Ffeatures.html#hot-module-replacement",[55],"Hot Reload"," eingeschlossen. Dieser Post erklärt, was Knecht erkennt, wie die Preview durchkommt und was wir dabei über ",[51,2060,2063],{"href":2061,"rel":2062},"https:\u002F\u002Fddev.com\u002F",[55],"DDEV",", ",[51,2066,2069],{"href":2067,"rel":2068},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002F",[55],"Docker Compose",[51,2071,2074],{"href":2072,"rel":2073},"https:\u002F\u002Fnuxt.com\u002F",[55],"Nuxt"," gelernt haben.",[19,2077,2079],{"id":2078},"was-knecht-erkennt","Was Knecht erkennt",[10,2081,2082],{},"Beim Start jedes Runs liest Knecht die Dateien aus dem Checkout, die die Versionen ohnehin festlegen:",[2084,2085,2086,2101],"table",{},[2087,2088,2089],"thead",{},[2090,2091,2092,2095,2098],"tr",{},[2093,2094],"th",{},[2093,2096,2097],{},"Quelle",[2093,2099,2100],{},"Regel",[2102,2103,2104,2122,2147],"tbody",{},[2090,2105,2106,2110,2119],{},[2107,2108,2109],"td",{},"PHP",[2107,2111,2112,2115,2116],{},[14,2113,2114],{},"require.php"," in ",[14,2117,2118],{},"composer.json",[2107,2120,2121],{},"die höchste Version des DDEV-Images, die den Constraint erfüllt",[2090,2123,2124,2127,2144],{},[2107,2125,2126],{},"Node",[2107,2128,2129,2064,2132,2064,2135,2064,2138,2064,2141],{},[14,2130,2131],{},"mise.toml",[14,2133,2134],{},".mise.toml",[14,2136,2137],{},".tool-versions",[14,2139,2140],{},".nvmrc",[14,2142,2143],{},"engines.node",[2107,2145,2146],{},"die erste Datei mit einem Wert gewinnt",[2090,2148,2149,2152,2159],{},[2107,2150,2151],{},"Package Manager",[2107,2153,2154,2155,2158],{},"Feld ",[14,2156,2157],{},"packageManager",", sonst das Lockfile",[2107,2160,2161],{},"npm, pnpm, yarn oder bun",[10,2163,2164,2165,70],{},"Fehlt eine Angabe, gilt der DDEV-Default. Das Run-Log nennt jeden Wert mit seiner Quelle, etwa ",[14,2166,2167],{},"PHP 8.2 from composer.json, Node 22 from .nvmrc",[79,2169,2171],{"id":2170},"die-generierte-config","Die generierte Config",[10,2173,2174,2175,2177,2178,2180],{},"Aus den Werten schreibt Knecht eine ",[14,2176,2051],{}," in den Checkout. Für ein Nuxt-Repo, das Node 22 pinnt und keine ",[14,2179,2118],{}," hat, sieht sie so aus:",[390,2182,2186],{"className":2183,"code":2184,"language":2185,"meta":290,"style":290},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","#knecht-generated\nname: knecht-run-42\ntype: php\ndocroot: \"\"\nwebserver_type: generic\nphp_version: \"8.4\"\nnodejs_version: \"22\"\nomit_containers:\n  - db\ncorepack_enable: true\ndisable_settings_management: true\n","yaml",[14,2187,2188,2194,2203,2213,2223,2233,2249,2263,2271,2279,2290],{"__ignoreMap":290},[398,2189,2190],{"class":400,"line":401},[398,2191,2193],{"class":2192},"sHwdD","#knecht-generated\n",[398,2195,2196,2198,2200],{"class":400,"line":291},[398,2197,1109],{"class":441},[398,2199,505],{"class":412},[398,2201,2202],{"class":419}," knecht-run-42\n",[398,2204,2205,2208,2210],{"class":400,"line":297},[398,2206,2207],{"class":441},"type",[398,2209,505],{"class":412},[398,2211,2212],{"class":419}," php\n",[398,2214,2215,2218,2220],{"class":400,"line":527},[398,2216,2217],{"class":441},"docroot",[398,2219,505],{"class":412},[398,2221,2222],{"class":412}," \"\"\n",[398,2224,2225,2228,2230],{"class":400,"line":542},[398,2226,2227],{"class":441},"webserver_type",[398,2229,505],{"class":412},[398,2231,2232],{"class":419}," generic\n",[398,2234,2235,2238,2240,2243,2246],{"class":400,"line":548},[398,2236,2237],{"class":441},"php_version",[398,2239,505],{"class":412},[398,2241,2242],{"class":412}," \"",[398,2244,2245],{"class":419},"8.4",[398,2247,2248],{"class":412},"\"\n",[398,2250,2251,2254,2256,2258,2261],{"class":400,"line":578},[398,2252,2253],{"class":441},"nodejs_version",[398,2255,505],{"class":412},[398,2257,2242],{"class":412},[398,2259,2260],{"class":419},"22",[398,2262,2248],{"class":412},[398,2264,2265,2268],{"class":400,"line":618},[398,2266,2267],{"class":441},"omit_containers",[398,2269,2270],{"class":412},":\n",[398,2272,2273,2276],{"class":400,"line":652},[398,2274,2275],{"class":412},"  -",[398,2277,2278],{"class":419}," db\n",[398,2280,2281,2284,2286],{"class":400,"line":687},[398,2282,2283],{"class":441},"corepack_enable",[398,2285,505],{"class":412},[398,2287,2289],{"class":2288},"sfNiH"," true\n",[398,2291,2292,2295,2297],{"class":400,"line":1133},[398,2293,2294],{"class":441},"disable_settings_management",[398,2296,505],{"class":412},[398,2298,2289],{"class":2288},[10,2300,2301,2302,2305,2306,2313],{},"Der Projekttyp ist ",[14,2303,2304],{},"php",", weil DDEV dafür keine CMS-Annahmen trifft. Der Web-Server ist ",[51,2307,2310],{"href":2308,"rel":2309},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fconfiguration\u002Fconfig\u002F#webserver_type",[55],[14,2311,2312],{},"generic",", also laufen weder nginx noch php-fpm, denn ohne Website gibt es nichts auszuliefern. Der Datenbank-Container fehlt, Corepack ist eingeschaltet, und DDEV schreibt keine Settings-Dateien ins Repo. Ports, Umgebungsvariablen und Daemons legt Knecht wie bei jedem Projekt in eigenen Override-Dateien daneben ab, dazu weiter unten mehr.",[10,2315,2316],{},"Den Kommentar in der ersten Zeile braucht Knecht, damit es bei jedem weiteren Run sicher sagen kann, dass es sich um ein Projekt ohne eigene DDEV-Config handelt, obwohl im Checkout jetzt eine liegt.",[10,2318,2319,2320,2323,2324,2331,2332,2335],{},"Der Marker sorgt auch dafür, dass nichts davon in einem Commit landet. Die Git-Actions im Run committen alles, was im Checkout liegt. Deshalb schreibt Knecht vor dem ersten Run das Verzeichnis ",[14,2321,2322],{},".ddev\u002F"," in die Datei ",[51,2325,2328],{"href":2326,"rel":2327},"https:\u002F\u002Fgit-scm.com\u002Fdocs\u002Fgitignore",[55],[14,2329,2330],{},".git\u002Finfo\u002Fexclude"," des Clones. Das ist eine Ignore-Liste, die nur in diesem Clone gilt und selbst nicht im Repo liegt, Git behandelt die Einträge wie eine ",[14,2333,2334],{},".gitignore",". Bei einem Repo mit eigener Config stehen dort nur Knechts eigene Dateien, bei einer generierten das ganze Verzeichnis, weil auch DDEV beim Start Dateien hineinlegt. Welcher Fall gilt, liest Knecht am Marker ab.",[79,2337,2151],{"id":2338},"package-manager",[10,2340,2341,2342,2347,2348,2350],{},"npm steckt im DDEV-Image und ist der Default, wenn das Repo nichts anderes festlegt. ",[51,2343,2346],{"href":2344,"rel":2345},"https:\u002F\u002Fnodejs.org\u002Fapi\u002Fcorepack.html",[55],"Corepack"," ist in jeder generierten Umgebung eingeschaltet, damit pnpm und yarn ohne Installation laufen. Corepack lädt die Version aus dem Feld ",[14,2349,2157],{},", im Repo muss nichts installiert werden.",[10,2352,2353,2358,2359,2364],{},[51,2354,2357],{"href":2355,"rel":2356},"https:\u002F\u002Fbun.sh\u002F",[55],"bun"," fehlt im Image. Knecht ergänzt den Image-Build um eine Zeile, die bun über npm installiert. Die Schicht bleibt im ",[51,2360,2363],{"href":2361,"rel":2362},"https:\u002F\u002Fdocs.docker.com\u002Fbuild\u002Fcache\u002F",[55],"Docker-Cache",", nur die erste bun-Session auf einem Server baut sie.",[10,2366,2367],{},"Die Downloads aller vier landen im serverweiten Cache von DDEV. Composer, npm und Corepack zeigt DDEV selbst dorthin, die Pfade für den pnpm-Store, yarn und bun setzt Knecht dazu, und zwar für jedes Projekt, auch für eines mit eigener DDEV-Config. Ein Server lädt jedes Paket einmal, spätere Sessions installieren aus dem Cache.",[79,2369,2371],{"id":2370},"node-version","Node-Version",[10,2373,2374,2375,2377,2378,2381,2382,2387,2388,2391,2392,2377,2395,2398,2399,2405,2406,2409,2410,2413],{},"Konkrete Versionen wie ",[14,2376,2260],{}," oder ",[14,2379,2380],{},"v20.11"," übernimmt Knecht bis zur Minor-Version, ",[51,2383,2386],{"href":2384,"rel":2385},"https:\u002F\u002Fgithub.com\u002Fnvm-sh\u002Fnvm#nvmrc",[55],"nvm","-Codenamen wie ",[14,2389,2390],{},"lts\u002Firon"," als Major-Version. Bewegliche Ziele wie ",[14,2393,2394],{},"lts\u002F*",[14,2396,2397],{},"latest"," pinnt Knecht nicht, dann gilt der Default, mit einer Warnung im Run-Log. ",[51,2400,2403],{"href":2401,"rel":2402},"https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fv10\u002Fconfiguring-npm\u002Fpackage-json#engines",[55],[14,2404,2143],{}," in der ",[14,2407,2408],{},"package.json"," ist meist ein Bereich wie ",[14,2411,2412],{},">=20",". Erfüllt der DDEV-Default den Bereich, bleibt er, sonst nimmt Knecht die höchste LTS-Version, die passt.",[79,2415,2417],{"id":2416},"php-version","PHP-Version",[10,2419,2420,2421,2426,2427,2377,2430,2433,2434,2439],{},"Zur Wahl stehen die Versionen des DDEV-Images, derzeit 5.6 bis 8.4. Composer-",[51,2422,2425],{"href":2423,"rel":2424},"https:\u002F\u002Fgetcomposer.org\u002Fdoc\u002Farticles\u002Fversions.md",[55],"Constraints"," wie ",[14,2428,2429],{},"^8.1",[14,2431,2432],{},">=8.1,\u003C8.3"," übersetzt Knecht in einen ",[51,2435,2438],{"href":2436,"rel":2437},"https:\u002F\u002Fgithub.com\u002Fnpm\u002Fnode-semver#ranges",[55],"Semver-Range"," und nimmt die höchste Version, die ihn erfüllt. Erfüllt keine den Constraint, gilt der Default, mit Warnung im Run-Log.",[79,2441,2443],{"id":2442},"das-ergebnis-in-den-settings","Das Ergebnis in den Settings",[10,2445,2446],{},[2447,2448],"img",{"alt":2449,"src":2450},"Projekt-Settings eines Nuxt-Repos ohne DDEV-Config: die Karte \"Environment\" mit PHP 8.4, Node 24 und pnpm, darunter die Boot Commands und die Karte \"Dev Server\" mit Befehl und Port","\u002Fassets\u002Fknecht-project-settings-noddev.png",[10,2452,2453],{},"Die Karte \"Environment\" in den Projekt-Settings zeigt die erkannten Werte mit ihrer Quelle. Wer will, wählt dort eine andere PHP-Version, eine andere Node-Version oder einen anderen Package Manager. Die Erkennung läuft schon beim Verbinden des Repos, über die GitHub-API und mit derselben Logik wie beim Boot. Warnungen, etwa zu einem Constraint, den keine Version erfüllt, stehen ebenfalls in der Karte.",[19,2455,2457],{"id":2456},"preview","Preview",[10,2459,2460,2461,2464,2465,2468],{},"Eine generierte Umgebung hat keine Website. Deshalb gibt es die Karte \"Dev Server\" mit dem Befehl, der den Dev-Server startet, etwa ",[14,2462,2463],{},"npm run dev",", und seinem Port. Knecht startet ihn nach den Boot-Befehlen und liefert ihn als Preview der Session aus. Die Preview-URL bekommt der Befehl als ",[14,2466,2467],{},"KNECHT_PREVIEW_URL",". Ohne Dev-Server bootet die Umgebung ohne Preview. Änderungen an den Karten \"Environment\" und \"Dev Server\" greifen beim nächsten Run der Session.",[2470,2471],"update-video",{"caption":2472,"src":2473},"Ein Nuxt-Dev-Server als Preview. Die Änderung im Code erscheint ohne Reload.","\u002Fassets\u002Fhmr-nuxt-knecht.mp4",[79,2475,2057],{"id":2476},"hot-reload",[10,2478,2479],{},"Hot Reload funktioniert in der Preview wie lokal, ohne Änderungen im Repo. Wie ein Vite-Dev-Server durchkommt, war aber ein Stück Arbeit.",[75,2481,2483,2488,2503,2574,2585,2589,2599,2603,2624,2628],{"level":2482},"4",[2484,2485,2487],"h4",{"id":2486},"der-daemon-im-container","Der Daemon im Container",[10,2489,2490,2491,2496,2497,2502],{},"Der Dev-Server läuft als Daemon unter ",[51,2492,2495],{"href":2493,"rel":2494},"https:\u002F\u002Fsupervisord.org\u002F",[55],"supervisord"," im Web-Container, so wie DDEV es für ",[51,2498,2501],{"href":2499,"rel":2500},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fconfiguration\u002Fconfig\u002F#web_extra_daemons",[55],"web_extra_daemons"," vorsieht. Knecht trägt ihn in seiner Override-Datei ein, zusammen mit dem Forwarder aus dem nächsten Schritt:",[390,2504,2506],{"className":2183,"code":2505,"language":2185,"meta":290,"style":290},"web_extra_daemons:\n  - name: knecht-dev\n    command: bash -lc 'npm run dev'\n    directory: \u002Fvar\u002Fwww\u002Fhtml\n  - name: knecht-forward\n    command: knecht-forward 41000 3000\n    directory: \u002Fvar\u002Fwww\u002Fhtml\n",[14,2507,2508,2514,2526,2536,2546,2557,2566],{"__ignoreMap":290},[398,2509,2510,2512],{"class":400,"line":401},[398,2511,2501],{"class":441},[398,2513,2270],{"class":412},[398,2515,2516,2518,2521,2523],{"class":400,"line":291},[398,2517,2275],{"class":412},[398,2519,2520],{"class":441}," name",[398,2522,505],{"class":412},[398,2524,2525],{"class":419}," knecht-dev\n",[398,2527,2528,2531,2533],{"class":400,"line":297},[398,2529,2530],{"class":441},"    command",[398,2532,505],{"class":412},[398,2534,2535],{"class":419}," bash -lc 'npm run dev'\n",[398,2537,2538,2541,2543],{"class":400,"line":527},[398,2539,2540],{"class":441},"    directory",[398,2542,505],{"class":412},[398,2544,2545],{"class":419}," \u002Fvar\u002Fwww\u002Fhtml\n",[398,2547,2548,2550,2552,2554],{"class":400,"line":542},[398,2549,2275],{"class":412},[398,2551,2520],{"class":441},[398,2553,505],{"class":412},[398,2555,2556],{"class":419}," knecht-forward\n",[398,2558,2559,2561,2563],{"class":400,"line":548},[398,2560,2530],{"class":441},[398,2562,505],{"class":412},[398,2564,2565],{"class":419}," knecht-forward 41000 3000\n",[398,2567,2568,2570,2572],{"class":400,"line":578},[398,2569,2540],{"class":441},[398,2571,505],{"class":412},[398,2573,2545],{"class":419},[10,2575,2576,2577,2580,2581,2584],{},"Der Befehl läuft in einer Login-Shell, damit das Profil des Images gilt. DDEV startet den Daemon direkt nach ",[14,2578,2579],{},"ddev start",", also bevor die Boot-Befehle ",[14,2582,2583],{},"npm ci"," ausgeführt haben. Der erste Start stirbt deshalb, und supervisord gibt nach ein paar Versuchen auf. Knecht startet die Daemon-Gruppe nach den Boot-Befehlen noch einmal und pollt dann, bis auf dem Port etwas antwortet. Erst dann gilt die Preview als bereit.",[2484,2586,2588],{"id":2587},"der-forwarder","Der Forwarder",[10,2590,2591,2592,2595,2596,2598],{},"Vite, Nuxt und Next binden standardmäßig nur an ",[14,2593,2594],{},"127.0.0.1",". Der Preview-Proxy erreicht den Container aber von außen, über dessen IP im Docker-Netz. Deshalb läuft als zweiter Daemon ein kleiner Forwarder daneben, ein Node-Script mit wenigen Zeilen. Es lauscht auf allen Interfaces des Containers an einem festen Port und reicht jede TCP-Verbindung an ",[14,2597,2594],{}," und den Port des Dev-Servers weiter. Alles vom Host aus zielt auf den Forwarder, auch das Polling beim Boot, damit derselbe Weg geprüft wird, den später der Browser nimmt.",[2484,2600,2602],{"id":2601},"der-proxy","Der Proxy",[10,2604,2605,2606,2609,2610,2613,2614,2619,2620,2623],{},"Ein Request auf ",[14,2607,2608],{},"\u003Csession>.preview.\u003Chost>"," landet beim Preview-Proxy von Knecht. Der prüft das Login-Cookie, ermittelt die IP des Web-Containers und reicht den Request per HTTP an den Forwarder durch. Der ",[14,2611,2612],{},"Host","-Header bleibt der Preview-Host, weil der Dev-Server keinen eigenen Hostnamen hat. Hot Reload läuft über eine ",[51,2615,2618],{"href":2616,"rel":2617},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWebSockets_API",[55],"WebSocket","-Verbindung. Der Proxy leitet den Upgrade an denselben Port weiter und gibt das vom Browser gewünschte Subprotokoll mit, weil Vite den HMR-Upgrade nur mit ",[14,2621,2622],{},"vite-hmr"," abschließt. Weil der Forwarder rohes TCP weiterreicht, kommt der WebSocket ohne weiteres Zutun durch.",[2484,2625,2627],{"id":2626},"die-host-prüfung","Die Host-Prüfung",[10,2629,2630,2631,2636,2637,2640,2641,2648],{},"Vite lehnt Anfragen für unbekannte Hostnamen ab, siehe ",[51,2632,2635],{"href":2633,"rel":2634},"https:\u002F\u002Fvite.dev\u002Fconfig\u002Fserver-options.html#server-allowedhosts",[55],"server.allowedHosts",". Seit Vite 5.4.12 und 6.0.9 liest der Dev-Server einen zusätzlichen erlaubten Host aus der Umgebungsvariable ",[14,2638,2639],{},"__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS",", und Knecht setzt sie auf den Preview-Host. Nuxt nutzt Vite und ist damit abgedeckt. Next.js führt seine eigene Liste in ",[51,2642,2645],{"href":2643,"rel":2644},"https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fapp\u002Fapi-reference\u002Fconfig\u002Fnext-config-js\u002FallowedDevOrigins",[55],[14,2646,2647],{},"allowedDevOrigins",", die kommt weiterhin aus dem Repo.",[79,2650,2652],{"id":2651},"dev-server-neben-der-eigenen-config","Dev-Server neben der eigenen Config",[10,2654,2655,2656,2661,2662,2665,2666,2669],{},"Seit demselben Release kann auch ein Repo mit eigener DDEV-Config einen Dev-Server angeben, etwa Vite für HMR neben einer ",[51,2657,2660],{"href":2658,"rel":2659},"https:\u002F\u002Fcraftcms.com\u002F",[55],"Craft","-Site. Der Dev-Server ersetzt die Site dann nicht. Die Hostnamen des Repos erreichen weiter den Web-Server, und der Dev-Server bekommt eine eigene Preview-Origin, die der Container als ",[14,2663,2664],{},"KNECHT_DEV_SERVER_URL"," sieht. Dorthin zeigt das Repo seine Asset-URL, etwa mit einem Env-Wert wie ",[14,2667,2668],{},"VITE_DEV_SERVER_PUBLIC=$KNECHT_DEV_SERVER_URL"," in den Projekt-Settings.",[10,2671,2672,2673,2675],{},"Die Seite lädt den Dev-Server als Module-Script, und Module-Scripts holt der Browser über Origins hinweg ohne Cookies. Das Login-Cookie der Preview kommt dort also nie an. Deshalb trägt die Origin des Dev-Servers einen Token pro Session im Hostnamen, statt am Cookie zu hängen. Sichtbar ist davon nichts, ",[14,2674,2664],{}," enthält die fertige URL.",[19,2677,2679],{"id":2678},"nebenbei-gelernt","Nebenbei gelernt",[10,2681,2682],{},"Ein Teil der Arbeit an diesem Release war weniger die Erkennung als das Zusammenspiel mit DDEV, Docker Compose und Nuxt. Fünf Dinge davon sind auch außerhalb von Knecht nützlich.",[79,2684,2686],{"id":2685},"ddev-merged-nach-dateinamen","DDEV merged nach Dateinamen",[10,2688,2689,2690,2115,2693,2695,2696,2701,2702,2705,2706,2709,2710,344,2713,2716],{},"DDEV liest alle ",[14,2691,2692],{},"config.*.yaml",[14,2694,2322],{}," und merged sie in alphabetischer Reihenfolge, bei Skalaren gewinnt die letzte Datei, Listen werden zusammengehängt (",[51,2697,2700],{"href":2698,"rel":2699},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fextend\u002Fcustomization-extendibility\u002F",[55],"Doku","). Knechts Override hieß ",[14,2703,2704],{},"config.knecht.yaml",". Ein Repo mit einer ",[14,2707,2708],{},"config.vite.yaml"," sortierte dahinter und konnte den Run-Namen oder die Umgebung überschreiben. Jetzt heißen Knechts Dateien ",[14,2711,2712],{},"config.zzz-knecht.yaml",[14,2714,2715],{},"docker-compose.zzz-knecht.yaml",", damit sie zuletzt gelesen werden.",[10,2718,2719,2720,2723,2724,2727,2728,2735,2736,2739],{},"Beim Merge ignoriert DDEV außerdem Nullwerte. Ein ",[14,2721,2722],{},"xdebug_enabled: false"," in einer Override-Datei hebt ein ",[14,2725,2726],{},"true"," aus der Haupt-Config nicht auf. Auf einem Knecht-Server läuft kein Debugger, jeder Request hätte auf einen gewartet. Knecht schaltet Xdebug deshalb über ",[51,2729,2732],{"href":2730,"rel":2731},"https:\u002F\u002Fxdebug.org\u002Fdocs\u002Fall_settings",[55],[14,2733,2734],{},"XDEBUG_MODE=off"," in der Umgebung ab, die Variable schlägt die ini, für php-fpm und die CLI. Eine eigene ",[14,2737,2738],{},"XDEBUG_MODE","-Zeile in den Projekt-Settings gewinnt trotzdem.",[79,2741,2743],{"id":2742},"variablen-ohne-router","Variablen ohne Router",[10,2745,2746,2747,2064,2754,344,2757,2760],{},"Ohne Router lässt DDEV ",[51,2748,2751],{"href":2749,"rel":2750},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fextend\u002Fcustom-commands\u002F#environment-variables-provided",[55],[14,2752,2753],{},"DDEV_PRIMARY_URL",[14,2755,2756],{},"DDEV_HOSTNAME",[14,2758,2759],{},"DDEV_SCHEME"," leer. Auf Knecht-Servern gibt es keinen Router, der Preview-Proxy spricht die Web-Container direkt an. Die Settings-Dateien, die DDEV selbst für Drupal, TYPO3 und WordPress schreibt, bauen ihre URLs aber aus genau diesen Variablen, und so zeigten sie ins Leere. Knecht schreibt sie jetzt selbst, so wie DDEV es lokal tut, mit der Preview-URL der Session als Wert.",[79,2762,2764],{"id":2763},"ports-aus-der-projektliste","Ports aus der Projektliste",[10,2766,2767,2768,2775],{},"Zwei parallele Runs desselben Projekts brauchen verschiedene Host-Ports, auch wenn das Repo für den GUI-Client eines Entwicklers einen ",[51,2769,2772],{"href":2770,"rel":2771},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fconfiguration\u002Fconfig\u002F#host_db_port",[55],[14,2773,2774],{},"host_db_port"," pinnt. Knecht hat freie Ports beim Betriebssystem erfragt, und DDEV lehnte den Start trotzdem ab:",[390,2777,2782],{"className":2778,"code":2780,"language":2781},[2779],"language-text","host port 36865 has already been allocated to project knecht-run-6\n","text",[14,2783,2780],{"__ignoreMap":290},[10,2785,2786,2787,2790],{},"DDEV merkt sich die Ports jedes Projekts in ",[14,2788,2789],{},"~\u002F.ddev\u002Fproject_list.yaml",", auch die von gestoppten, und prüft gegen diese Liste. Knecht liest die Liste jetzt mit und probiert weiter, bis es Ports hat, die DDEV nicht kennt.",[79,2792,2794],{"id":2793},"docker-compose-expandiert-gegen-den-host","Docker Compose expandiert gegen den Host",[10,2796,2797,2798,2803,2804,2807,2808,2811,2812,2814,2815,2817,2818,2821,2822,2825,2826,2829,2830,2832,2833,70],{},"Die Env-Werte aus den Projekt-Settings landen über eine Compose-Datei im Container. Docker Compose ",[51,2799,2802],{"href":2800,"rel":2801},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002Fhow-tos\u002Fenvironment-variables\u002Fvariable-interpolation\u002F",[55],"interpoliert"," ein ",[14,2805,2806],{},"$NAME"," in dieser Datei gegen die Umgebung des Hosts, nicht des Containers. Ein nicht gesetzter Name wurde ein leerer String, ein Passwort mit ",[14,2809,2810],{},"$"," kam verstümmelt an. Knecht expandiert Referenzen jetzt selbst, gegen ",[14,2813,2467],{}," und die vorherigen Zeilen, und escaped jedes übrige ",[14,2816,2810],{}," als ",[14,2819,2820],{},"$$",". ",[14,2823,2824],{},"APP_URL=$KNECHT_PREVIEW_URL"," ergibt so die Preview-URL der Session, ",[14,2827,2828],{},"SITE_URL=${APP_URL}\u002Fen"," baut darauf auf, und ein ",[14,2831,2810],{}," im Passwort bleibt ein ",[14,2834,2810],{},[79,2836,2838],{"id":2837},"nuxt-hinter-nuxt","Nuxt hinter Nuxt",[10,2840,2841,2842,2845,2846,2851,2852,2857,2858,2064,2861,2866,2867,2870],{},"Das Dashboard ist eine Nuxt-App, und die Preview eines Nuxt-Projekts läuft über einen Proxy in derselben App. Beide liefern ihre Assets unter ",[14,2843,2844],{},"\u002F_nuxt\u002F"," aus. ",[51,2847,2850],{"href":2848,"rel":2849},"https:\u002F\u002Fvite.dev\u002F",[55],"Vites"," Dev-Middleware und ",[51,2853,2856],{"href":2854,"rel":2855},"https:\u002F\u002Fnitro.build\u002F",[55],"Nitros"," Static-Handler beantworten Anfragen auf diesen Pfad, bevor eine Server-Middleware dran ist. Die Assets der Preview kamen also beim Dashboard an und liefen ins Leere, 404 in Production und ENOENT im Dev-Modus. Die Lösung ist eine Zeile in der ",[14,2859,2860],{},"nuxt.config.ts",[51,2862,2865],{"href":2863,"rel":2864},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fnuxt-config#buildassetsdir",[55],"buildAssetsDir"," auf ",[14,2868,2869],{},"\u002F_knecht\u002F",". Wer eine Nuxt-App durch eine Nuxt-App proxyt, braucht sie auch.",[19,2872,2874],{"id":2873},"einschränkungen","Einschränkungen",[10,2876,2877,2878,2880],{},"Die generierte Umgebung ist ein DDEV-Projekt mit PHP und Node, mehr nicht. Damit läuft alles, was mit diesen beiden auskommt, also Libraries, Plugins, Frontends und Node-Tools. Was darüber hinausgeht, braucht weiterhin eine eigene ",[14,2879,2051],{}," im Repo:",[139,2882,2883,2886,2889],{},[142,2884,2885],{},"eine Datenbank, weil die generierte Umgebung keinen Datenbank-Container hat",[142,2887,2888],{},"zusätzliche Dienste wie Redis oder Elasticsearch",[142,2890,2891],{},"andere Laufzeiten wie Python, Ruby oder Go, die das DDEV-Image nicht mitbringt",[10,2893,2894],{},"Repos, die statt DDEV eine eigene docker-compose-Datei mitbringen, kann Knecht weiterhin nicht booten.",[34,2896,2899],{"color":2897,"icon":2898},"neutral","i-lucide-arrow-right",[10,2900,2901,2902,70],{},"Fälle, in denen ein Projekt auf Knecht anders läuft als lokal, sammeln wir mit dem Fix je Framework in den Docs unter ",[51,2903,2905],{"href":2904},"\u002Fdocs\u002Fresources\u002Ftroubleshooting","Troubleshooting",[2010,2907,2908],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":290,"searchDepth":291,"depth":291,"links":2910},[2911,2918,2922,2929],{"id":2078,"depth":291,"text":2079,"children":2912},[2913,2914,2915,2916,2917],{"id":2170,"depth":297,"text":2171},{"id":2338,"depth":297,"text":2151},{"id":2370,"depth":297,"text":2371},{"id":2416,"depth":297,"text":2417},{"id":2442,"depth":297,"text":2443},{"id":2456,"depth":291,"text":2457,"children":2919},[2920,2921],{"id":2476,"depth":297,"text":2057},{"id":2651,"depth":297,"text":2652},{"id":2678,"depth":291,"text":2679,"children":2923},[2924,2925,2926,2927,2928],{"id":2685,"depth":297,"text":2686},{"id":2742,"depth":297,"text":2743},{"id":2763,"depth":297,"text":2764},{"id":2793,"depth":297,"text":2794},{"id":2837,"depth":297,"text":2838},{"id":2873,"depth":291,"text":2874},"2026-09-11","Knecht baut die Umgebung jetzt aus den Repo-Dateien, wenn keine DDEV-Config da ist. Ein Dev-Server wird zur Live-Preview, mit Hot Reload.",{},"\u002Fde\u002Fupdates\u002Frepos-without-ddev-config",{"title":2043,"description":2931},"de\u002Fupdates\u002Frepos-without-ddev-config","Engine","SaRM07iKbKQrgCUWpY9iFkLI6V4ZooBDM_8Be326_ZM",{"id":2939,"title":2940,"body":2941,"date":3180,"description":3181,"extension":310,"meta":3182,"navigation":312,"path":3183,"seo":3184,"stem":3185,"tag":3186,"__hash__":3187},"updates_de\u002Fde\u002Fupdates\u002Fdocs-online.md","Die Docs sind online",{"type":7,"value":2942,"toc":3168},[2943,2951,2955,2958,2986,2989,2993,2997,3009,3015,3019,3028,3034,3037,3041,3044,3060,3064,3067,3081,3085,3088,3146,3150,3153,3157,3160],[10,2944,2945,2946,2950],{},"Bisher gab es zu Knecht nur diese Updates. Sie erzählen, warum etwas so gebaut ist, aber nicht, wie man es einrichtet. Wer eine Instanz aufsetzen oder einen Workflow bauen wollte, musste sich die Schritte aus mehreren Posts zusammensuchen. Seit dieser Woche gibt es unter ",[51,2947,2949],{"href":2948},"\u002Fdocs","knecht.works\u002Fdocs"," eine Dokumentation. Dieser Post zeigt, was drinsteht und was die Seiten außer Text noch können.",[19,2952,2954],{"id":2953},"was-drinsteht","Was drinsteht",[10,2956,2957],{},"Die Docs sind auf Englisch, so wie das Dashboard, damit die Begriffe auf beiden Seiten gleich heißen. Sie bestehen aus drei Teilen.",[2959,2960,2961,2970,2978],"card-group",{},[2962,2963,2967],"card",{"icon":2964,"title":2965,"to":2966},"i-lucide-rocket","Get Started","\u002Fdocs\u002Fget-started\u002Fintroduction",[10,2968,2969],{},"Was Knecht ist, die Begriffe aus dem Dashboard, Installation, Setup und der laufende Betrieb.",[2962,2971,2975],{"icon":2972,"title":2973,"to":2974},"i-lucide-workflow","Usage","\u002Fdocs\u002Fusage\u002Fprojects",[10,2976,2977],{},"Projekte, Workflows, Trigger und der AI Agent, also die Dinge, mit denen man täglich arbeitet.",[2962,2979,2983],{"icon":2980,"title":2981,"to":2982},"i-lucide-life-buoy","Resources","\u002Fdocs\u002Fresources\u002Fbeta-testers",[10,2984,2985],{},"Die Seite für Beta-Tester und ein Troubleshooting je Framework.",[10,2987,2988],{},"Vieles davon ist noch nicht sehr detailliert. Die Docs wachsen nach und nach mit dem Produkt. Knecht selbst hat noch einige Ecken und Kanten, und die polieren wir zuerst aus, bevor wir sie ausführlich dokumentieren. Sonst müssten wir die Seiten kurz darauf wieder umschreiben.",[19,2990,2992],{"id":2991},"features","Features",[79,2994,2996],{"id":2995},"suche","Suche",[10,2998,2999,3000,3004,3005,3008],{},"Oben rechts sitzt eine Suche, die auch über ",[3001,3002],"kbd",{"value":3003},"meta"," ",[3001,3006],{"value":3007},"K"," aufgeht. Sie durchsucht alle Docs-Seiten bis auf Abschnittsebene, ein Treffer springt also direkt zur passenden Überschrift.",[10,3010,3011],{},[2447,3012],{"alt":3013,"src":3014},"Die Docs-Suche über der Seite \"Introduction\", die Treffer gruppiert nach Get Started, Usage und Resources","\u002Fassets\u002Fdocs-search.png",[79,3016,3018],{"id":3017},"frag-knecht","Frag Knecht",[10,3020,3021,3022,3004,3024,3027],{},"Neben der Suche gibt es den Button \"Frag Knecht\", der auch auf ",[3001,3023],{"value":3003},[3001,3025],{"value":3026},"I"," hört. Er öffnet ein Chat-Fenster, in dem man Fragen in ganzen Sätzen stellt, auf Deutsch oder Englisch. Der Assistent antwortet aus dem Inhalt der Website, also aus den Docs und diesen Updates, und verlinkt die Seite, auf der die Antwort steht. Was auf der Website nicht steht, weiß er nicht, und das sagt er dann auch.",[10,3029,3030],{},[2447,3031],{"alt":3032,"src":3033},"Das Panel \"Frag Knecht\" rechts neben den Docs, mit der Frage nach der Installation und einer Antwort mit Anforderungen und dem Install-Befehl","\u002Fassets\u002Fdocs-ai.png",[10,3035,3036],{},"Öffnet man das Fenster auf einer Docs-Seite, bekommt der Assistent diese Seite als Kontext mit und bevorzugt sie bei der Antwort. Das lässt sich im Fenster abschalten. Dahinter läuft ein Claude-Modell, das bei jeder Frage den kompletten Inhalt der Website mitbekommt. Knecht speichert die Chats nicht.",[79,3038,3040],{"id":3039},"docs-als-markdown","Docs als Markdown",[10,3042,3043],{},"Jede Docs-Seite gibt es auch als reines Markdown. Der Button \"Copy page\" oben auf der Seite kopiert den Text in die Zwischenablage. Das Menü daneben bietet den Markdown-Link, die Rohansicht und die Möglichkeit, die Seite direkt in ChatGPT oder Claude zu öffnen. Wer Knecht mit einem Coding-Agenten einrichtet, gibt ihm so die passende Seite mit, ohne HTML drumherum.",[3045,3046,3047],"tip",{},[10,3048,3049,3050,3054,3055,3059],{},"Für alles auf einmal gibt es ",[51,3051,3053],{"href":3052},"\u002Fllms.txt","llms.txt"," mit der Übersicht und ",[51,3056,3058],{"href":3057},"\u002Fllms-full.txt","llms-full.txt"," mit dem kompletten Inhalt der Website in einer Datei.",[79,3061,3063],{"id":3062},"feedback-pro-seite","Feedback pro Seite",[10,3065,3066],{},"Auf jeder Docs-Seite kann man uns direkt sagen, ob sie geholfen hat und was fehlt.",[139,3068,3069,3072],{},[142,3070,3071],{},"Am Ende jeder Seite steht die Frage \"Was this page helpful?\" mit Daumen hoch und Daumen runter. Ein Klick genügt, wir sehen pro Seite, wie oft welche Antwort kam.",[142,3073,3074,3075,3080],{},"\"Provide Feedback\" öffnet ein GitHub-Issue im ",[51,3076,3079],{"href":3077,"rel":3078},"https:\u002F\u002Fgithub.com\u002Fknecht-works\u002Fknecht-www",[55],"Repo der Website",", mit dem Seitentitel schon im Betreff. Das Repo ist öffentlich, ein Tippfehler lässt sich also auch direkt als Pull Request korrigieren.",[79,3082,3084],{"id":3083},"shortcuts","Shortcuts",[10,3086,3087],{},"Suche und \"Frag Knecht\" lassen sich komplett über die Tastatur bedienen.",[2084,3089,3090,3100],{},[2087,3091,3092],{},[2090,3093,3094,3097],{},[2093,3095,3096],{},"Shortcut",[2093,3098,3099],{},"Aktion",[2102,3101,3102,3113,3124,3136],{},[2090,3103,3104,3110],{},[2107,3105,3106,3004,3108],{},[3001,3107],{"value":3003},[3001,3109],{"value":3007},[2107,3111,3112],{},"Suche öffnen",[2090,3114,3115,3121],{},[2107,3116,3117,3004,3119],{},[3001,3118],{"value":3003},[3001,3120],{"value":3026},[2107,3122,3123],{},"\"Frag Knecht\" öffnen und schließen",[2090,3125,3126,3133],{},[2107,3127,3128,3004,3130],{},[3001,3129],{"value":3003},[3001,3131],{"value":3132},"O",[2107,3134,3135],{},"Neuen Chat starten",[2090,3137,3138,3143],{},[2107,3139,3140],{},[3001,3141],{"value":3142},"tab",[2107,3144,3145],{},"Aktuelle Seite als Kontext an- oder abschalten",[19,3147,3149],{"id":3148},"ecken-und-kanten","Ecken und Kanten",[10,3151,3152],{},"Die Docs sind neu, und wir haben sie als die Leute geschrieben, die Knecht am besten kennen. Was für uns offensichtlich ist, fehlt vielleicht genau denen, die es zum ersten Mal einrichten. Uns interessiert deshalb vor allem, wo ihr hängen geblieben seid. Welche Seite hat eine Frage offen gelassen, welcher Begriff ist unerklärt, welches Setup hat nicht funktioniert wie beschrieben. Kleine Hinweise sind dabei oft die nützlichsten.",[19,3154,3156],{"id":3155},"discord","Discord",[10,3158,3159],{},"Für alles, was kein Issue ist, gibt es seit Kurzem einen Discord-Server. Dort beantworten wir Fragen zum Setup, diskutieren Workflow-Ideen und zeigen, woran wir gerade bauen. Wer Knecht ausprobiert oder es vorhat, ist dort willkommen, auch ohne konkrete Frage.",[34,3161,3165],{"color":36,"icon":3162,"target":3163,"to":3164},"i-simple-icons-discord","_blank","https:\u002F\u002Fdiscord.gg\u002FWuxjmtgUyX",[10,3166,3167],{},"Dem Knecht-Discord beitreten",{"title":290,"searchDepth":291,"depth":291,"links":3169},[3170,3171,3178,3179],{"id":2953,"depth":291,"text":2954},{"id":2991,"depth":291,"text":2992,"children":3172},[3173,3174,3175,3176,3177],{"id":2995,"depth":297,"text":2996},{"id":3017,"depth":297,"text":3018},{"id":3039,"depth":297,"text":3040},{"id":3062,"depth":297,"text":3063},{"id":3083,"depth":297,"text":3084},{"id":3148,"depth":291,"text":3149},{"id":3155,"depth":291,"text":3156},"2026-09-10","Knecht hat jetzt eine Dokumentation mit Suche, einem Chat-Assistenten und Feedback auf jeder Seite. Und einen Discord-Server, auf dem wir eure Rückmeldungen sammeln.",{},"\u002Fde\u002Fupdates\u002Fdocs-online",{"title":2940,"description":3181},"de\u002Fupdates\u002Fdocs-online","Projekt","Cfh_v7FHO7FrmEwzpBiRcWAQHyBk6svzEvJ9Vp2rodw",{"id":3189,"title":3190,"body":3191,"date":3563,"description":3564,"extension":310,"meta":3565,"navigation":312,"path":3566,"seo":3567,"stem":3568,"tag":2936,"__hash__":3569},"updates_de\u002Fde\u002Fupdates\u002Flangdock-provider.md","Langdock als AI-Provider",{"type":7,"value":3192,"toc":3557},[3193,3207,3211,3219,3222,3226,3235,3242,3398,3404,3410,3414,3422,3428,3431,3436,3440,3443,3551,3554],[10,3194,3195,3196,3200,3201,3206],{},"Der Agent in der ",[51,3197,3199],{"href":3198},"\u002Fupdates\u002Fworkflow-engine","AI-Action"," ist ",[51,3202,3205],{"href":3203,"rel":3204},"https:\u002F\u002Fopencode.ai",[55],"opencode",". Bisher lief er über die Provider von OpenCode selbst, also Zen oder Go, mit einem Key aus der OpenCode-Konsole. Jetzt haben wir Langdock als Provider dazu gebaut, eine europäische Alternative. Ein API-Key deckt auch hier die Modelle mehrerer Anbieter ab, aber jede Anfrage des Agenten bleibt in der gewählten Region.",[19,3208,3210],{"id":3209},"was-langdock-ist","Was Langdock ist",[10,3212,3213,3218],{},[51,3214,3217],{"href":3215,"rel":3216},"https:\u002F\u002Flangdock.com",[55],"Langdock"," ist ein AI-Gateway für europäische Firmen. Es sitzt vor mehreren Modell-Anbietern, darunter OpenAI, Anthropic, Google, Meta und Mistral. Das Team schließt einen Vertrag und bekommt einen API-Key, dahinter sind die Modelle aller Anbieter erreichbar.",[10,3220,3221],{},"Der zweite Grund für so ein Gateway ist Data Residency, also die Zusage, dass jede Anfrage in einer festen Region verarbeitet wird, EU oder US. Das betrifft den Agenten direkt, denn seine Prompts enthalten Code und Daten aus dem Kundenprojekt. Viele Kundenverträge schreiben vor, dass solche Daten die EU nicht verlassen. Mit Langdock in der Region EU gilt das auch für die Arbeit des Agenten, und oft ist das die Bedingung, unter der er auf dem Projekt überhaupt eingesetzt werden darf.",[19,3223,3225],{"id":3224},"opencode-kennt-langdock-nicht","Opencode kennt Langdock nicht",[10,3227,3228,3229,3234],{},"opencode löst Provider über das Register ",[51,3230,3233],{"href":3231,"rel":3232},"https:\u002F\u002Fmodels.dev",[55],"models.dev"," auf. Für die bisherigen Provider reicht deshalb eine Umgebungsvariable mit dem API-Key, alles Weitere weiß opencode selbst. Langdock steht in dem Register nicht, dieser Weg fällt also aus.",[10,3236,3237,3238,3241],{},"Stattdessen erzeugt Knecht für jeden Run eine ",[14,3239,3240],{},"opencode.json"," im Checkout und deklariert Langdock darin als eigenen Provider, mit dem Endpoint der gewählten Region und genau den Modellen, die dieser Run nutzen darf. Gekürzt sieht der Block so aus:",[390,3243,3247],{"className":3244,"code":3245,"language":3246,"meta":290,"style":290},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"provider\": {\n    \"langdock\": {\n      \"name\": \"Langdock\",\n      \"options\": {\n        \"baseURL\": \"https:\u002F\u002Fapi.langdock.com\u002Fopenai\u002Feu\u002Fv1\",\n        \"apiKey\": \"{env:LANGDOCK_API_KEY}\"\n      },\n      \"models\": { \"gpt-5.5\": {} }\n    }\n  }\n}\n","json",[14,3248,3249,3253,3268,3282,3302,3314,3335,3353,3358,3385,3390,3394],{"__ignoreMap":290},[398,3250,3251],{"class":400,"line":401},[398,3252,429],{"class":412},[398,3254,3255,3258,3261,3264,3266],{"class":400,"line":291},[398,3256,3257],{"class":412},"  \"",[398,3259,3260],{"class":489},"provider",[398,3262,3263],{"class":412},"\"",[398,3265,505],{"class":412},[398,3267,497],{"class":412},[398,3269,3270,3273,3276,3278,3280],{"class":400,"line":297},[398,3271,3272],{"class":412},"    \"",[398,3274,3275],{"class":493},"langdock",[398,3277,3263],{"class":412},[398,3279,505],{"class":412},[398,3281,497],{"class":412},[398,3283,3284,3287,3290,3292,3294,3296,3298,3300],{"class":400,"line":527},[398,3285,3286],{"class":412},"      \"",[398,3288,1109],{"class":3289},"sbssI",[398,3291,3263],{"class":412},[398,3293,505],{"class":412},[398,3295,2242],{"class":412},[398,3297,3217],{"class":419},[398,3299,3263],{"class":412},[398,3301,946],{"class":412},[398,3303,3304,3306,3308,3310,3312],{"class":400,"line":542},[398,3305,3286],{"class":412},[398,3307,1363],{"class":3289},[398,3309,3263],{"class":412},[398,3311,505],{"class":412},[398,3313,497],{"class":412},[398,3315,3316,3319,3322,3324,3326,3328,3331,3333],{"class":400,"line":548},[398,3317,3318],{"class":412},"        \"",[398,3320,3321],{"class":441},"baseURL",[398,3323,3263],{"class":412},[398,3325,505],{"class":412},[398,3327,2242],{"class":412},[398,3329,3330],{"class":419},"https:\u002F\u002Fapi.langdock.com\u002Fopenai\u002Feu\u002Fv1",[398,3332,3263],{"class":412},[398,3334,946],{"class":412},[398,3336,3337,3339,3342,3344,3346,3348,3351],{"class":400,"line":578},[398,3338,3318],{"class":412},[398,3340,3341],{"class":441},"apiKey",[398,3343,3263],{"class":412},[398,3345,505],{"class":412},[398,3347,2242],{"class":412},[398,3349,3350],{"class":419},"{env:LANGDOCK_API_KEY}",[398,3352,2248],{"class":412},[398,3354,3355],{"class":400,"line":618},[398,3356,3357],{"class":412},"      },\n",[398,3359,3360,3362,3365,3367,3369,3371,3373,3376,3378,3380,3383],{"class":400,"line":652},[398,3361,3286],{"class":412},[398,3363,3364],{"class":3289},"models",[398,3366,3263],{"class":412},[398,3368,505],{"class":412},[398,3370,587],{"class":412},[398,3372,2242],{"class":412},[398,3374,3375],{"class":441},"gpt-5.5",[398,3377,3263],{"class":412},[398,3379,505],{"class":412},[398,3381,3382],{"class":412}," {}",[398,3384,615],{"class":412},[398,3386,3387],{"class":400,"line":687},[398,3388,3389],{"class":412},"    }\n",[398,3391,3392],{"class":400,"line":1133},[398,3393,545],{"class":412},[398,3395,3396],{"class":400,"line":1143},[398,3397,453],{"class":412},[10,3399,3400,3401,3403],{},"Der Key selbst steht nicht in der Datei. Die Config verweist mit ",[14,3402,3350],{}," nur auf eine Umgebungsvariable, und Knecht übergibt den Wert direkt dem opencode-Prozess. Im Checkout, in dem der Agent arbeitet, landet der Key damit nie.",[10,3405,3406,3407,3409],{},"Eine Eigenheit des Gateways steckt noch in der ",[14,3408,3321],{},". Langdock spricht je nach Modell eine andere API, Claude-Modelle die Anthropic-kompatible, alle anderen die OpenAI-kompatible. Knecht entscheidet am Modellnamen, unter welchem Endpoint ein Modell in der Config deklariert wird. Im Picker wählt man nur ein Modell, von der Unterscheidung merkt man nichts.",[19,3411,3413],{"id":3412},"die-einrichtung","Die Einrichtung",[10,3415,3416,3417,3421],{},"Alles dazu liegt unter \"Settings\" → \"Agent\". Dort wählt man Langdock als Provider, daneben erscheint die Auswahl der Region. Sie gilt für die ganze Instanz, also für jede Anfrage aus jedem Run. Danach speichert man den Langdock-API-Key. Er liegt wie alle Provider-Keys ",[51,3418,3420],{"href":3419},"\u002Fupdates\u002Fgh-auth-evolution","verschlüsselt"," in der Datenbank und lässt sich nachträglich nur ersetzen, nicht wieder auslesen.",[10,3423,3424],{},[2447,3425],{"alt":3426,"src":3427},"Die Agent-Settings mit Langdock als Provider, der Region-Auswahl und dem geöffneten Modell-Picker","\u002Fassets\u002Fsettings-agent-langdock.png",[10,3429,3430],{},"Sobald der Key gespeichert ist, lädt der Modell-Picker die Liste live aus dem Langdock-Workspace. Es erscheinen genau die Modelle, die dort freigeschaltet sind, GPT- und Claude-Modelle im selben Picker. Default-Modell und das optionale Subtask-Modell, das kleinere Modell für Nebenaufgaben des Agenten, funktionieren wie bei jedem anderen Provider.",[325,3432,3433],{},[10,3434,3435],{},"Beim Wechsel des Providers leert Knecht die gespeicherten Modelle, denn die alten Namen würden im Katalog des neuen Providers nicht auflösen. Die AI-Action läuft erst wieder, wenn ein neues Default-Modell gewählt ist.",[19,3437,3439],{"id":3438},"modelle-mischen","Modelle mischen",[10,3441,3442],{},"An der AI-Action selbst ändert sich nichts. Der Agent arbeitet wie gewohnt im laufenden Projekt, liest Code, ändert Dateien und führt Befehle aus, auch mit Reasoning-Modellen wie GPT-5.5. Und weil ein Key alle Modelle des Workspace abdeckt, kann ein Workflow über den Modell-Override pro Schritt weiterhin die Anbieter mischen:",[390,3444,3446],{"className":2183,"code":3445,"language":2185,"meta":290,"style":290},"steps:\n  - type: ai\n    id: analyze\n    label: Analyze bug\n    model: gpt-5.5\n    prompt: Stelle den Bug aus {{ inputs.title }} nach und beschreibe die Ursache.\n  - type: ai\n    id: fix\n    label: Fix bug\n    model: claude-sonnet-4-5\n    prompt: Behebe die Ursache aus {{ steps.analyze.text }}.\n",[14,3447,3448,3454,3465,3475,3485,3495,3505,3515,3524,3533,3542],{"__ignoreMap":290},[398,3449,3450,3452],{"class":400,"line":401},[398,3451,75],{"class":441},[398,3453,2270],{"class":412},[398,3455,3456,3458,3460,3462],{"class":400,"line":291},[398,3457,2275],{"class":412},[398,3459,1435],{"class":441},[398,3461,505],{"class":412},[398,3463,3464],{"class":419}," ai\n",[398,3466,3467,3470,3472],{"class":400,"line":297},[398,3468,3469],{"class":441},"    id",[398,3471,505],{"class":412},[398,3473,3474],{"class":419}," analyze\n",[398,3476,3477,3480,3482],{"class":400,"line":527},[398,3478,3479],{"class":441},"    label",[398,3481,505],{"class":412},[398,3483,3484],{"class":419}," Analyze bug\n",[398,3486,3487,3490,3492],{"class":400,"line":542},[398,3488,3489],{"class":441},"    model",[398,3491,505],{"class":412},[398,3493,3494],{"class":419}," gpt-5.5\n",[398,3496,3497,3500,3502],{"class":400,"line":548},[398,3498,3499],{"class":441},"    prompt",[398,3501,505],{"class":412},[398,3503,3504],{"class":419}," Stelle den Bug aus {{ inputs.title }} nach und beschreibe die Ursache.\n",[398,3506,3507,3509,3511,3513],{"class":400,"line":578},[398,3508,2275],{"class":412},[398,3510,1435],{"class":441},[398,3512,505],{"class":412},[398,3514,3464],{"class":419},[398,3516,3517,3519,3521],{"class":400,"line":618},[398,3518,3469],{"class":441},[398,3520,505],{"class":412},[398,3522,3523],{"class":419}," fix\n",[398,3525,3526,3528,3530],{"class":400,"line":652},[398,3527,3479],{"class":441},[398,3529,505],{"class":412},[398,3531,3532],{"class":419}," Fix bug\n",[398,3534,3535,3537,3539],{"class":400,"line":687},[398,3536,3489],{"class":441},[398,3538,505],{"class":412},[398,3540,3541],{"class":419}," claude-sonnet-4-5\n",[398,3543,3544,3546,3548],{"class":400,"line":1133},[398,3545,3499],{"class":441},[398,3547,505],{"class":412},[398,3549,3550],{"class":419}," Behebe die Ursache aus {{ steps.analyze.text }}.\n",[10,3552,3553],{},"Beide Schritte laufen über denselben Langdock-Key und in derselben Region. Der Wechsel auf das EU-Gateway kostet an dieser Stelle also nichts, was man von OpenCode Zen gewohnt ist.",[2010,3555,3556],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":290,"searchDepth":291,"depth":291,"links":3558},[3559,3560,3561,3562],{"id":3209,"depth":291,"text":3210},{"id":3224,"depth":291,"text":3225},{"id":3412,"depth":291,"text":3413},{"id":3438,"depth":291,"text":3439},"2026-08-18","Der Agent läuft jetzt auch über Langdock. Ein API-Key deckt GPT- und Claude-Modelle ab, und jede Anfrage bleibt in der EU oder in den USA.",{},"\u002Fde\u002Fupdates\u002Flangdock-provider",{"title":3190,"description":3564},"de\u002Fupdates\u002Flangdock-provider","v0B0iRiSrixuufHHrkRDiDpf6ZPZwtvPDZflxGIzLIQ",{"id":3571,"title":338,"body":3572,"date":3687,"description":3688,"extension":310,"meta":3689,"navigation":312,"path":3690,"seo":3691,"stem":3692,"tag":2936,"__hash__":3693},"updates_de\u002Fde\u002Fupdates\u002Fsessions-and-mentions.md",{"type":7,"value":3573,"toc":3681},[3574,3580,3584,3596,3602,3605,3611,3614,3620,3623,3627,3630,3636,3639,3653,3661,3665,3674,3678],[10,3575,3576,3577,3579],{},"Bisher lief bei Knecht alles in eine Richtung. Ein ",[51,3578,787],{"href":3198}," feuerte, ein Workflow lief, und das Ergebnis lag im Dashboard. Wer im GitHub-Issue auf eine Antwort wartete, sah dort nichts. Mit diesem Update arbeitet Knecht dort, wo die Arbeit gemeldet wird. Er antwortet und labelt direkt im Issue-Thread, und mit einer Mention im Kommentar schickt ihr ihn zurück an die Arbeit.",[19,3581,3583],{"id":3582},"vom-bug-report-zum-pr","Vom Bug-Report zum PR",[10,3585,3586,3587,3591,3592,3595],{},"So sieht das auf unserem Craft-Testprojekt aus. Jemand meldet per Issue, dass die Seite keinen Dark Mode hat. Ein Trigger startet den Workflow \"Classify Issue\". Knecht bootet die Seite in ihrer ",[51,3588,3590],{"href":3589},"\u002Fupdates\u002Fsandbox-rollback","Preview-Umgebung",", stellt den Report nach und findet heraus, dass Dark Mode schlicht nie implementiert wurde. Er setzt das vorhandene Label ",[14,3593,3594],{},"enhancement"," und antwortet dem Reporter im Thread.",[10,3597,3598],{},[2447,3599],{"alt":3600,"src":3601},"GitHub-Issue mit einem Dark-Mode-Bug-Report, unter dem die App knecht-works das Label enhancement gesetzt hat","\u002Fassets\u002Fissue-creation-with-label.png",[10,3603,3604],{},"Die Antwort enthält den Befund und einen konkreten Plan für die Umsetzung, mit der Preview-URL am Ende.",[10,3606,3607],{},[2447,3608],{"alt":3609,"src":3610},"Kommentar der App knecht-works im Issue mit Befund und Umsetzungsplan, darunter ein Kommentar des Teammitglieds mit einer Mention und einer Augen-Reaktion","\u002Fassets\u002Fissue-follow-up.png",[10,3612,3613],{},"Ein Teammitglied liest die Analyse und antwortet mit einer Mention, sinngemäß \"@knecht-works, setz das um und öffne einen PR\". Knecht bestätigt sofort mit einer Augen-Reaktion und arbeitet weiter, in derselben Umgebung und demselben Gespräch wie die Triage. Er kennt also den Befund und den Plan schon. Ein paar Minuten später steht die Antwort im Thread, mit dem geöffneten PR und der Preview-URL.",[10,3615,3616],{},[2447,3617],{"alt":3618,"src":3619},"Antwort der App knecht-works im Issue mit einer Zusammenfassung der beiden Commits, am Ende Preview-URL und PR-Link","\u002Fassets\u002Fissue-enhancement-finished.png",[10,3621,3622],{},"Dass der Fix auf der Triage aufbauen kann, liegt an der größten Änderung dieses Updates, den Sessions.",[19,3624,3626],{"id":3625},"eine-session-pro-issue","Eine Session pro Issue",[10,3628,3629],{},"Bisher war jede Workflow-Ausführung ihre eigene Welt, mit eigenem Checkout, eigener Umgebung und eigenem Agenten-Gespräch. Beim Bauen der Antwort-Schleife hat sich gezeigt, dass das die falsche Einheit ist, denn Umgebung und Kontext gehören zum Issue, nicht zur einzelnen Ausführung. Deshalb gibt es jetzt Sessions. Eine Session gehört zu genau einem Issue oder PR und hält dessen Checkout, dessen Umgebung und ein gemeinsames Agenten-Gespräch. Darin laufen die Runs, also einzelne Workflow-Ausführungen, und Follow-ups, also einzelne Nachrichten an den Agenten.",[10,3631,3632],{},[2447,3633],{"alt":3634,"src":3635},"Projektseite im Knecht-Dashboard mit der Run-Liste, gruppiert nach Issues, unter dem Dark-Mode-Issue vier Runs von der Triage bis zu zwei Mentions","\u002Fassets\u002Fknecht-project-triggers-in-action.png",[10,3637,3638],{},"Die Projektseite und die Run-Übersicht gruppieren Runs jetzt nach Session. Im Screenshot gehören vier Runs zum Dark-Mode-Issue, von \"Classify Issue\" bis zu den beiden Mentions, alle in derselben Umgebung. Im Detail heißt das:",[139,3640,3641,3644,3647,3650],{},[142,3642,3643],{},"Feuern zwei Trigger auf dasselbe Issue, laufen sie nacheinander in der Session, nie parallel.",[142,3645,3646],{},"Wird das Issue geschlossen, schließt auch die Session. Ein Reopen belebt sie wieder, solange ihre Umgebung noch existiert.",[142,3648,3649],{},"Beim Archivieren einer Umgebung sichert Knecht jetzt auch das Gespräch des Agenten, ein paar Megabyte. Es übersteht damit einen Archive-Restore-Zyklus.",[142,3651,3652],{},"Events ohne Issue oder PR, also Push, Zeitplan und manueller Start, verhalten sich wie bisher und bekommen eine Session mit einem einzigen Run.",[10,3654,3655,3656,3660],{},"Wird eine Umgebung ganz abgebaut, ist das Gespräch weg. Der Agent setzt dann nicht stumm bei null an, sondern liest den GitHub-Thread, die Ergebnisse der bisherigen Actions und das ",[51,3657,3659],{"href":3658},"\u002Fupdates\u002Fagent-memory","Projekt-Gedächtnis",", und sagt im Thread dazu, dass er neu ansetzt. Bestehende Installationen müssen dafür nichts tun, das Update stellt vorhandene Runs und Umgebungen beim Start automatisch auf Sessions um.",[19,3662,3664],{"id":3663},"das-beispiel-cloudflare","Das Beispiel Cloudflare",[10,3666,3667,3668,3673],{},"Dass sich Issue-Triage komplett automatisieren lässt, hat Cloudflare im August 2026 am Repository des Astro-Frameworks gezeigt. Ihr ",[51,3669,3672],{"href":3670,"rel":3671},"https:\u002F\u002Fblog.cloudflare.com\u002Fastro-issue-triage\u002F",[55],"Triage-System"," reproduziert jeden Report, diagnostiziert die Ursache und baut den Fix, die offenen Issues sind damit von über 200 auf rund 30 gefallen. Es ist allerdings ein eigenes Stück Software, von einem Team bei Cloudflare für genau dieses Repository gebaut. Bei Knecht ist derselbe Ablauf ein Workflow, den ihr auf eure eigenen Projekte legt, und die Session bringt die gebootete Seite gleich mit.",[19,3675,3677],{"id":3676},"wie-es-weitergeht","Wie es weitergeht",[10,3679,3680],{},"Als Nächstes gehen wir Jira an. Die Konzepte bleiben dieselben, ein Ticket bekommt seine Session mit Umgebung und Gespräch, und Antworten und Mentions funktionieren im Ticket genauso wie im Issue-Thread.",{"title":290,"searchDepth":291,"depth":291,"links":3682},[3683,3684,3685,3686],{"id":3582,"depth":291,"text":3583},{"id":3625,"depth":291,"text":3626},{"id":3663,"depth":291,"text":3664},{"id":3676,"depth":291,"text":3677},"2026-08-17","Jedes Issue bekommt eine eigene Session mit Umgebung und Gespräch, der Agent antwortet und labelt direkt im Thread, und mit einer Mention schickt ihr ihn zurück an die Arbeit.",{},"\u002Fde\u002Fupdates\u002Fsessions-and-mentions",{"title":338,"description":3688},"de\u002Fupdates\u002Fsessions-and-mentions","StP4NoR0sQvyXnz52kIItjekFU-XQTzgAIn6d71ainc",{"id":3695,"title":3696,"body":3697,"date":3936,"description":3937,"extension":310,"meta":3938,"navigation":312,"path":3939,"seo":3940,"stem":3941,"tag":2936,"__hash__":3942},"updates_de\u002Fde\u002Fupdates\u002Fagent-memory.md","Der Agent bekommt ein Gedächtnis pro Projekt",{"type":7,"value":3698,"toc":3930},[3699,3709,3713,3720,3736,3756,3759,3763,3775,3875,3884,3890,3894,3897,3900,3914,3920,3924,3927],[10,3700,3701,3702,3704,3705,3708],{},"Die ",[51,3703,3199],{"href":3198}," startet für jeden Run eine frische ",[51,3706,3707],{"href":3589},"Sandbox"," mit einem eigenen Checkout des Projekts. Der Agent wusste darin bisher nichts von früheren Runs. Was er über ein Projekt herausgefunden hatte, war danach weg, und der nächste Run musste es neu erkunden. Jetzt legt Knecht pro Projekt Notizen an. Der Agent schreibt sie selbst und findet sie beim nächsten Run wieder vor.",[19,3710,3712],{"id":3711},"jeder-run-fing-bei-null-an","Jeder Run fing bei null an",[10,3714,3715,3716,3719],{},"Wir haben uns im August 2026 angeschaut, wo langsame Runs ihre Zeit verlieren. Ein großer Teil war Wiederentdeckung. Eine einzeilige Änderung an einem ",[14,3717,3718],{},"font-weight"," dauerte auf einem Projekt, das schon Dutzende Runs hinter sich hatte, 280 Sekunden. Rund 160 davon verbrachte ein Subagent damit herauszufinden, wo die Styles liegen und wie das Projekt gebaut wird, also Fakten, die frühere Runs längst kannten.",[10,3721,3722,3725,3726,3731,3732,3735],{},[51,3723,3205],{"href":3203,"rel":3724},[55],", der Agent hinter der AI-Action, kennt von Haus aus nur statische ",[51,3727,3730],{"href":3728,"rel":3729},"https:\u002F\u002Fopencode.ai\u002Fdocs\u002Frules\u002F",[55],"Regel-Dateien"," wie eine ",[14,3733,3734],{},"AGENTS.md"," im Repository. Die schreibt ein Mensch von Hand, der Agent trägt dort selbst nichts ein. Es gibt Community-Plugins für gelerntes Memory, aber sie passen nicht zu Knecht.",[139,3737,3738,3741],{},[142,3739,3740],{},"Alles, was ein Plugin auf die Platte schreibt, landet im Wegwerf-Checkout des Runs und ist danach weg. Die Persistenz außerhalb des Runs müsste Knecht also sowieso bauen.",[142,3742,3743,3744,3749,3750,3755],{},"Das bekannteste Plugin, ",[51,3745,3748],{"href":3746,"rel":3747},"https:\u002F\u002Fgithub.com\u002Ftickernelz\u002Fopencode-mem",[55],"opencode-mem",", speichert seine Erinnerungen erst, wenn in der Session eine Weile nichts passiert, also in der Pause, in der ein Mensch im Chat gerade nichts tippt. Knecht startet opencode aber ",[51,3751,3754],{"href":3752,"rel":3753},"https:\u002F\u002Fopencode.ai\u002Fdocs\u002Fcli\u002F",[55],"nicht-interaktiv",", und dieser Prozess beendet sich sofort nach seiner Antwort. Die Pause gibt es nie, das Plugin würde also nie speichern.",[10,3757,3758],{},"Deshalb haben wir das direkt in Knecht gebaut, als kleinen Speicher pro Projekt auf dem Host, außerhalb jeder Sandbox.",[19,3760,3762],{"id":3761},"ein-index-und-themen-dateien","Ein Index und Themen-Dateien",[10,3764,3765,3766,3769,3770,3774],{},"Das Gedächtnis besteht aus einer Index-Datei ",[14,3767,3768],{},"MEMORY.md"," und beliebigen Themen-Dateien. Nach ein paar Runs auf einem ",[51,3771,2660],{"href":3772,"rel":3773},"https:\u002F\u002Fcraftcms.com",[55],"-Projekt kann es zum Beispiel so aussehen:",[3776,3777,3778,3799,3835],"code-tree",{"default-value":3768},[390,3779,3782],{"className":3780,"code":3781,"filename":3768,"language":310,"meta":290,"style":290},"language-md shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","- Styles: Tailwind, tokens, build step, see styles.md\n- Build and tests: commands and pitfalls, see build.md\n",[14,3783,3784,3792],{"__ignoreMap":290},[398,3785,3786,3789],{"class":400,"line":401},[398,3787,3788],{"class":412},"-",[398,3790,3791],{"class":408}," Styles: Tailwind, tokens, build step, see styles.md\n",[398,3793,3794,3796],{"class":400,"line":291},[398,3795,3788],{"class":412},[398,3797,3798],{"class":408}," Build and tests: commands and pitfalls, see build.md\n",[390,3800,3803],{"className":3780,"code":3801,"filename":3802,"language":310,"meta":290,"style":290},"Tailwind 4, tokens live in src\u002Fcss\u002Fconfig.css.\nFont sizes and weights only via text-* utilities, no raw values.\nAfter CSS changes run `ddev npm run build`,\ntemplates load the built file from web\u002Fdist\u002F.\n","styles.md",[14,3804,3805,3810,3815,3830],{"__ignoreMap":290},[398,3806,3807],{"class":400,"line":401},[398,3808,3809],{"class":408},"Tailwind 4, tokens live in src\u002Fcss\u002Fconfig.css.\n",[398,3811,3812],{"class":400,"line":291},[398,3813,3814],{"class":408},"Font sizes and weights only via text-* utilities, no raw values.\n",[398,3816,3817,3820,3823,3826,3828],{"class":400,"line":297},[398,3818,3819],{"class":408},"After CSS changes run ",[398,3821,3822],{"class":412},"`",[398,3824,3825],{"class":419},"ddev npm run build",[398,3827,3822],{"class":412},[398,3829,946],{"class":408},[398,3831,3832],{"class":400,"line":527},[398,3833,3834],{"class":408},"templates load the built file from web\u002Fdist\u002F.\n",[390,3836,3839],{"className":3780,"code":3837,"filename":3838,"language":310,"meta":290,"style":290},"Build: `ddev npm run build`\nTests: `ddev php vendor\u002Fbin\u002Fphpunit`\nThe build needs DDEV running, plain npm on the host\nfails on the node version.\n","build.md",[14,3840,3841,3853,3865,3870],{"__ignoreMap":290},[398,3842,3843,3846,3848,3850],{"class":400,"line":401},[398,3844,3845],{"class":408},"Build: ",[398,3847,3822],{"class":412},[398,3849,3825],{"class":419},[398,3851,3852],{"class":412},"`\n",[398,3854,3855,3858,3860,3863],{"class":400,"line":291},[398,3856,3857],{"class":408},"Tests: ",[398,3859,3822],{"class":412},[398,3861,3862],{"class":419},"ddev php vendor\u002Fbin\u002Fphpunit",[398,3864,3852],{"class":412},[398,3866,3867],{"class":400,"line":297},[398,3868,3869],{"class":408},"The build needs DDEV running, plain npm on the host\n",[398,3871,3872],{"class":400,"line":527},[398,3873,3874],{"class":408},"fails on the node version.\n",[10,3876,3877,3878,3883],{},"Der Index hält eine Zeile pro Thema und wird bei jedem Agenten-Aufruf in die Instructions gemischt, er ist also immer im Kontext. Damit das billig bleibt, ist er hart auf 2 KB begrenzt. Die Themen-Dateien liest der Agent nur, wenn der Index auf etwas Relevantes zeigt, sie kosten bis dahin keinen Kontext. Das Muster stammt aus Claude Code, dessen ",[51,3879,3882],{"href":3880,"rel":3881},"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fmemory",[55],"Auto-Memory"," genauso aufgebaut ist.",[10,3885,3886,3887,3889],{},"Gepflegt wird das Ganze vom Agenten selbst, nach Regeln in der ",[14,3888,3734],{},", die Knecht in jede Sandbox legt. Er soll Notizen umschreiben statt anhängen, Veraltetes löschen und Korrekturen festhalten, die im Follow-up-Chat auf der Run-Seite kommen. Wer dort einmal \"nein, wir benutzen keine Utility-Klassen\" schreibt, soll das kein zweites Mal schreiben müssen. Und er soll seine eigenen Notizen kurz prüfen, bevor er darauf aufbaut, denn das Projekt kann sich seit dem letzten Run geändert haben.",[19,3891,3893],{"id":3892},"kopieren-statt-mounten","Kopieren statt mounten",[10,3895,3896],{},"Vor jedem Agenten-Aufruf kopiert Knecht die Notizen in den Checkout des Runs, nach dem Aufruf zurück auf den Host. Das Zurückkopieren passiert auch bei fehlgeschlagenen Schritten, denn die Notizen halten Fakten über das Projekt fest, keinen Run-Status.",[10,3898,3899],{},"Ein Mount in die Sandbox wäre einfacher gewesen. Mit der Kopie arbeitet jeder Run aber nur auf seinem eigenen Stand, und Knecht hat beim Zurückkopieren eine Stelle, an der es prüfen kann, was der Agent geschrieben hat.",[139,3901,3902,3911],{},[142,3903,3904,3905,3910],{},"Übernommen werden nur einfache Markdown-Dateien auf oberster Ebene, keine ",[51,3906,3909],{"href":3907,"rel":3908},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fsymlink.7.html",[55],"Symlinks",", keine Unterordner, keine versteckten Dateien.",[142,3912,3913],{},"Der Index darf höchstens 2 KB groß sein, alle Dateien zusammen höchstens 64 KB. Ist die Kopie größer, übernimmt Knecht sie gar nicht, der letzte gültige Stand bleibt erhalten, und im Run-Log steht eine Zeile dazu.",[10,3915,3916,3917,3919],{},"Knecht verwirft zu große Kopien, statt sie zu kürzen, weil der Agent eine Kürzung nicht mitbekommen würde und dann mit lückenhaften Notizen weiterarbeitet. Mit dem alten Stand kann er beim nächsten Run selbst aufräumen, die Regeln dafür stehen in der ",[14,3918,3734],{},". Dass die Limits eingehalten werden, prüft Knecht trotzdem selbst, damit der Kontext nicht wächst, wenn das Modell die Regeln ignoriert.",[19,3921,3923],{"id":3922},"was-noch-fehlt","Was noch fehlt",[10,3925,3926],{},"Eine Ansicht in den Projekt-Einstellungen, die zeigt, was sich Knecht gemerkt hat, gibt es noch nicht. Dort will man die Notizen lesen und auch korrigieren können. Offen ist auch, ob Knecht eine zu große Kopie irgendwann zusammenfassen soll, statt sie zu verwerfen, das lohnt sich aber erst, wenn Verwerfungen in der Praxis häufig vorkommen.",[2010,3928,3929],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":290,"searchDepth":291,"depth":291,"links":3931},[3932,3933,3934,3935],{"id":3711,"depth":291,"text":3712},{"id":3761,"depth":291,"text":3762},{"id":3892,"depth":291,"text":3893},{"id":3922,"depth":291,"text":3923},"2026-08-14","Bisher hat der Agent in jedem Run das Projekt neu erkundet. Jetzt merkt sich Knecht pro Projekt, was der Agent gelernt hat.",{},"\u002Fde\u002Fupdates\u002Fagent-memory",{"title":3696,"description":3937},"de\u002Fupdates\u002Fagent-memory","GHLoKjF5i7laRREfRc93y2QgZDRSgaOsZAZ_NYrq-tQ",{"id":3944,"title":3945,"body":3946,"date":4210,"description":4211,"extension":310,"meta":4212,"navigation":312,"path":4213,"seo":4214,"stem":4215,"tag":3186,"__hash__":4216},"updates_de\u002Fde\u002Fupdates\u002Flicense-decision.md","Warum Knecht nicht Open Source ist",{"type":7,"value":3947,"toc":4202},[3948,3957,3961,3970,3973,3977,3980,4013,4016,4020,4023,4110,4117,4136,4140,4143,4154,4163,4172,4176,4191,4193],[10,3949,3950,3951,3956],{},"Das ",[51,3952,3955],{"href":3953,"rel":3954},"https:\u002F\u002Fgithub.com\u002Fknecht-works\u002Fknecht-cloud",[55],"Knecht-Repo"," war von Anfang an öffentlich auf GitHub, eine Lizenz hatte es aber nie. Das haben wir jetzt geändert: Knecht steht ab sofort unter der Functional Source License, kurz FSL. Hier steht, warum überhaupt eine Lizenz nötig war, warum es kein MIT geworden ist und was du mit dem Code offiziell darfst.",[19,3958,3960],{"id":3959},"ohne-lizenz-geht-nichts","Ohne Lizenz geht nichts",[10,3962,3963,3964,3969],{},"Öffentlich sichtbar und frei nutzbar sind zwei verschiedene Dinge. Code ist urheberrechtlich geschützt wie ein Text oder ein Foto, und ohne ausdrückliche Lizenz gilt der Standard des Urheberrechts: alle Rechte vorbehalten. ",[51,3965,3968],{"href":3966,"rel":3967},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Frepositories\u002Fmanaging-your-repositorys-settings-and-features\u002Fcustomizing-your-repository\u002Flicensing-a-repository",[55],"GitHub weist selbst darauf hin",", dass ein öffentliches Repo ohne Lizenz nur angesehen und auf der Plattform geforkt werden darf. Herunterladen, verändern, produktiv einsetzen: genau genommen alles nicht erlaubt.",[10,3971,3972],{},"Ein Repo ohne Lizenz ist damit restriktiver als jede noch so strenge Lizenz. Absicht war das bei Knecht nicht, wir hatten die Lizenzfrage schlicht noch nicht entschieden. Wer Knecht in der Beta selbst hostet, tat das bisher mit unserer mündlichen, aber nirgends festgehaltenen Erlaubnis.",[19,3974,3976],{"id":3975},"das-dilemma","Das Dilemma",[10,3978,3979],{},"An die Lizenz hatten wir zwei Anforderungen, die sich auf den ersten Blick widersprechen. Der Code soll öffentlich bleiben, und jeder soll Knecht kostenlos selbst hosten, lesen und anpassen dürfen. Gleichzeitig soll Knecht irgendwann Geld verdienen können, damit das Projekt dauerhaft betrieben werden kann. Wie genau, ist offen. Vielleicht eine gehostete Version, vielleicht bezahlte Zusatzfunktionen für Teams.",[10,3981,3982,3983,3988,3989,3994,3995,4000,4001,4006,4007,4012],{},"Mit einer permissiven Lizenz wie ",[51,3984,3987],{"href":3985,"rel":3986},"https:\u002F\u002Fopensource.org\u002Flicense\u002Fmit",[55],"MIT"," oder Apache verträgt sich das nicht. Permissiv heißt: jeder darf alles, auch Knecht nehmen und als eigenes bezahltes Produkt anbieten. Elasticsearch hat vorgemacht, wohin das führt. Es war Apache-lizenziert, AWS verkaufte es als eigenen Managed Service, und Elastic ging dabei leer aus. 2021 ",[51,3990,3993],{"href":3991,"rel":3992},"https:\u002F\u002Fwww.elastic.co\u002Fblog\u002Flicensing-change",[55],"wechselte Elastic auf eine restriktivere Lizenz",", AWS forkte die letzte freie Version und betreibt sie seitdem als ",[51,3996,3999],{"href":3997,"rel":3998},"https:\u002F\u002Fopensearch.org\u002F",[55],"OpenSearch"," weiter. Terraform (Fork: ",[51,4002,4005],{"href":4003,"rel":4004},"https:\u002F\u002Fopentofu.org\u002F",[55],"OpenTofu",") und Redis (Fork: ",[51,4008,4011],{"href":4009,"rel":4010},"https:\u002F\u002Fvalkey.io\u002F",[55],"Valkey",") haben denselben Zyklus hinter sich.",[10,4014,4015],{},"Diese Geschichten haben eine Gemeinsamkeit: Der Lizenzwechsel kam spät, als längst eine Community auf der freien Lizenz aufgebaut hatte. Ändern lässt sich eine Lizenz nämlich nur für neue Versionen. Was einmal permissiv veröffentlicht ist, bleibt es für immer, und beim Wechsel steht die Community verständlicherweise auf der Seite des Forks. Wenn eine Grenze nötig ist, dann ehrlicherweise von Anfang an.",[19,4017,4019],{"id":4018},"die-kandidaten","Die Kandidaten",[10,4021,4022],{},"Ernsthaft angeschaut haben wir uns fünf Lizenzen:",[2084,4024,4025,4041],{},[2087,4026,4027],{},[2090,4028,4029,4032,4035,4038],{},[2093,4030,4031],{},"Lizenz",[2093,4033,4034],{},"Modell",[2093,4036,4037],{},"Verbietet",[2093,4039,4040],{},"Wird Open Source",[2102,4042,4043,4057,4070,4084,4097],{},[2090,4044,4045,4048,4051,4054],{},[2107,4046,4047],{},"MIT \u002F Apache",[2107,4049,4050],{},"Open Source, permissiv",[2107,4052,4053],{},"nichts",[2107,4055,4056],{},"ist es schon",[2090,4058,4059,4062,4065,4068],{},[2107,4060,4061],{},"AGPL",[2107,4063,4064],{},"Open Source, Copyleft",[2107,4066,4067],{},"nichts, erzwingt aber Offenlegung eigener Änderungen",[2107,4069,4056],{},[2090,4071,4072,4075,4078,4081],{},[2107,4073,4074],{},"BSL",[2107,4076,4077],{},"source available",[2107,4079,4080],{},"was der Hersteller selbst festlegt",[2107,4082,4083],{},"nach bis zu vier Jahren",[2090,4085,4086,4089,4091,4094],{},[2107,4087,4088],{},"ELv2",[2107,4090,4077],{},[2107,4092,4093],{},"das Anbieten als Managed Service",[2107,4095,4096],{},"nie",[2090,4098,4099,4102,4104,4107],{},[2107,4100,4101],{},"FSL",[2107,4103,4077],{},[2107,4105,4106],{},"Konkurrenzprodukte",[2107,4108,4109],{},"nach zwei Jahren",[10,4111,3701,4112,4116],{},[51,4113,4061],{"href":4114,"rel":4115},"https:\u002F\u002Fwww.gnu.org\u002Flicenses\u002Fagpl-3.0.html",[55]," ist die eine echte Open-Source-Lizenz, die ein Geschäftsmodell schützt. Wer AGPL-Software als Webdienst betreibt oder in ein Produkt einbaut, muss seinen eigenen Code offenlegen, und das schreckt Konkurrenten zuverlässig ab. Es schreckt aber auch die Falschen ab: Viele Firmen verbieten AGPL-Software pauschal, weil ihre Rechtsabteilung die Offenlegungspflicht nicht im Einzelfall prüfen will. Ausgerechnet Agenturen mit vorsichtigen Kunden, also die Leute, für die Knecht gebaut wird, wären davon betroffen.",[10,4118,4119,4120,4124,4125,4129,4130,4135],{},"BSL, ELv2 und FSL stehen für dasselbe Modell, es nennt sich source available. Der Code ist öffentlich und frei nutzbar, nur die kommerzielle Verwertung bleibt dem Hersteller vorbehalten. Die ",[51,4121,4074],{"href":4122,"rel":4123},"https:\u002F\u002Fmariadb.com\u002Fbsl11\u002F",[55]," (von MariaDB) verlangt, dass der Hersteller die erlaubte Nutzung selbst formuliert, deshalb liest sich jede BSL anders und muss einzeln geprüft werden. Selbst ob kostenloses Self-Hosting erlaubt ist, hängt dort vom jeweiligen Hersteller ab. Die ",[51,4126,4088],{"href":4127,"rel":4128},"https:\u002F\u002Fwww.elastic.co\u002Flicensing\u002Felastic-license",[55]," (von Elastic) ist einfach, wird aber nie zu Open Source. Die FSL (",[51,4131,4134],{"href":4132,"rel":4133},"https:\u002F\u002Fblog.sentry.io\u002Fintroducing-the-functional-source-license-freedom-without-free-riding\u002F",[55],"von Sentry",") ist die jüngste der drei: eine Seite Text, feste Regeln und eine eingebaute Umwandlung zu Open Source.",[19,4137,4139],{"id":4138},"was-die-fsl-erlaubt","Was die FSL erlaubt",[10,4141,4142],{},"Für die allermeisten ändert sich durch die Lizenz nichts, sie erlaubt genau das, was Knecht-Nutzer ohnehin tun:",[139,4144,4145,4148,4151],{},[142,4146,4147],{},"Knecht selbst hosten und produktiv einsetzen, auch kommerziell, auch für Kundenprojekte",[142,4149,4150],{},"den Code lesen, verändern und forken",[142,4152,4153],{},"Dienstleistungen rund um Knecht anbieten, etwa Setup und Betrieb für einen Kunden",[10,4155,4156,4157,4162],{},"Verboten ist eine Sache: Knecht anderen als eigenes kommerzielles Produkt anzubieten, also etwa Knecht-Hosting zu verkaufen oder Knecht unter anderem Namen zu vertreiben. Der ",[51,4158,4161],{"href":4159,"rel":4160},"https:\u002F\u002Ffsl.software\u002F",[55],"Lizenztext"," nennt das Competing Use.",[10,4164,4165,4166,4171],{},"Und dann ist da die Klausel, wegen der es am Ende die FSL geworden ist: Jede veröffentlichte Version wird zwei Jahre nach ihrem Release automatisch unter die ",[51,4167,4170],{"href":4168,"rel":4169},"https:\u002F\u002Fwww.apache.org\u002Flicenses\u002FLICENSE-2.0",[55],"Apache-2.0-Lizenz"," gestellt, also echtes Open Source. Das steht unwiderruflich im Lizenztext, wir könnten es nicht einmal zurücknehmen. Sollte Knecht eines Tages eingestellt werden, ist der komplette Code spätestens zwei Jahre später frei. Niemand baut hier auf etwas, das ihm dauerhaft weggenommen werden kann.",[19,4173,4175],{"id":4174},"kein-open-source","Kein Open Source",[10,4177,4178,4179,4184,4185,4190],{},"Im Alltag wird jedes öffentliche Repo schnell Open Source genannt. Open Source ist aber ein definierter Begriff: Die ",[51,4180,4183],{"href":4181,"rel":4182},"https:\u002F\u002Fopensource.org\u002Fosd",[55],"Definition der Open Source Initiative"," verlangt unter anderem, dass eine Lizenz kein Einsatzfeld ausschließt, auch nicht das der Konkurrenz. Genau das tut die FSL mit ihrem Competing-Use-Verbot. Knecht ist damit kein Open Source. Die passenden Begriffe sind source available oder ",[51,4186,4189],{"href":4187,"rel":4188},"https:\u002F\u002Ffair.io\u002F",[55],"Fair Source",", so heißt die Initiative, unter der Sentry und andere dieses Modell zusammengefasst haben.",[19,4192,3677],{"id":3676},[10,4194,4195,4196,4201],{},"Ein Punkt steht noch auf der Liste: Bevor der erste externe Pull Request gemergt wird, richten wir ein CLA ein, ein ",[51,4197,4200],{"href":4198,"rel":4199},"https:\u002F\u002Fde.wikipedia.org\u002Fwiki\u002FContributor_License_Agreement",[55],"Contributor License Agreement",". Das ist eine kurze Vereinbarung, mit der Contributors uns die Rechte einräumen, ihren Beitrag zusammen mit dem restlichen Code zu lizenzieren. Ohne sie würden sich die Rechte am Code auf viele Personen verteilen, und jedes spätere Lizenzmodell müsste mit jedem einzelnen Contributor neu verhandelt werden.",{"title":290,"searchDepth":291,"depth":291,"links":4203},[4204,4205,4206,4207,4208,4209],{"id":3959,"depth":291,"text":3960},{"id":3975,"depth":291,"text":3976},{"id":4018,"depth":291,"text":4019},{"id":4138,"depth":291,"text":4139},{"id":4174,"depth":291,"text":4175},{"id":3676,"depth":291,"text":3677},"2026-08-09","Knecht hat jetzt eine Lizenz, die Functional Source License. Was du mit dem Code alles darfst, warum es kein MIT geworden ist und wie jede Version trotzdem irgendwann Open Source wird.",{},"\u002Fde\u002Fupdates\u002Flicense-decision",{"title":3945,"description":4211},"de\u002Fupdates\u002Flicense-decision","XXvzO4Xv0dk5flxJ9hVJOKFFVSaoFVHa0E2PrN6AvPM",{"id":4218,"title":4219,"body":4220,"date":4368,"description":4369,"extension":310,"meta":4370,"navigation":312,"path":4371,"seo":4372,"stem":4373,"tag":4374,"__hash__":4375},"updates_de\u002Fde\u002Fupdates\u002Fweb-terminal-vscode.md","Ein Terminal und VS Code für jeden Run",{"type":7,"value":4221,"toc":4361},[4222,4229,4233,4236,4240,4244,4247,4261,4266,4270,4273,4332,4335,4339,4348,4352,4355,4358],[10,4223,4224,4225,4228],{},"Ein Run ist eine einzelne Ausführung eines ",[51,4226,4227],{"href":3198},"Workflows"," auf einem Projekt, mit eigener Umgebung auf dem Server. Bisher kam man an diese Umgebung nur über das Dashboard heran. Man konnte dem Agenten Anweisungen schicken und die Preview anschauen, aber nicht selbst hinein. Wer nur kurz ein Log lesen oder einen Befehl ausprobieren wollte, musste dafür den Agenten bemühen. Jetzt gibt es auf der Run-Seite drei direkte Wege hinein.",[19,4230,4232],{"id":4231},"das-terminal","Das Terminal",[10,4234,4235],{},"Der direkteste Weg in einen Run ist eine Shell. Es gibt sie in zwei Varianten, im Browser für jedes Dashboard-Mitglied und über SSH für alle, die ohnehin auf den Server dürfen. Das Video zeigt beide.",[2470,4237],{"caption":4238,"src":4239},"Das Terminal im Browser und der fertige SSH-Befehl","\u002Fassets\u002Fknecht-ssh-showcase.webm",[79,4241,4243],{"id":4242},"im-browser","Im Browser",[10,4245,4246],{},"Ein Klick auf \"Terminal\" öffnet eine Shell im Container des Runs. Sie geht als Fenster direkt auf der Run-Seite auf, man tippt also im selben Browser-Tab, in dem man den Run gerade anschaut. Ein Terminal-Programm oder ein SSH-Client auf dem eigenen Rechner ist dafür nicht nötig.",[139,4248,4249,4252,4255,4258],{},[142,4250,4251],{},"Funktioniert für jedes Dashboard-Mitglied, ganz ohne Zugang zum Server.",[142,4253,4254],{},"Die Shell läuft im selben Container wie der Agent. Composer, npm und Git sind da und verhalten sich wie im echten Projekt.",[142,4256,4257],{},"Es geht kein zusätzlicher Port auf, die Verbindung läuft über dieselbe HTTPS-Verbindung wie das Dashboard.",[142,4259,4260],{},"Läuft ein Run mit mehreren Containern, etwa einer eigenen Datenbank, hat das Terminal einen Tab pro Service.",[325,4262,4263],{},[10,4264,4265],{},"Knecht stoppt die Umgebung eines Runs automatisch, wenn sie eine Weile niemand benutzt, damit sie keinen RAM belegt. Arbeit im Terminal zählt dabei als Benutzung, die Umgebung bleibt also an, solange man darin tippt.",[79,4267,4269],{"id":4268},"über-ssh","Über SSH",[10,4271,4272],{},"Wer lieber im eigenen Terminal arbeitet, findet im Terminal-Fenster einen fertigen SSH-Befehl zum Kopieren. Auf der eigenen Maschine eingefügt, landet er in genau demselben Container.",[390,4274,4278],{"className":4275,"code":4276,"language":4277,"meta":290,"style":290},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","ssh -t knecht@my-server.com docker exec -it -u 1000:1000 -w \u002Fvar\u002Fwww\u002Fhtml -e HOME=\u002Fhome\u002Fnode -e USER=node ddev-knecht-run-4-web bash -l\n","bash",[14,4279,4280],{"__ignoreMap":290},[398,4281,4282,4285,4288,4291,4294,4297,4300,4303,4306,4309,4312,4315,4318,4320,4323,4326,4329],{"class":400,"line":401},[398,4283,4284],{"class":493},"ssh",[398,4286,4287],{"class":419}," -t",[398,4289,4290],{"class":419}," knecht@my-server.com",[398,4292,4293],{"class":419}," docker",[398,4295,4296],{"class":419}," exec",[398,4298,4299],{"class":419}," -it",[398,4301,4302],{"class":419}," -u",[398,4304,4305],{"class":419}," 1000:1000",[398,4307,4308],{"class":419}," -w",[398,4310,4311],{"class":419}," \u002Fvar\u002Fwww\u002Fhtml",[398,4313,4314],{"class":419}," -e",[398,4316,4317],{"class":419}," HOME=\u002Fhome\u002Fnode",[398,4319,4314],{"class":419},[398,4321,4322],{"class":419}," USER=node",[398,4324,4325],{"class":419}," ddev-knecht-run-4-web",[398,4327,4328],{"class":419}," bash",[398,4330,4331],{"class":419}," -l\n",[10,4333,4334],{},"Knecht betreibt dafür keinen eigenen SSH-Server und verwaltet keine Keys. Der Befehl nutzt den SSH-Zugang zum Server, den es sowieso schon gibt. In den Einstellungen wird dafür einmal die SSH-Adresse des Servers hinterlegt, mehr nicht. Dieser Weg ist damit denen vorbehalten, die ohnehin auf den Server dürfen. Für alle anderen bleibt das Web-Terminal.",[19,4336,4338],{"id":4337},"vs-code-im-browser","VS Code im Browser",[10,4340,4341,4342,4347],{},"VS Code läuft direkt im Run. Ein Klick auf \"Open in VS Code\" auf der Run-Seite öffnet die IDE als neuen Browser-Tab. Dahinter steckt ",[51,4343,4346],{"href":4344,"rel":4345},"https:\u002F\u002Fgithub.com\u002Fgitpod-io\u002Fopenvscode-server",[55],"openvscode-server",", der Open-Source-Build von VS Code für den Browser.",[2470,4349],{"caption":4350,"src":4351},"VS Code öffnet den Run im Browser","\u002Fassets\u002Fknecht-vs-code.webm",[10,4353,4354],{},"Die IDE arbeitet dabei direkt am Projekt im Run, nicht an einer Kopie. Eine gespeicherte Änderung liegt sofort im laufenden Projekt, es ist derselbe Code, den auch die Preview und der Agent sehen. Und weil jeder Run ein vollwertiger Git-Clone ist, funktionieren Commits und Branches in IDE und Terminal ganz normal.",[10,4356,4357],{},"Ein lokales Setup braucht es nicht. Die IDE läuft für jedes Dashboard-Mitglied im Browser und ist wie die Previews nur mit Knecht-Login erreichbar.",[2010,4359,4360],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":290,"searchDepth":291,"depth":291,"links":4362},[4363,4367],{"id":4231,"depth":291,"text":4232,"children":4364},[4365,4366],{"id":4242,"depth":297,"text":4243},{"id":4268,"depth":297,"text":4269},{"id":4337,"depth":291,"text":4338},"2026-07-27","Ein Terminal im Browser, ein fertiger SSH-Befehl und VS Code ohne lokales Setup. Jeder Run lässt sich jetzt direkt öffnen und weiterbearbeiten.",{},"\u002Fde\u002Fupdates\u002Fweb-terminal-vscode",{"title":4219,"description":4369},"de\u002Fupdates\u002Fweb-terminal-vscode","Dashboard","392Cs3B2Y1SGURko6T-l_Yy6zvVVhp7tgiYM1oO_sPw",{"id":4377,"title":4378,"body":4379,"date":4661,"description":4662,"extension":310,"meta":4663,"navigation":312,"path":4664,"seo":4665,"stem":4666,"tag":2039,"__hash__":4667},"updates_de\u002Fde\u002Fupdates\u002Fsandbox-rollback.md","Zurück zum geteilten Daemon, eine Preview für 180 MB",{"type":7,"value":4380,"toc":4650},[4381,4391,4406,4410,4424,4432,4435,4439,4442,4462,4465,4469,4473,4476,4489,4493,4502,4517,4520,4524,4539,4548,4552,4555,4626,4629,4642,4645,4647],[325,4382,4383],{},[10,4384,4385,4386,4390],{},"Dieser Post ist die Fortsetzung von ",[51,4387,4389],{"href":4388},"\u002Fupdates\u002Fdashboard-architecture","Vom geteilten Docker Daemon zur Sandbox pro Run",". Dort steht, warum wir uns für eine eigene Sandbox pro Run entschieden haben. Hier steht, warum wir sie wieder ausgebaut haben.",[10,4392,4393,4394,4399,4400,4405],{},"Der letzte Architektur-Post endete mit einem klaren Plan. Jeder Run bekommt seine eigene ",[51,4395,4398],{"href":4396,"rel":4397},"https:\u002F\u002Fgithub.com\u002Fnestybox\u002Fsysbox",[55],"Sysbox","-Sandbox mit eigenem ",[51,4401,4404],{"href":4402,"rel":4403},"https:\u002F\u002Fdocs.docker.com\u002Fget-started\u002Fdocker-overview\u002F#docker-architecture",[55],"Docker Daemon",". Wir haben das gebaut, und es lief. Trotzdem haben wir die Sandbox wieder ausgebaut, denn zwei Probleme, die wir bewusst aufgeschoben hatten, ließen sich nicht mehr aufschieben.",[19,4407,4409],{"id":4408},"die-rechnung","Die Rechnung",[10,4411,4412,4413,4418,4419,4423],{},"Eine Sysbox-Sandbox ist im Grunde ein kleines Betriebssystem mit ",[51,4414,4417],{"href":4415,"rel":4416},"https:\u002F\u002Fsystemd.io",[55],"systemd",", eigenem Docker Daemon und darüber dem kompletten ",[51,4420,2063],{"href":4421,"rel":4422},"https:\u002F\u002Fddev.com",[55],"-Stack. Dass das Ressourcen kostet, war uns von Anfang an klar. Wir dachten nur, wir bekommen das später in den Griff, dem war aber leider nicht so.",[139,4425,4426,4429],{},[142,4427,4428],{},"Eine laufende Preview kostete gemessen rund 3 GB RAM. Bei 20 parallelen Previews wären das 60 GB, nur für Sandboxes.",[142,4430,4431],{},"Jede Sandbox braucht alle DDEV-Images einmal für sich, das sind 1-2 GB Disk pro Preview. Ein Registry-Cache beschleunigte die Downloads, am Platz änderte er nichts.",[10,4433,4434],{},"Das Ziel von Knecht ist aber, dass eine Agentur alle ihre Projekte auf einem eigenen, bezahlbaren Server laufen lässt. Dafür ist eine Architektur mit 3 GB pro Preview das falsche Fundament, egal wie sauber sie isoliert.",[19,4436,4438],{"id":4437},"was-die-sandbox-löste","Was die Sandbox löste",[10,4440,4441],{},"Zur Erinnerung, der letzte Post hatte zwei Gründe für die Sandbox.",[139,4443,4444,4453],{},[142,4445,4446,4447,4452],{},"Auf dem geteilten Daemon gab es keine Isolation zwischen Runs. Die festen Ports des ",[51,4448,4451],{"href":4449,"rel":4450},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fusage\u002Farchitecture\u002F",[55],"DDEV-Routers"," kollidierten, deshalb mussten wir ihn weglassen und in die DDEV-Config eingreifen.",[142,4454,4455,4456,4461],{},"Der Agent führt fremden Code aus, und wer den ",[51,4457,4460],{"href":4458,"rel":4459},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fsecurity\u002F#docker-daemon-attack-surface",[55],"Docker-Socket"," des Hosts erreicht, kontrolliert den ganzen Server.",[10,4463,4464],{},"Die Sandbox löste beides mit einem Schlag. Aber sie war die gründlichste Lösung, nicht die einzige. Der Rückbau war erst möglich, als wir beide Probleme noch einmal einzeln angeschaut haben.",[19,4466,4468],{"id":4467},"die-neuen-lösungen","Die neuen Lösungen",[79,4470,4472],{"id":4471},"kein-socket-im-run","Kein Socket im Run",[10,4474,4475],{},"Das Problem ist der Docker-Socket. Der Agent führt fremden Code aus, und sobald der Socket in seiner Umgebung liegt, kontrolliert ein gekaperter Agent den ganzen Server. Die Sandbox hat deshalb jedem Run einen eigenen Daemon gegeben, damit der Socket des Hosts außer Reichweite bleibt.",[10,4477,4478,4479,4482,4483,4488],{},"Unsere Lösung setzt früher an, der Run bekommt überhaupt keinen Docker-Zugriff mehr. Knecht fährt DDEV host-seitig hoch. Der Agent und alle Projekt-Befehle wie ",[14,4480,4481],{},"composer install"," laufen ausschließlich im Web-Container des Projekts. Ein Agent, der per ",[51,4484,4487],{"href":4485,"rel":4486},"https:\u002F\u002Fsimonwillison.net\u002Fseries\u002Fprompt-injection\u002F",[55],"Prompt Injection"," gekapert wird, steht damit in einem ganz normalen Container mit eigenem Netz und begrenzten Ressourcen, und er findet dort weder Socket noch Daemon vor.",[79,4490,4492],{"id":4491},"umbenennen-statt-einsperren","Umbenennen statt einsperren",[10,4494,4495,4496,4501],{},"Die Port-Kollisionen kamen allein vom Router, dem Reverse Proxy, den DDEV vor alle Projekte stellt und der die Ports 80 und 443 auf dem Host bindet. Der bleibt weiter weg. Stattdessen spricht der Preview-Proxy den Web-Container jedes Runs direkt über das ",[51,4497,4500],{"href":4498,"rel":4499},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fnetwork\u002F",[55],"Docker-Netzwerk"," an.",[10,4503,4504,4505,4510,4511,4516],{},"Eine Sache muss der Proxy dabei mitliefern, den Hostnamen. Projekte mit mehreren Domains, etwa ein ",[51,4506,4509],{"href":4507,"rel":4508},"https:\u002F\u002Fcraftcms.com\u002Fdocs\u002F5.x\u002Fsystem\u002Fsites.html",[55],"Craft-Multisite",", entscheiden anhand der aufgerufenen Domain, welche Site sie ausliefern. Die Preview läuft aber unter einer Knecht-URL, nicht unter der echten Domain des Projekts. Deshalb schickt der Proxy die echte Domain im ",[51,4512,4515],{"href":4513,"rel":4514},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FReference\u002FHeaders\u002FHost",[55],"Host-Header"," mit, und für das Projekt sieht jede Anfrage so aus, als wäre sie über die normale URL gekommen.",[10,4518,4519],{},"Damit parallele Runs desselben Projekts sich nicht in die Quere kommen, registriert Knecht jeden Run als eigenes DDEV-Projekt mit eindeutigem Namen. Der Eingriff in die DDEV-Config, im letzten Post noch ein Minuspunkt, ist geblieben, aber er ist für das Projekt unsichtbar. Der Code im Repo bleibt unangetastet, umgeschrieben wird nur die generierte Laufzeit-Config.",[19,4521,4523],{"id":4522},"der-tradeoff","Der Tradeoff",[10,4525,4526,4527,4532,4533,4538],{},"Sicherheitstechnisch ist das ein Rückschritt gegenüber Sysbox. Sysbox mappte Root im Container über ",[51,4528,4531],{"href":4529,"rel":4530},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fuser_namespaces.7.html",[55],"User Namespaces"," auf einen unprivilegierten Host-User. Jetzt laufen die Container wieder mit ",[51,4534,4537],{"href":4535,"rel":4536},"https:\u002F\u002Fgithub.com\u002Fopencontainers\u002Frunc",[55],"runc",", der normalen Docker-Runtime, die Grenze zum Host ist dünner. Aufgefangen wird das durch genau die Punkte von oben, denn der Run hat keinen Socket, ein eigenes Netz und feste Limits.",[10,4540,4541,4542,4547],{},"Ob dieser Rückschritt vertretbar ist, entscheidet wie schon im letzten Post das ",[51,4543,4546],{"href":4544,"rel":4545},"https:\u002F\u002Fowasp.org\u002Fwww-community\u002FThreat_Modeling",[55],"Threat Model",". Knecht läuft bei einer Agentur, für ihre eigenen Projekte, auf ihrem eigenen Server. Der Angreifer, vor dem die Sandbox zusätzlich geschützt hätte, braucht einen Kernel-Exploit und landet dann auf einem Server, auf dem ohnehin nur die eigenen Projekte liegen. Für dieses Risiko 3 GB statt 180 MB pro Preview zu bezahlen, ist der falsche Tausch.",[19,4549,4551],{"id":4550},"das-ergebnis","Das Ergebnis",[10,4553,4554],{},"Beide Varianten liefen auf derselben Dev-Maschine, gemessen im Juli 2026:",[2084,4556,4557,4569],{},[2087,4558,4559],{},[2090,4560,4561,4563,4566],{},[2093,4562],{},[2093,4564,4565],{},"Sandbox pro Run (Sysbox)",[2093,4567,4568],{},"DDEV auf dem Host (heute)",[2102,4570,4571,4582,4593,4604,4615],{},[2090,4572,4573,4576,4579],{},[2107,4574,4575],{},"RAM pro laufender Preview",[2107,4577,4578],{},"~3 GB",[2107,4580,4581],{},"~180 MB",[2090,4583,4584,4587,4590],{},[2107,4585,4586],{},"DDEV-Images",[2107,4588,4589],{},"pro Sandbox, 1-2 GB extra",[2107,4591,4592],{},"einmal pro Host",[2090,4594,4595,4598,4601],{},[2107,4596,4597],{},"Gestoppte Preview wecken",[2107,4599,4600],{},"Minuten, kompletter Sandbox-Boot",[2107,4602,4603],{},"Sekunden",[2090,4605,4606,4609,4612],{},[2107,4607,4608],{},"Docker-Zugriff im Run",[2107,4610,4611],{},"eigener Daemon in der Sandbox",[2107,4613,4614],{},"keiner, Knecht steuert von außen",[2090,4616,4617,4620,4623],{},[2107,4618,4619],{},"Isolationsgrenze",[2107,4621,4622],{},"eigener Daemon plus User Namespaces",[2107,4624,4625],{},"eigenes Netz, Limits, kein Socket",[10,4627,4628],{},"Dazu kommen noch zwei kleinere Optimierungen.",[139,4630,4631,4639],{},[142,4632,4633,4634,70],{},"Die Datenbank läuft für die Preview mit einer sparsameren Konfiguration. Die MySQL-Config, die DDEV mitliefert, erlaubt jeder Datenbank rund 1 GB ",[51,4635,4638],{"href":4636,"rel":4637},"https:\u002F\u002Fmariadb.com\u002Fkb\u002Fen\u002Finnodb-buffer-pool\u002F",[55],"Puffer",[142,4640,4641],{},"Eine gestoppte Preview kostet 0 RAM, übrig bleiben nur Datenbank-Volume und Code.",[10,4643,4644],{},"Aus \"ein paar Previews pro Server\" wurde damit \"praktisch alle Projekte einer Agentur gleichzeitig\".",[19,4646,1986],{"id":1985},[10,4648,4649],{},"Der letzte Post begann mit dem Satz, dass wir die riskanteste Annahme zuerst testen. Das war richtig, und es hat funktioniert, die Sandbox lief. Nur war \"läuft es?\" die falsche letzte Frage. Die vollständige lautet \"läuft es zu Kosten, die zum Ziel passen?\", und die lässt sich erst beantworten, wenn das System unter echter, paralleler Last steht. Diese Messung machen wir jetzt früher.",{"title":290,"searchDepth":291,"depth":291,"links":4651},[4652,4653,4654,4658,4659,4660],{"id":4408,"depth":291,"text":4409},{"id":4437,"depth":291,"text":4438},{"id":4467,"depth":291,"text":4468,"children":4655},[4656,4657],{"id":4471,"depth":297,"text":4472},{"id":4491,"depth":297,"text":4492},{"id":4522,"depth":291,"text":4523},{"id":4550,"depth":291,"text":4551},{"id":1985,"depth":291,"text":1986},"2026-07-26","Die Sandbox pro Run hat funktioniert und kostete 3 GB RAM pro Preview. Warum wir sie wieder ausgebaut haben und wie die alten Probleme heute gelöst sind.",{},"\u002Fde\u002Fupdates\u002Fsandbox-rollback",{"title":4378,"description":4662},"de\u002Fupdates\u002Fsandbox-rollback","LWPVAoQhFTWNjGiNf8fcbUayr1LxUHQIKoYM2Y7JcSI",{"id":4669,"title":4670,"body":4671,"date":4867,"description":4868,"extension":310,"meta":4869,"navigation":312,"path":4870,"seo":4871,"stem":4872,"tag":2936,"__hash__":4873},"updates_de\u002Fde\u002Fupdates\u002Fworkflow-engine.md","DDEV-Projekte automatisieren mit Workflows",{"type":7,"value":4672,"toc":4856},[4673,4676,4682,4686,4703,4706,4712,4719,4723,4726,4751,4758,4762,4769,4775,4782,4786,4789,4795,4799,4802,4822,4825,4829,4832,4838,4841,4845,4848,4851,4853],[10,4674,4675],{},"Ein Workflow ist eine Liste von Schritten, die Knecht automatisch für ein Projekt ausführt. Zum Beispiel Projekt starten, Composer-Pakete updaten, Tests laufen lassen und einen Pull Request öffnen. Die letzten Wochen sind fast komplett in diese Engine geflossen. Hier steht, was sie kann, was die AI-Action von einem Chat unterscheidet und was noch fehlt.",[10,4677,4678],{},[2447,4679],{"alt":4680,"src":4681},"Der Workflow-Editor mit Trigger, Schritten und der Action-Bibliothek","\u002Fassets\u002Fworkflow-detail.png",[19,4683,4685],{"id":4684},"wie-ein-workflow-läuft","Wie ein Workflow läuft",[10,4687,4688,4689,4692,4693,4695,4696,4699,4700,70],{},"Wird ein Workflow gestartet, entsteht ein ",[59,4690,4691],{},"Run",", eine einzelne Ausführung auf einem Projekt. Jeder Run bekommt eine eigene, abgeschottete Umgebung, die ",[59,4694,3707],{},". Darin läuft das Projekt komplett, der Code frisch von GitHub, die Datenbank eingespielt, ",[51,4697,2063],{"href":4421,"rel":4698},[55]," gestartet. Das Live-System und andere Runs bekommen davon nichts mit. Warum jeder Run seine eigene Sandbox bekommt, steht im ",[51,4701,4702],{"href":4388},"Architektur-Post",[10,4704,4705],{},"Die Schritte laufen nacheinander, und jeder hinterlässt Ergebnisse, die spätere Schritte über Platzhalter einsetzen.",[390,4707,4710],{"className":4708,"code":4709,"language":2781},[2779],"{{ steps.run_tests.stdout }}      die Ausgabe des Schritts \"Run tests\"\n{{ inputs.title }}                der Titel des Tickets, das den Workflow gestartet hat\n",[14,4711,4709],{"__ignoreMap":290},[10,4713,4714,4715,4718],{},"Auswendig wissen muss man das nicht. Wer ",[14,4716,4717],{},"{{ "," tippt, bekommt eine Liste mit allem, was an dieser Stelle verfügbar ist. Und nach jedem Schritt speichert Knecht, was passiert ist. Deshalb sieht man live, wo ein Run steht, und ein fehlgeschlagener Run lässt sich genau ab dem Schritt fortsetzen, an dem er hängen blieb.",[19,4720,4722],{"id":4721},"die-actions","Die Actions",[10,4724,4725],{},"Jeder Schritt führt eine Action aus. Grob gibt es drei Sorten.",[4727,4728,4729,4740,4746],"field-group",{},[4730,4731,4733],"field",{"name":4732},"Deterministische Actions",[10,4734,4735,4736,4739],{},"Erledigen genau eine Sache, immer gleich. Sie booten das Projekt, führen einen Shell-Befehl wie ",[14,4737,4738],{},"composer update"," aus, lassen eigenen JavaScript-Code laufen, rufen eine externe URL auf, prüfen die ganze Sitemap auf kaputte Links oder bringen die Änderungen als Pull Request zurück nach GitHub.",[4730,4741,4743],{"name":4742},"Kontrollfluss-Actions",[10,4744,4745],{},"If\u002Felse führt Schritte nur unter einer Bedingung aus, zum Beispiel \"wenn die Tests fehlgeschlagen sind\". Loop wiederholt Schritte für jeden Eintrag einer Liste.",[4730,4747,4748],{"name":3199},[10,4749,4750],{},"Lässt einen Agenten im laufenden Projekt arbeiten, dazu gleich mehr.",[10,4752,4753,4754,4757],{},"Weil nicht jeder Fehler ein echter Fehler ist, hat jeder Schritt seine eigene Fehler-Policy. Wackelige Befehle bekommen Retries mit Backoff, und mit ",[14,4755,4756],{},"continueOnError"," läuft der Run trotz Fehler weiter. Die Bibliothek wächst gerade noch, einen teil der aktuellen Actions können im oberen Screenshot angesehen werden.",[79,4759,4761],{"id":4760},"ein-agent-kein-chat","Ein Agent, kein Chat",[10,4763,4764,4765,4768],{},"Die AI-Action startet ",[51,4766,3205],{"href":3203,"rel":4767},[55]," mit eurem eigenen API-Key direkt in der Sandbox. Der Agent arbeitet im echten, laufenden Projekt, er liest Code, ändert Dateien und führt Befehle aus. Ein Chat sagt dir, wie ein Fix aussehen könnte. Der Agent wendet ihn an und probiert aus, ob er funktioniert.",[10,4770,4771],{},[2447,4772],{"alt":4773,"src":4774},"Die AI-Action mit Prompt, festem Output-Format und den verfügbaren Variablen","\u002Fassets\u002Fworkflow-ai-action.png",[10,4776,4777,4778,4781],{},"Statt Fließtext kann der Agent feste Output-Felder wie ",[14,4779,4780],{},"prTitle"," liefern, als geprüftes JSON für spätere Schritte. Und er ist nach dem Run nicht weg. Ein Chat auf der Run-Seite setzt dieselbe Session fort, \"Mach den Button noch blau\" braucht keinen neuen Run.",[79,4783,4785],{"id":4784},"sicher-eingesperrt","Sicher eingesperrt",[10,4787,4788],{},"Alles, was ein Workflow ausführt (Shell-Befehle, eigener Code, der Agent), läuft ausschließlich in der Sandbox, nie auf dem Server selbst.",[10,4790,4791,4792,4794],{},"Auch Projekt-Secrets bleiben, wo sie hingehören. API-Keys und Tokens liegen ",[51,4793,3420],{"href":3419}," in der Datenbank. Die Preview-URLs sind ebenfalls nicht öffentlich, denn wer eine öffnet, braucht einen Knecht-Login.",[19,4796,4798],{"id":4797},"die-trigger","Die Trigger",[10,4800,4801],{},"Ein Trigger legt fest, wann ein Workflow von selbst startet. Das ist der Punkt, an dem aus einem Werkzeug Automatisierung wird, denn niemand muss mehr daran denken, den Ablauf anzustoßen. Der Workflow läuft, sobald seine Bedingung eintritt, auch nachts und am Wochenende. Aktuell gibt es drei Arten.",[4727,4803,4804,4810,4816],{},[4730,4805,4807],{"name":4806},"Manuell",[10,4808,4809],{},"Per Klick im Dashboard oder als Test-Run direkt im Editor.",[4730,4811,4813],{"name":4812},"Integrationen",[10,4814,4815],{},"Knecht reagiert auf Events aus GitHub und Jira. Bekommt ein Jira-Ticket zum Beispiel das Label \"knecht\", startet der Workflow, und der fertige PR-Link landet als Kommentar auf dem Ticket.",[4730,4817,4819],{"name":4818},"Cron",[10,4820,4821],{},"Nach Zeitplan, zum Beispiel jeden Montag um 6 Uhr die Security-Updates prüfen.",[10,4823,4824],{},"Ein Trigger kann mehrere Projekte anhängen und startet für jedes einen eigenen Run.",[19,4826,4828],{"id":4827},"nach-dem-start","Nach dem Start",[10,4830,4831],{},"Bevor ein Trigger scharf geschaltet wird, lässt sich der Workflow direkt im Editor testen, gegen ein echtes Projekt in einer eigenen Sandbox. Für Test-Runs gilt dabei dasselbe wie für echte.",[10,4833,4834],{},[2447,4835],{"alt":4836,"src":4837},"Ein Test-Run direkt im Editor, mit Live-Log und laufendem Schritt","\u002Fassets\u002Fworkflow-test-run.png",[10,4839,4840],{},"Innerhalb eines Runs läuft alles bewusst nacheinander, parallele Zweige gibt es nicht. Ein Run bleibt eine lineare, nachvollziehbare Kette. Wie viele Runs gleichzeitig laufen, ist konfigurierbar, alles darüber wartet in der Queue.",[19,4842,4844],{"id":4843},"workflows-als-code","Workflows als Code",[10,4846,4847],{},"Jeder Workflow lässt sich als YAML oder JSON exportieren und wieder importieren. Damit wandert ein Workflow ins Git-Repo, bekommt Reviews wie jeder andere Code und lässt sich zwischen Projekten oder Instanzen kopieren. Wer lieber im Texteditor arbeitet, schreibt die Datei direkt und importiert sie fertig.",[10,4849,4850],{},"Das Format öffnet auch die Tür für später. Ein Workflow, der sich bei einer Agentur bewährt hat, funktioniert bei der nächsten genauso, weil er nur aus Schritten und Platzhaltern besteht. Denkbar ist ein Workflow-Store, aus dem man sich fertige Abläufe wie Security-Updates oder Link-Checks direkt installiert, statt sie selbst zusammenzuklicken.",[19,4852,3923],{"id":3922},[10,4854,4855],{},"Ein Benachrichtigungssystem gibt es noch nicht. Schlägt ein Run fehl, sieht man das bisher nur im Dashboard. Wer es sofort wissen will, ruft mit dem HTTP-Schritt am Ende des Workflows einen Slack-Webhook auf. Auch ein hartes Zeitlimit pro Run fehlt noch, ein hängender Schritt läuft, bis man ihn abbricht.",{"title":290,"searchDepth":291,"depth":291,"links":4857},[4858,4859,4863,4864,4865,4866],{"id":4684,"depth":291,"text":4685},{"id":4721,"depth":291,"text":4722,"children":4860},[4861,4862],{"id":4760,"depth":297,"text":4761},{"id":4784,"depth":297,"text":4785},{"id":4797,"depth":291,"text":4798},{"id":4827,"depth":291,"text":4828},{"id":4843,"depth":291,"text":4844},{"id":3922,"depth":291,"text":3923},"2026-07-13","Automatisierung für DDEV-Projekte. Knecht führt Composer-Updates und Tests in einer Sandbox aus und öffnet Pull Requests. Mit AI-Agent, der Bugs im Projekt fixt.",{},"\u002Fde\u002Fupdates\u002Fworkflow-engine",{"title":4670,"description":4868},"de\u002Fupdates\u002Fworkflow-engine","xauOb5Gcp3AGIN0hv_UKFCYMKE6Rswg56TikkhVPfak",{"id":4875,"title":4876,"body":4877,"date":5115,"description":5116,"extension":310,"meta":5117,"navigation":312,"path":5118,"seo":5119,"stem":5120,"tag":5121,"__hash__":5122},"updates_de\u002Fde\u002Fupdates\u002Fgh-auth-evolution.md","GitHub Auth Evolution",{"type":7,"value":4878,"toc":5107},[4879,4882,4885,4889,4910,4913,4917,4923,4940,4944,4957,4986,4992,5002,5006,5009,5021,5030,5033,5036,5040,5051,5059,5074,5079,5086,5089,5102,5104],[10,4880,4881],{},"Der GitHub-Login funktionierte vom ersten Tag an. Trotzdem haben wir ihn dreimal umgebaut.",[10,4883,4884],{},"Der Grund: Hinter \"mit GitHub verbinden\" stecken drei getrennte Fragen, die gerne vermischt werden. Wer sitzt vor dem Dashboard? Womit klont der Server Repos und öffnet PRs? Und wer darf diese Instanz überhaupt benutzen? Am Anfang haben wir alle drei mit einem einzigen Token beantwortet. Hier der Weg von dort bis zum heutigen Stand, inklusive der GitHub-Eigenheiten, über die wir dabei gestolpert sind.",[19,4886,4888],{"id":4887},"was-ist-eigentlich-eine-oauth-app","Was ist eigentlich eine OAuth App?",[10,4890,4891,4892,4897,4898,4901,4902,4905,4906,4909],{},"Eine OAuth App ist der klassische \"Sign in with GitHub\"-Button. Der User wird zu GitHub geschickt, bestätigt dort den Zugriff und kommt mit einem Access Token zurück. Was dieser Token darf, bestimmen die ",[51,4893,4896],{"href":4894,"rel":4895},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Foauth-apps\u002Fbuilding-oauth-apps\u002Fscopes-for-oauth-apps",[55],"Scopes",", die die App anfragt. ",[14,4899,4900],{},"read:user"," heißt: nur das Profil lesen. ",[14,4903,4904],{},"repo"," heißt: voller Lese- und Schreibzugriff auf ",[59,4907,4908],{},"alle"," Repos, die der User erreichen kann.",[10,4911,4912],{},"Und genau da fing unser Problem an.",[19,4914,4916],{"id":4915},"stufe-1-ein-token-für-alles","Stufe 1: Ein Token für alles",[10,4918,4919,4920,4922],{},"Die erste Version von Knecht war eine OAuth App mit ",[14,4921,4904],{},"-Scope. Der Token, der den Login bestätigt hat, war gleichzeitig das Credential für alle Git-Operationen. Kein zweites Secret, nichts einzurichten. Praktisch, aber zweifach falsch:",[139,4924,4925,4934],{},[142,4926,4927,4930,4931,4933],{},[59,4928,4929],{},"Der Scope ist zu grob."," Ein ",[14,4932,4904],{},"-Token kann alles, was der User kann. In jedem seiner Repos, nicht nur in den Projekten, die Knecht betreut. Feiner geht es bei OAuth-Scopes nicht.",[142,4935,4936,4939],{},[59,4937,4938],{},"Das Credential hängt an einer Person."," Rotiert der User seinen Token oder verlässt die Agentur, kommt der Server an kein Repo mehr. Server-Infrastruktur, die auf dem Account eines Mitarbeiters steht, ist eine tickende Uhr.",[19,4941,4943],{"id":4942},"was-eine-github-app-anders-macht","Was eine GitHub App anders macht",[10,4945,4946,4947,4950,4951,4956],{},"Für Stufe 2 haben wir den Repo-Zugriff auf eine ",[59,4948,4949],{},"GitHub App"," umgestellt. Der ",[51,4952,4955],{"href":4953,"rel":4954},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Foauth-apps\u002Fbuilding-oauth-apps\u002Fdifferences-between-github-apps-and-oauth-apps",[55],"Unterschied zur OAuth App"," ist grundlegend:",[139,4958,4959,4966,4973],{},[142,4960,4961,4962,4965],{},"Eine GitHub App ist ein ",[59,4963,4964],{},"eigener Akteur"," mit eigener Identität, nicht ein User, der seine Rechte verleiht.",[142,4967,4968,4969,4972],{},"Sie wird auf ausgewählten Repos ",[59,4970,4971],{},"installiert",". Der Zugriff ist auf genau diese Repos begrenzt.",[142,4974,4975,4976,4979,4980,4985],{},"Sie authentifiziert sich mit ihrem ",[59,4977,4978],{},"Private Key"," und erzeugt daraus ",[51,4981,4984],{"href":4982,"rel":4983},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Fcreating-github-apps\u002Fauthenticating-with-a-github-app\u002Fgenerating-an-installation-access-token-for-a-github-app",[55],"Installation Tokens",": gültig für etwa eine Stunde, gescoped auf die installierten Repos. Läuft einer ab, wird einfach der nächste erzeugt.",[10,4987,4988,4989,4991],{},"Der Login blieb OAuth, aber mit geschrumpftem Scope: ",[14,4990,4900],{},". Der Token wird einmal benutzt, um das Profil zu laden, und danach verworfen. Er wird nie gespeichert. Identität und Repo-Zugriff waren damit sauber getrennt.",[10,4993,4994,4995,4997,4998,5001],{},"Nur das Setup war eine Zumutung. Der Betreiber musste eine OAuth App ",[59,4996,61],{}," eine GitHub App von Hand anlegen, vier Secrets in die ",[14,4999,5000],{},".env"," kopieren, den Private Key base64-encoden und Callback-URLs eintragen. Bevor irgendetwas lief.",[19,5003,5005],{"id":5004},"stufe-3-der-manifest-flow","Stufe 3: Der Manifest Flow",[10,5007,5008],{},"Dann fiel uns auf: Eine GitHub App bringt ihren eigenen OAuth-Client schon mit. Eine einzige App kann also beides, Login und Repo-Zugriff. Die separate OAuth App war überflüssig.",[10,5010,5011,5012,5017,5018,5020],{},"Und GitHub hat für genau diesen Fall den ",[51,5013,5016],{"href":5014,"rel":5015},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Fsharing-github-apps\u002Fregistering-a-github-app-from-a-manifest",[55],"App Manifest Flow",": Eine Software beschreibt die gewünschte App als JSON (Name, Callback-URLs, Permissions), schickt das an GitHub, GitHub legt die App an und gibt ",[59,5019,4908],{}," Credentials zurück.",[10,5022,5023,5024,5029],{},"Bei Knecht sieht das so aus: Beim ersten Start zeigt das Dashboard einen Button. Ein Klick, GitHub erstellt die App, Knecht speichert die Credentials verschlüsselt in der eigenen Datenbank. AES-256-GCM, der Schlüssel wird per ",[51,5025,5028],{"href":5026,"rel":5027},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc5869",[55],"HKDF"," aus dem Session-Passwort abgeleitet, das ohnehin gesetzt sein muss. Die Verschlüsselung kostet also keine neue Env-Variable.",[10,5031,5032],{},"Übrig bleiben genau zwei Dinge, die der Betreiber liefert: das Session-Passwort und die Base URL.",[10,5034,5035],{},"Ein Trade-off gehört dazu: Wer das Session-Passwort rotiert, macht die gespeicherten Credentials unlesbar. Die Lösung ist dann nicht Entschlüsseln, sondern das Setup einmal neu durchlaufen.",[19,5037,5039],{"id":5038},"stufe-4-wer-darf-überhaupt-rein","Stufe 4: Wer darf überhaupt rein?",[10,5041,5042,5043,5046,5047,5050],{},"Ein Loch blieb bis zuletzt offen: Der Login prüfte ",[59,5044,5045],{},"Identität",", aber keine ",[59,5048,5049],{},"Erlaubnis",". Jeder GitHub-Account, der den OAuth-Flow abschließt, bekam eine Session.",[10,5052,5053,5054,505],{},"Ob das ausnutzbar ist, hängt an einer GitHub-Eigenheit, die man einmal verstanden haben muss. Eine App ist entweder ",[51,5055,5058],{"href":5056,"rel":5057},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Fcreating-github-apps\u002Fsetting-up-a-github-app\u002Fmaking-a-github-app-public-or-private",[55],"privat oder öffentlich",[139,5060,5061,5068],{},[142,5062,5063,5064,5067],{},"Eine ",[59,5065,5066],{},"private"," GitHub App zeigt die Authorize-Seite nur ihrem Besitzer (oder Mitgliedern der besitzenden Org). Alle anderen sehen einen 404. Das wirkt wie eingebaute Zugriffskontrolle, macht aber das Einladen externer Teammitglieder unmöglich. Sie kommen nicht einmal bis zur Zustimmungsseite.",[142,5069,5063,5070,5073],{},[59,5071,5072],{},"öffentliche"," App lässt jeden GitHub-User durch den Identity-Flow. Nur so lassen sich beliebige Teammitglieder onboarden. Dafür gated GitHub dann gar nichts mehr.",[34,5075,5076],{"color":36,"icon":278},[10,5077,5078],{},"Die Sichtbarkeit einer GitHub App ist keine Zugriffskontrolle. Privat sperrt die Falschen aus, öffentlich lässt alle rein. Wer entscheiden will, wer sich einloggen darf, muss das selbst tun.",[10,5080,5081,5082,5085],{},"Also ist Knechts App öffentlich, und die Erlaubnis liegt bei Knecht selbst: eine ",[59,5083,5084],{},"Allowlist",". Eine simple Members-Tabelle mit GitHub-Logins. Beim Setup wird der Ersteller automatisch als Owner eingetragen, alle weiteren werden über die Settings eingeladen. Wer nicht auf der Liste steht, wird nach dem GitHub-Login abgewiesen.",[10,5087,5088],{},"\"Öffentlich\" heißt übrigens nur, dass Fremde die App auf ihren eigenen Repos installieren könnten. Das ist harmlos: Knecht benutzt ausschließlich Installationen, von denen er weiß.",[10,5090,5091,5092,5097,5098,5101],{},"Ein Detail noch, weil es oft übersehen wird: Eine Session ist typischerweise ein Cookie, das bis zu seinem Ablauf gültig bleibt. Wer nur beim Login prüft, hat entfernte Mitglieder also noch tagelang im System, die ",[51,5093,5096],{"href":5094,"rel":5095},"https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FSession_Management_Cheat_Sheet.html",[55],"OWASP nennt das fehlende serverseitige Invalidierung",". Knecht prüft die Mitgliedschaft deshalb bei ",[59,5099,5100],{},"jedem"," Request neu. Wird jemand entfernt, ist er beim nächsten Klick draußen.",[19,5103,3923],{"id":3922},[10,5105,5106],{},"Rollen. Aktuell hat jedes Mitglied vollen Zugriff, inklusive Einladen. Für die kleinen Teams, für die Knecht gerade gebaut wird, reicht das. Feinere Rechte kommen, wenn echte Rollen gebraucht werden.",{"title":290,"searchDepth":291,"depth":291,"links":5108},[5109,5110,5111,5112,5113,5114],{"id":4887,"depth":291,"text":4888},{"id":4915,"depth":291,"text":4916},{"id":4942,"depth":291,"text":4943},{"id":5004,"depth":291,"text":5005},{"id":5038,"depth":291,"text":5039},{"id":3922,"depth":291,"text":3923},"2026-07-06","Ein Deep Dive in Knechts GitHub-Anbindung. Warum ein OAuth-Token nicht reicht, was eine GitHub App anders macht und wieso wir die Zugriffskontrolle am Ende selbst bauen mussten.",{},"\u002Fde\u002Fupdates\u002Fgh-auth-evolution",{"title":4876,"description":5116},"de\u002Fupdates\u002Fgh-auth-evolution","Security","YxC37Kd6YeFJpn3GAPhifwosMkad73seRXjVpTvOat4",{"id":5124,"title":4389,"body":5125,"date":5396,"description":5397,"extension":310,"meta":5398,"navigation":312,"path":5399,"seo":5400,"stem":5401,"tag":2039,"__hash__":5402},"updates_de\u002Fde\u002Fupdates\u002Fdashboard-architecture.md",{"type":7,"value":5126,"toc":5375},[5127,5146,5149,5153,5164,5179,5183,5226,5229,5233,5236,5240,5251,5255,5262,5266,5269,5272,5276,5287,5291,5315,5319,5337,5341,5354,5358,5367,5369,5372],[325,5128,5129,5139],{},[10,5130,5131,5132,5134,5135,70],{},"Dieser Post ist ein Blick unter die Haube. Die Container-Architektur ist der Unterbau, damit parallele Previews mit DDEV-Projekten funktionieren, nicht das Produkt selbst. Was Knecht macht und für wen er gedacht ist, steht auf der ",[51,5133,333],{"href":332}," und in ",[51,5136,5138],{"href":5137},"\u002Fupdates\u002Fwas-macht-knecht","Was macht der Knecht",[10,5140,5141,5142,5145],{},"Update, Juli 2026: Wir haben die Sysbox-Sandbox gebaut, und sie lief. Trotzdem haben wir sie wieder ausgebaut. Warum, steht in der Fortsetzung ",[51,5143,5144],{"href":3589},"Zurück zum geteilten Daemon",". Der Weg zur Entscheidung unten bleibt trotzdem lesenswert, die Fortsetzung baut darauf auf.",[10,5147,5148],{},"Knecht läuft selbst als Docker Container, muss aber DDEV-Projekte hochfahren, die wieder aus Containern bestehen. Ob das überhaupt funktioniert, war die riskanteste Annahme der ganzen Architektur. Deshalb haben wir sie zuerst getestet, noch bevor der eigentliche Code entsteht.",[19,5150,5152],{"id":5151},"der-ansatz","Der Ansatz",[10,5154,5155,5156,5159,5160,5163],{},"Kurz zum Hintergrund: Docker besteht aus zwei Teilen. Der ",[51,5157,4404],{"href":4402,"rel":5158},[55]," ist der Dienst, der Container tatsächlich erstellt und verwaltet. Die Docker CLI ist nur ein Client, der über einen Unix-Socket (",[14,5161,5162],{},"\u002Fvar\u002Frun\u002Fdocker.sock",") mit dem Daemon spricht. Wer den Socket hat, steuert den Daemon.",[10,5165,5166,5167,5172,5173,5178],{},"Genau das nutzt Knecht: Der Container startet keinen eigenen Daemon, sondern mountet den Socket des Hosts und steuert damit den Host-Daemon. Das Muster heißt ",[51,5168,5171],{"href":5169,"rel":5170},"https:\u002F\u002Fwww.avonture.be\u002Fblog\u002Fdocker-out-of-docker-dood\u002F",[55],"Docker-out-of-Docker"," (DooD). Die DDEV-Container laufen dann als Geschwister neben dem Knecht-Container auf dem Host, nicht verschachtelt in ihm. ",[51,5174,5177],{"href":5175,"rel":5176},"https:\u002F\u002Fcoolify.io",[55],"Coolify"," und viele CI-Runner arbeiten genauso.",[19,5180,5182],{"id":5181},"der-test","Der Test",[75,5184,5185,5189,5195,5199,5205,5209,5212,5216],{"level":77},[79,5186,5188],{"id":5187},"host-daemon-erreichbar","Host-Daemon erreichbar",[10,5190,5191,5194],{},[14,5192,5193],{},"docker ps"," im Container zeigt die Host-Container. Der Socket-Mount funktioniert.",[79,5196,5198],{"id":5197},"ddev-projekt-bootet","DDEV-Projekt bootet",[10,5200,5201,5202,5204],{},"Ein minimales PHP-Projekt am festen Pfad, dann ",[14,5203,2579],{},". DDEV startet Webserver und Datenbank auf dem Host.",[79,5206,5208],{"id":5207},"container-sind-host-geschwister","Container sind Host-Geschwister",[10,5210,5211],{},"Auf dem Host tauchen die Projekt-Container als normale Nachbarn des Knecht-Containers auf.",[79,5213,5215],{"id":5214},"app-erreichbar","App erreichbar",[10,5217,5218,5219,5222,5223,5225],{},"Das gebootete Projekt antwortet. Wichtig dabei: ",[14,5220,5221],{},"*.ddev.site"," löst per DNS immer auf ",[14,5224,2594],{}," auf, also auf den Container selbst. Der Preview-Proxy muss den Web-Container deshalb direkt über das Docker-Netzwerk ansprechen.",[10,5227,5228],{},"Damit ist die Basis bewiesen: Ein Prozess im Knecht-Container kann echte DDEV-Stacks booten, mit korrekt gemountetem Source Code.",[19,5230,5232],{"id":5231},"zwei-probleme-sind-geblieben","Zwei Probleme sind geblieben",[10,5234,5235],{},"Der Test hat aber auch die Grenzen des Ansatzes gezeigt.",[79,5237,5239],{"id":5238},"keine-isolation-zwischen-runs","Keine Isolation zwischen Runs",[10,5241,5242,5243,5247,5248,5250],{},"Alles läuft auf einem gemeinsamen Daemon. Knecht soll aber mehrere Previews parallel fahren, ohne dass sich Runs gegenseitig stören. Wir mussten sogar den ",[51,5244,5246],{"href":4449,"rel":5245},[55],"DDEV-Router"," weglassen. Das ist der Reverse Proxy, den DDEV vor alle Projekte stellt und der die Ports 80 und 443 auf dem Host bindet. Feste Host-Ports gibt es nur einmal, bei vielen parallelen Projekten kollidiert das. Ohne Router muss Knecht in die ",[14,5249,2051],{}," eingreifen und zum Beispiel dir Urls überschreiben.",[79,5252,5254],{"id":5253},"fremder-code-am-host-socket","Fremder Code am Host-Socket",[10,5256,5257,5258,5261],{},"Der Agent führt Code aus, und wir kontrollieren nicht vollständig, was er tut. Ein Agent lässt sich über ",[51,5259,4487],{"href":4485,"rel":5260},[55]," kapern, etwa durch manipulierte Inhalte im Repo oder in einer Datenquelle, und macht dann nicht mehr das, was wir wollten. Wer an dieser Stelle an den Host-Socket kommt, steuert den Daemon, und wer den Daemon steuert, kann jeden Container mit jedem Mount starten. Das ist faktisch Root auf dem Server.",[19,5263,5265],{"id":5264},"die-lösung-eine-sandbox-pro-run","Die Lösung: eine Sandbox pro Run",[10,5267,5268],{},"Beide Probleme haben dieselbe Antwort: Jeder Run bekommt seinen eigenen Docker Daemon in einer eigenen Sandbox. Damit verschwindet auch der Grund, den Router wegzulassen. Port 80 in Sandbox A kann nicht mit Sandbox B kollidieren, weil jede Sandbox ihren eigenen Netzwerk-Namespace hat. Projekte laufen also wieder mit allen ihren URLs und Settings.",[10,5270,5271],{},"Bleibt die Frage, was die Sandbox ist. Wir haben uns die üblichen Kandidaten angesehen.",[79,5273,5275],{"id":5274},"privileged-docker-in-docker","Privileged Docker-in-Docker",[10,5277,5278,5279,5286],{},"Ein zweiter Daemon in einem Container mit ",[51,5280,5283],{"href":5281,"rel":5282},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fcontainers\u002Frun\u002F#runtime-privilege-and-linux-capabilities",[55],[14,5284,5285],{},"--privileged",". Die Flag schaltet praktisch alle Schutzmechanismen ab und gibt dem Container Zugriff auf die Host-Geräte. Eine bösartige Dependency kommt damit ohne Exploit auf den Host. Fällt raus.",[79,5288,5290],{"id":5289},"kata-containers","Kata Containers",[10,5292,5293,5298,5299,5304,5305,5308,5309,5314],{},[51,5294,5297],{"href":5295,"rel":5296},"https:\u002F\u002Fkatacontainers.io",[55],"Kata"," startet jeden Container in einer leichtgewichtigen VM mit eigenem Kernel. Das ist die stärkste Grenze und der Ansatz hinter ",[51,5300,5303],{"href":5301,"rel":5302},"https:\u002F\u002Ffirecracker-microvm.github.io",[55],"Firecracker",", auf dem etwa AWS Lambda läuft. Kata braucht aber ",[14,5306,5307],{},"\u002Fdev\u002Fkvm",", und normale Cloud-VPS bieten ",[51,5310,5313],{"href":5311,"rel":5312},"https:\u002F\u002Flowendtalk.com\u002Fdiscussion\u002F154261\u002Fsearching-vps-for-nested-virtualization",[55],"keine Nested Virtualization",". Das würde Bare-Metal erzwingen und kollidiert mit dem Ziel, Knecht auf einem normalen VPS zu installieren. Fällt raus.",[79,5316,5318],{"id":5317},"gvisor","gVisor",[10,5320,5321,5325,5326,344,5331,5336],{},[51,5322,5318],{"href":5323,"rel":5324},"https:\u002F\u002Fgvisor.dev",[55]," fängt Syscalls in einem Userspace-Kernel ab und braucht kein KVM. Es isoliert Anwendungsprozesse gut, tut sich aber schwer damit, selbst ein Docker-Host zu sein: ",[51,5327,5330],{"href":5328,"rel":5329},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fgvisor\u002Fissues\u002F9917",[55],"iptables wird nur teilweise unterstützt",[51,5332,5335],{"href":5333,"rel":5334},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fgvisor\u002Fissues\u002F12475",[55],"Overlayfs bricht mit Docker v29",". Genau ein Docker-Host ist aber, was DDEV braucht. Fällt raus.",[79,5338,5340],{"id":5339},"unsere-wahl-sysbox","Unsere Wahl: Sysbox",[10,5342,5343,5346,5347,5349,5350,5353],{},[51,5344,4398],{"href":4396,"rel":5345},[55]," ist eine Container-Runtime, die Docker-in-Docker ohne ",[14,5348,5285],{}," erlaubt. Sie nutzt dafür ",[51,5351,4531],{"href":4529,"rel":5352},[55],", Root im Container wird auf einen unprivilegierten User auf dem Host gemappt. Sysbox ist genau für diesen Fall gebaut, braucht kein KVM und läuft damit auf jedem normalen Linux-Server.",[19,5355,5357],{"id":5356},"das-threat-model-entscheidet","Das Threat Model entscheidet",[10,5359,5360,5361,5366],{},"Den Ausschlag gibt am Ende nicht die Runtime, sondern das Threat Model, also die Frage, wer hier eigentlich angreift und was er erreicht. Knecht ist ",[51,5362,5365],{"href":5363,"rel":5364},"https:\u002F\u002Fwww.geeksforgeeks.org\u002Fsystem-design\u002Fsingle-tenant-vs-multi-tenant-architecture\u002F",[55],"Single-Tenant",": eine Agentur, ihre Projekte, ihr Server. Die VM-Grenze von Kata schützt vor allem davor, dass ein Kunde aus seiner Umgebung in die eines anderen ausbricht. Diesen Fall gibt es hier nicht.",[19,5368,3677],{"id":3676},[10,5370,5371],{},"Als Nächstes bauen wir die Sysbox-Sandbox pro Run (eigener Daemon, DDEV inklusive Router) und einen zentralen Ingress mit Auth-Gate, der Preview-URLs in die richtige Sandbox routet. Der nächste Meilenstein: DDEV mit Router in einer Sysbox-Sandbox auf einem echten Linux-Host, erreichbar vom Ingress.",[10,5373,5374],{},"Vielleicht gibt es auch schon den ersten Showcase vom Prototypen.",{"title":290,"searchDepth":291,"depth":291,"links":5376},[5377,5378,5384,5388,5394,5395],{"id":5151,"depth":291,"text":5152},{"id":5181,"depth":291,"text":5182,"children":5379},[5380,5381,5382,5383],{"id":5187,"depth":297,"text":5188},{"id":5197,"depth":297,"text":5198},{"id":5207,"depth":297,"text":5208},{"id":5214,"depth":297,"text":5215},{"id":5231,"depth":291,"text":5232,"children":5385},[5386,5387],{"id":5238,"depth":297,"text":5239},{"id":5253,"depth":297,"text":5254},{"id":5264,"depth":291,"text":5265,"children":5389},[5390,5391,5392,5393],{"id":5274,"depth":297,"text":5275},{"id":5289,"depth":297,"text":5290},{"id":5317,"depth":297,"text":5318},{"id":5339,"depth":297,"text":5340},{"id":5356,"depth":291,"text":5357},{"id":3676,"depth":291,"text":3677},"2026-07-01","Knecht kann aus einem Container heraus echte DDEV-Projekte booten. Was der Test gezeigt hat und warum jeder Run künftig seine eigene Sandbox bekommt.",{},"\u002Fde\u002Fupdates\u002Fdashboard-architecture",{"title":4389,"description":5397},"de\u002Fupdates\u002Fdashboard-architecture","vwIkZdt1--gy-iCEXBtXnflpKRdK-gfPdC-_tg3dBDY",{"id":4,"title":5,"body":5404,"date":308,"description":309,"extension":310,"meta":5583,"navigation":312,"path":313,"seo":5584,"stem":315,"tag":316,"__hash__":317},{"type":7,"value":5405,"toc":5568},[5406,5410,5412,5414,5418,5422,5424,5437,5439,5456,5458,5460,5465,5467,5473,5475,5496,5503,5505,5507,5514,5516,5518,5522,5524,5526,5528,5533,5549,5551,5553,5560,5564,5566],[10,5407,12,5408,17],{},[14,5409,16],{},[19,5411,22],{"id":21},[10,5413,25],{},[10,5415,28,5416,32],{},[14,5417,31],{},[34,5419,5420],{"color":36,"icon":37},[10,5421,40],{},[19,5423,44],{"id":43},[10,5425,5426,49,5428,57,5431,62,5433,66,5435,70],{},[14,5427,16],{},[51,5429,56],{"href":53,"rel":5430},[55],[59,5432,61],{},[14,5434,65],{},[14,5436,69],{},[10,5438,73],{},[75,5440,5441,5443,5445,5447,5449,5451],{"level":77},[79,5442,82],{"id":81},[10,5444,85],{},[79,5446,89],{"id":88},[10,5448,92],{},[79,5450,96],{"id":95},[10,5452,99,5453,105],{},[51,5454,104],{"href":102,"rel":5455},[55],[10,5457,108],{},[19,5459,112],{"id":111},[10,5461,115,5462,121],{},[51,5463,120],{"href":118,"rel":5464},[55],[10,5466,124],{},[19,5468,5469,131,5471],{"id":127},[14,5470,130],{},[14,5472,134],{},[10,5474,137],{},[139,5476,5477,5485],{},[142,5478,5479,148,5483,152],{},[59,5480,5481],{},[14,5482,31],{},[59,5484,151],{},[142,5486,5487,164,5494,168],{},[59,5488,5489],{},[51,5490,5492],{"href":159,"rel":5491},[55],[14,5493,163],{},[59,5495,167],{},[10,5497,171,5498,177,5501,180],{},[51,5499,176],{"href":174,"rel":5500},[55],[14,5502,134],{},[19,5504,184],{"id":183},[10,5506,187],{},[10,5508,190,5509,195,5512,199],{},[51,5510,194],{"href":53,"rel":5511},[55],[14,5513,198],{},[19,5515,203],{"id":202},[10,5517,206],{},[10,5519,209,5520,212],{},[14,5521,134],{},[10,5523,215],{},[10,5525,218],{},[19,5527,222],{"id":221},[10,5529,225,5530,231],{},[51,5531,230],{"href":228,"rel":5532},[55],[139,5534,5535,5539,5545],{},[142,5536,5537,239],{},[59,5538,238],{},[142,5540,5541,245,5543,249],{},[59,5542,244],{},[59,5544,248],{},[142,5546,252,5547,256],{},[59,5548,255],{},[19,5550,260],{"id":259},[10,5552,263],{},[10,5554,5555,269,5557,275],{},[59,5556,268],{},[51,5558,274],{"href":272,"rel":5559},[55],[34,5561,5562],{"color":36,"icon":278},[10,5563,281],{},[19,5565,285],{"id":284},[10,5567,288],{},{"title":290,"searchDepth":291,"depth":291,"links":5569},[5570,5571,5576,5577,5578,5579,5580,5581,5582],{"id":21,"depth":291,"text":22},{"id":43,"depth":291,"text":44,"children":5572},[5573,5574,5575],{"id":81,"depth":297,"text":82},{"id":88,"depth":297,"text":89},{"id":95,"depth":297,"text":96},{"id":111,"depth":291,"text":112},{"id":127,"depth":291,"text":302},{"id":183,"depth":291,"text":184},{"id":202,"depth":291,"text":203},{"id":221,"depth":291,"text":222},{"id":259,"depth":291,"text":260},{"id":284,"depth":291,"text":285},{},{"title":5,"description":309},{"id":5586,"title":5587,"body":5588,"date":5664,"description":5665,"extension":310,"meta":5666,"navigation":312,"path":5667,"seo":5668,"stem":5669,"tag":3186,"__hash__":5670},"updates_de\u002Fde\u002Fupdates\u002Fbeta-tester.md","Beta Tester gesucht",{"type":7,"value":5589,"toc":5658},[5590,5598,5602,5605,5619,5622,5626,5629,5633,5636,5640,5651],[10,5591,5592,5593,5597],{},"Knecht läuft. Was ihm jetzt fehlt, sind echte Projekte, denn lokale Tests erzeugen keine echten Bugs. Deshalb suchen wir eine Handvoll Agenturen und Freelancer, die Knecht als Erste im Alltag einsetzen. Wenn du direkt dabei sein willst, trag dich im ",[51,5594,5596],{"href":5595},"#cta","Formular unter diesem Post"," ein.",[19,5599,5601],{"id":5600},"was-ihr-bekommt","Was ihr bekommt",[10,5603,5604],{},"Der Einstieg soll euch nichts kosten. Deshalb bekommt ihr von uns:",[139,5606,5607,5610,5613,5616],{},[142,5608,5609],{},"Ihr müsst Knecht nicht selbst installieren. Wir hosten eure Instanz während der Beta und kümmern uns um Updates und Betrieb.",[142,5611,5612],{},"100 € OpenCode-Guthaben, damit die AI Agents sofort loslegen können.",[142,5614,5615],{},"Das erste Jahr nach dem Release ist für euch kostenlos.",[142,5617,5618],{},"Euer Logo mit Link auf der Startseite.",[10,5620,5621],{},"Ihr geht also kein finanzielles Risiko ein und startet mit einem lauffähigen Knecht, eingerichtet auf eure Projekte. Wer Knecht lieber auf dem eigenen Server betreibt, kann das weiterhin tun.",[19,5623,5625],{"id":5624},"was-wir-brauchen","Was wir brauchen",[10,5627,5628],{},"Im Gegenzug wünschen wir uns eine Person, die Knecht wirklich benutzt und uns regelmäßig sagt, wo er sie blockiert, was fehlt und was gut funktioniert. Mehr verlangen wir nicht. Dieses Feedback aus echten Projekten ist für uns mehr wert als jeder lokale Test.",[19,5630,5632],{"id":5631},"für-wen-das-passt","Für wen das passt",[10,5634,5635],{},"Am meisten bringt Knecht dort, wo viele Projekte gewartet werden. Ihr passt gut, wenn ihr mit DDEV arbeitet und genug Projekte betreut, dass Security-Updates und Bugfixes bei euch Routinearbeit sind. Wenn eure Projekte noch nicht auf DDEV laufen, sich aber leicht umstellen lassen, passt das auch.",[19,5637,5639],{"id":5638},"mitmachen","Mitmachen",[10,5641,5642,5643,5646,5647,70],{},"Wenn das nach euch klingt, trag dich im ",[51,5644,5645],{"href":5595},"Formular direkt unter diesem Post"," ein und wähle \"Ich will aktiv mittesten & Feedback geben\". Wir melden uns dann bei dir. Wenn du vorab Fragen hast, schreib uns an ",[51,5648,5650],{"href":5649},"mailto:hallo@knecht.works","hallo@knecht.works",[10,5652,5653,5654,5657],{},"Und wenn Testen gerade nichts für euch ist, ihr aber wissen wollt, wie es weitergeht, wählt im ",[51,5655,5656],{"href":5595},"Formular"," einfach \"Nur Updates bekommen\".",{"title":290,"searchDepth":291,"depth":291,"links":5659},[5660,5661,5662,5663],{"id":5600,"depth":291,"text":5601},{"id":5624,"depth":291,"text":5625},{"id":5631,"depth":291,"text":5632},{"id":5638,"depth":291,"text":5639},"2026-06-08","Wir suchen eine Handvoll Agenturen und Freelancer, die Knecht auf echten Projekten einsetzen. Hosting, Guthaben und das erste Jahr übernehmen wir.",{},"\u002Fde\u002Fupdates\u002Fbeta-tester",{"title":5587,"description":5665},"de\u002Fupdates\u002Fbeta-tester","rqnxRVRSCcfdr6LPkEjGk_wP4J06NdpDjKZej6cjDPY",{"id":5672,"title":5673,"body":5674,"date":5740,"description":5741,"extension":310,"meta":5742,"navigation":312,"path":5743,"seo":5744,"stem":5745,"tag":3186,"__hash__":5746},"updates_de\u002Fde\u002Fupdates\u002Flandingpage.md","Landingpage ist Online",{"type":7,"value":5675,"toc":5733},[5676,5679,5685,5689,5692,5703,5707,5710,5714,5717,5719,5722,5725],[10,5677,5678],{},"Die Landingpage ist online, und das ganz bewusst, bevor überhaupt noch der Prototyp steht.",[34,5680,5682],{"color":36,"icon":5681},"i-lucide-mouse",[10,5683,5684],{},"Schon auf den Knecht geklickt?",[19,5686,5688],{"id":5687},"warum-die-seite","Warum die Seite",[10,5690,5691],{},"Ein Produkt komplett fertig zu bauen und dann zu hoffen, dass es jemand braucht, ist riskant. Deshalb steht die Landingpage am Anfang. Sie macht drei Dinge:",[139,5693,5694,5697,5700],{},[142,5695,5696],{},"Sie erklärt die Idee in wenigen Sätzen.",[142,5698,5699],{},"Sie sammelt die ersten Beta-Tester.",[142,5701,5702],{},"Sie macht den Weg öffentlich, damit Feedback früh kommt und nicht erst zum Launch.",[79,5704,5706],{"id":5705},"sprache","Sprache",[10,5708,5709],{},"Viele Startups zielen gleich auf den Internationalen Markt ab. Wir finden, dass dies mit der Sprache anfängt. Wir werden uns zuerst auf den DACH Raum konzentrieren. Englisch als zweite Sprache muss daher noch etwas warten.",[19,5711,5713],{"id":5712},"was-sie-noch-nicht-ist","Was sie (noch) nicht ist",[10,5715,5716],{},"Damit keine falschen Erwartungen entstehen: Diese Seite ist kein Produkt. Kein Login, keine Demo, nichts zum Ausprobieren. Was du siehst, ist die Idee und ein erster Blick auf die geplante Oberfläche.",[19,5718,3677],{"id":3676},[10,5720,5721],{},"Als Nächstes steht der Prototyp an: Projekte, Workflows und der AI Agent.",[10,5723,5724],{},"Das Ziel für die nächsten Wochen ist klar: die ersten 3 bis 5 Agenturen als Beta-Tester finden, die Knecht an echten Projekten testen und ehrlich zurückmelden, was funktioniert.",[34,5726,5727],{"color":2897,"icon":2898},[10,5728,5729,5730,5597],{},"Willst du früh dabei sein? Trag dich als ",[51,5731,5732],{"href":5595},"Beta-Tester",{"title":290,"searchDepth":291,"depth":291,"links":5734},[5735,5738,5739],{"id":5687,"depth":291,"text":5688,"children":5736},[5737],{"id":5705,"depth":297,"text":5706},{"id":5712,"depth":291,"text":5713},{"id":3676,"depth":291,"text":3677},"2026-06-07","Warum die Landingpage noch vor dem Produkt kommt und wie es weitergeht.",{},"\u002Fde\u002Fupdates\u002Flandingpage",{"title":5673,"description":5741},"de\u002Fupdates\u002Flandingpage","kJ5_0QdjoBF8GZX_T6ff8axeB5En4fAnQkcKwhGzl-0",{"id":5748,"title":5138,"body":5749,"date":5853,"description":5854,"extension":310,"meta":5855,"navigation":312,"path":5856,"seo":5857,"stem":5858,"tag":3186,"__hash__":5859},"updates_de\u002Fde\u002Fupdates\u002Fwas-macht-knecht.md",{"type":7,"value":5750,"toc":5840},[5751,5754,5758,5761,5764,5768,5774,5778,5781,5791,5794,5797,5800,5803,5807,5810,5814,5817],[10,5752,5753],{},"Ein Bugreport beginnt immer mit dem Versuch, den Fehler zu reproduzieren. Und nach dem Fix muss man sicherstellen, dass er wirklich behoben ist. Oder?",[19,5755,5757],{"id":5756},"motivation","Motivation",[10,5759,5760],{},"Agenturen betreuen oft viele Projekte. Diese sind häufig unterschiedlich konfiguriert oder laufen nur auf dem Rechner des einen Mitarbeiters, der von Anfang an dabei ist. Trotzdem brauchen alle regelmäßig Security-Updates und Bugfixes.",[10,5762,5763],{},"Damit AI Agents hier wirklich helfen können, brauchen wir vollständig lauffähige Projekte.",[79,5765,5767],{"id":5766},"security-updates","Security Updates",[10,5769,5770,5771,5773],{},"Um ein Security-Update in ein CMS einzuspielen, ist fast immer eine laufende Datenbank nötig – denn nach einem Update können Datenbank-Migrationen anstehen. Dependabot und andere Bots können deshalb nicht einfach ein ",[14,5772,4738],{}," machen und live ausrollen.",[79,5775,5777],{"id":5776},"bug-fixes","Bug fixes",[10,5779,5780],{},"Leider ist es nicht immer so einfach:",[5782,5783,5785],"prompt",{"description":5784},"Pls fix!",[10,5786,5787],{},[51,5788,5789],{"href":5789,"rel":5790},"https:\u002F\u002Fyoutu.be\u002FdQw4w9WgXcQ?t=0",[55],[10,5792,5793],{},"Für einen Bugfix muss zuerst ein Mitarbeiter frei sein – im besten Fall einer, der das Projekt schon lokal laufen hat, um den Fehler nachhaltig zu lösen. Und wenn nicht?",[19,5795,2063],{"id":5796},"ddev",[10,5798,5799],{},"DDEV ist eine Docker-basierte Entwicklungsumgebung. Mit ihr lässt sich ein Projekt vollständig betreiben, ohne Entwickler-Tools direkt auf dem Gerät zu installieren.",[10,5801,5802],{},"So fährt jedes Projekt reproduzierbar und isoliert hoch: Web und Datenbank kommen sauber konfiguriert und immer auf die gleiche Weise. Genau diese Verlässlichkeit ist die Grundlage für alles Weitere. Denn was sich auf Knopfdruck immer gleich booten lässt, kann auch eine Maschine booten.",[19,5804,5806],{"id":5805},"was-der-knecht-macht","Was der Knecht macht",[10,5808,5809],{},"Knecht wird als Dashboard gebaut, das du auf deinem eigenen Server hostest – so behältst du die Daten. Im Dashboard gibt es Preview-Links und Screenshots (über Penthouse), mit denen du einen Bugfix direkt verifizieren kannst.",[79,5811,5813],{"id":5812},"how-it-works","How it works",[10,5815,5816],{},"In Knecht gibt es Projekte, Workflows und Trigger.",[75,5818,5819,5823,5826,5829,5832,5835,5837],{"level":77},[79,5820,5822],{"id":5821},"projekte","Projekte",[10,5824,5825],{},"Ein Projekt besteht immer aus einem GitHub-Repository, in dem DDEV bereits konfiguriert ist. Darin lassen sich dann noch Environment-Variablen und die Datenbank festlegen.",[79,5827,4227],{"id":5828},"workflows",[10,5830,5831],{},"Ein Workflow ist ein vordefinierter Prozess, der mit Projekten arbeitet. Er kann zum Beispiel so aussehen: Projekt booten → Composer-Pakete updaten → PR mit den Änderungen erstellen. Die Schritte werden also deterministisch abgearbeitet.",[10,5833,5834],{},"Bugfixes sind aber selten immer gleiche, wiederholbare Schritte. Hier kommen AI Agents ins Spiel: Über Opencode können sie einen Bug im laufenden Projekt reproduzieren und beheben.",[79,5836,787],{"id":1177},[10,5838,5839],{},"Ein Trigger ist der Startpunkt eines Workflows. Über Webhooks – etwa von Jira oder GitHub – kann der Knecht vordefinierte Workflows automatisch starten und die Ergebnisse später als Pull Request liefern.",{"title":290,"searchDepth":291,"depth":291,"links":5841},[5842,5846,5847],{"id":5756,"depth":291,"text":5757,"children":5843},[5844,5845],{"id":5766,"depth":297,"text":5767},{"id":5776,"depth":297,"text":5777},{"id":5796,"depth":291,"text":2063},{"id":5805,"depth":291,"text":5806,"children":5848},[5849,5850,5851,5852],{"id":5812,"depth":297,"text":5813},{"id":5821,"depth":297,"text":5822},{"id":5828,"depth":297,"text":4227},{"id":1177,"depth":297,"text":787},"2026-05-15","Über die Ideenfindung, die Motivation und wie Knecht dabei hilft.",{},"\u002Fde\u002Fupdates\u002Fwas-macht-knecht",{"title":5138,"description":5854},"de\u002Fupdates\u002Fwas-macht-knecht","msrJ1TNS3K6UwxP10vSuvIWlVpfQfhc_9j7ywOlIOwA",1790265015160]