Ingest-Queue: Dokumente von außen einreichen, ohne dass ungeprüfter Fremdinhalt in raw/ landet #32
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Umgesetzt und ausgeliefert in
5.0.0-beta.11, Commit8285218(plus5b916c6für diedocs/-Nachziehung). Abgespalten aus #19 (Sitzung 2026-09-01), entworfen und gebaut am 2026-09-11.Der Anspruch, neu gefasst: Positiv-Liste statt Abwesenheit
#19s Eigenschaft war strukturell:
chemenu.apiundchemenu.mcp.serverimportieren nichts unterchemenu.commands, also existierennew,touch,xref,cite,publishim Servercode gar nicht. Einsubmit-Tool macht diesen Satz falsch — „es gibt kein Tool, das schreibt" hält nicht mehr, sobald eines schreibt.Die tragfähige Fassung ist deshalb keine Abwesenheit, sondern eine Positiv-Liste (Operator-Entscheidung 2026-09-11):
Umgesetzt als ein Nadelöhr:
chemenu.upload._write_atomic_within()löst jeden Zielpfad auf (Path.resolve(), also auch über Symlinks und..), verweigert alles außerhalb<root>/mcp-upload/und schreibt erst dann — Temp-Datei plusos.replace. Jeder Schreibvorgang dessubmit-Pfades geht durch diese eine Funktion.Die Abwesenheitseigenschaft bleibt daneben unverändert bestehen und wird nicht aufgegeben: die Reviewer-Kommandos (
upload accept/upload reject) liegen unterchemenu.commandsund sind vom Server aus nicht erreichbar. Der bestehende Struktur-Test (sys.modulesin einem frischen Interpreter) ist unverändert grün.Die Entscheidungen
1. Quarantäne ist ein eigenes Top-Level-Verzeichnis
mcp-upload/, gitignoriert. Nichtincoming/und nicht darunter:raw accepttoleriert und ignoriert seit #67 ein Unterverzeichnis inincoming/, einincoming/submitted/x.pdfwäre also ungeprüft promotierbar gewesen. Im Repo und gitignoriert, nicht außerhalb — #58 Entscheidung 1 gilt hier wörtlich weiter (Beweismittelgit check-ignore --no-index, keine neue Konfigurationsfläche). Kein.gitkeep: die Schreibprimitive legt das Verzeichnis selbst an, also braucht es weder einen Bootstrap-Schritt nochdist export-Seeding (das ist genau die Lücke, die #88 fürincoming/beschreibt und die hier nicht entsteht).Die Pipeline hat damit vor
raw/zwei Stufen und zwei verschiedene Grenzen:2. Das Nadelöhr ist eine Funktion, nicht eine Konvention.
tools/chemenu/upload.py(neu) importiert nur Stdlib undchemenu.config— keintyper, keinsubprocess, nichts unterchemenu.commands.3. Die Einreicher-Identität kommt aus einem Header, den die Middleware setzt — nie aus einem Tool-Argument.
Context.headers(mcp 2.1.1) liefert die HTTP-Header, auf stdioNone; das SDK warnt ausdrücklich, einen Header nie als Identitätszusicherung zu lesen. Umgesetzt: Headername in der Konfiguration (DefaultX-Forwarded-User), fehlender Header ⇒ Verweigerung ohne jeden Schreibvorgang, und das Manifest hältsubmitter_sourcenebensubmitter, damit der Datensatz sagt, worauf die Behauptung ruht. Ein Tool-Argumentsubmitterwird gar nicht angeboten. Die Deployment-Pflicht (Header setzen, Client-Kopie verwerfen) steht inINSTALL-MCP.mdSchritt 5 undinstructions/ingest-queue.md.4. Die menschliche Prüfung ist ein Gate mit Exit 42 — der vierte des Stacks, Upload Review Gate.
upload accept <id>verweigert beim ersten Aufruf, druckt Manifest, Hash, Größe, Einreicher und die--confirm <token>-Zeile. Token wie beim Mass-Update-Gate: sha256 über Id, Dateiname, Größe, Sha256 und Einreicher, auf 12 Hexstellen.upload rejecthat keinen Gate — Ablehnen braucht keine Freigabe, nur Annehmen.5. Limits stehen in
.wikitool-upload.json, und ihre Abwesenheit schaltet den Schreibpfad ab. Nicht „unbeschränkt" wie bei.wikitool-remotes.json, sondern „dassubmit-Tool wird gar nicht registriert" — die sichere Richtung. Eine defekte Datei ist ein Startfehler des Servers und einFAILim neuendoctor-Check, nie „keine Beschränkung".6. Das Ledger ist append-only und die Buchhaltung zugleich.
mcp-upload/ledger.jsonl, eine JSON-Zeile pro Ereignis (submitted/accepted/rejected). Kontingent = Summe dersubmitted-Ereignisse dieses Einreichers im rollierenden 24-Stunden-Fenster; kein Read-Modify-Write, also kein Wettlauf. Verweigerungen kommen nicht ins Ledger. Arbeitsteilung gegen das Manifest (Invariante 8): Manifest = was angekommen ist, Ledger = was passiert ist.7. Ablehnung behält den Eintrag, löscht das Material. Der Ledger-Eintrag wird vor dem Löschen geschrieben, sodass eine Unterbrechung zwischen beiden den Grund trotzdem auf Platte lässt.
8. Keine Statusoberfläche für den Einreicher in diesem Paket. Die Antwort des
submit-Aufrufs (Manifest mit Id) ist die Rückmeldung. Eine Statusabfrage wäre eine zweite Leseoberfläche mit eigenem Autorisierungsmodell — bei Bedarf ein eigenes Issue.9. Die Verbindung Rohdatei ↔ Einreichung ist der Inhalts-Hash, kein neues Frontmatter-Feld. Das Ledger hält Id und sha256, die Datei ist unverändert, also ist die Zuordnung nachrechenbar. Kein
submitted_by:im Type-Spec.Umsetzungsnotizen
len*3/4), aber sie überschätzt um genau die 0–2 Padding-Zeichen. Der erste Entwurf hätte damit eine Einreichung exakt an der Grenze fälschlich abgelehnt; die Prüfung verweigert jetzt erst beiapprox - 2 > max_bytes, und der Nachdekodier-Check bleibt die exakte Durchsetzung. Test für beide Richtungen (exakt an der Grenze wird angenommen, ein Byte darüber abgelehnt).read_configläuft einmal inbuild_server, sodass eine defekte Datei ein Startfehler ist und nicht erst beim erstensubmitauffällt.ValidationErrorstattfail()inupload.py. Die Bibliotheksgrenze aus #31: der Server bekommt Exceptions (über_guardalsToolErroran den Aufrufer), die CLI übersetzt sie infail()→ ERROR-Zeile, Exit 1.docs verify-Regel zu Issue-Nummern beachtet:tools/CONTRACT.md,instructions/gates.mdundinstructions/mcp-read-server.mdnennen keine Issue-Nummern;tools/**/*.pyist davon ausgenommen (sieheissue-tracking.md) und behält seine Verweise im Code.docs/-Veralterung geprüft und behoben:docs/pipeline-rationale.mdbehauptete, die Vertrauensgrenze sei allein die Beförderung nachraw/undincoming/liege „ganz auf der Nahseite" — mit einem Weg von außen beschreibt „ein Mensch hat die Datei irgendwo abgelegt" nicht mehr alles, wasincoming/erreicht. Der Absatz benennt jetzt zwei Grenzen (Vertrauen, Unveränderlichkeit).docs/why-gates-are-code.mdträgt den vierten Gate samt Begründung, warum er die Form des Mass-Update-Gates wiederverwendet statt eine vierte zu erfinden.docs/ownership-and-templates.mdunddocs/version-model.mdgeprüft, unverändert gültig.upload list/showsind read-only, zählen aber wiesources coverage— die Ausnahmeliste ist eine kurierte Konstante, keine „ändert das Wiki nicht"-Regel.Akzeptanzkriterien
<root>/mcp-upload/— Tests für.., absoluten Pfad, Pfadtrenner im Dateinamen und Symlink, der aus dem Verzeichnis hinausführt (test_write_primitive_refuses_a_relative_escape,..._an_absolute_path,..._a_symlink_escape,test_sanitize_filename_rejects_unsafe_names)submithat sich keine Datei außerhalb<root>/mcp-upload/geändert (test_submit_writes_only_into_mcp_upload_incoming_stays_untouched, Baum-Schnappschuss vorher/nachher plusgit status --porcelainauf getrackte Dateien)test_the_server_module_cannot_reach_a_write_command(frischer Interpreter,sys.moduleskennt nichts unterchemenu.commands) bleibt unverändert grünsubmitohne Identitätsheader verweigert und schreibt nichts; Manifest trägtsubmitter,submitter_source, Zeit, sha256, Größe, Dateinamen, Id (test_submit_without_identity_writes_nothing,test_submit_without_identity_header_is_a_tool_error,test_submit_on_stdio_with_no_headers_at_all_is_a_tool_error)submitist nicht registriert, solange.wikitool-upload.jsonfehlt; eine defekte Datei ist ein Startfehler — Test für alle drei Zustände (test_submit_is_absent_without_the_upload_config,test_submit_is_present_once_armed,test_a_malformed_upload_config_refuses_to_build)mcp-upload/sich ändert; die Größenprüfung greift vor dem Dekodieren (test_submit_rejects_oversized_before_decodingmit gemocktemb64decode, plus die vier weiteren Größen-/Endungs-/Kontingent-Tests)test_submit_rejects_a_duplicate_pending_hash_naming_the_waiting_id); nach einer Ablehnung ist derselbe Inhalt wieder einreichbar (test_submit_allows_resubmission_after_rejection)upload accept <id>ohne Token: Exit 42, Manifest und Token-Zeile im Output, nichts bewegt (test_accept_without_a_token_needs_clearance,test_accept_with_a_stale_token_needs_clearance_again)upload accept --confirm <token>: Datei inincoming/, sha256 unverändert, Einreichungsverzeichnis fort,accepted-Ereignis im Ledger,raw//kb//work//reports/unberührt (test_promote_moves_file_deletes_dir_and_ledgers_accepted,test_accept_with_the_right_token_promotes)upload reject --reason: Material gelöscht,rejected-Ereignis mit Grund und sha256, nichts außerhalbmcp-upload/<id>/angefasst; leerer Grund verweigert (test_reject_deletes_material_and_ledgers_with_reason,test_reject_requires_a_reason)kb/gelangen kann: die Kette endet inincoming/, und der Serverprozess erreichtkb/schreibend in keiner Richtung (Positiv-Listen-Tests plus der unverändertesys.modules-Test)mcp-upload/erzeugt keinuncovered_raw_files-Finding und ist fürlint/sources coverageunsichtbar — per Konstruktion (beide gehen überconfig.iter_raw_files(RAW_DIR)); im Promote-Test zusätzlich geprüft, dassraw/,kb/,work/,reports/gar nicht entstehengit check-ignore --no-indexmeldetmcp-upload/probe.pdfund.wikitool-upload.jsonals ignoriert, geprüft überdocs_verify.REQUIRED_IGNORE_CANARIESdoctorberichtet den Zustand der Intake-Konfiguration und den Füllstand der Quarantäne (check_upload_intake, drei Tests: fehlend, scharf, defekt)AGENTS.md§ Gates undinstructions/gates.md; alle Zählwörter nachgezogen („Four limits", „Four gates use it today", „four hard limits", „why the four gates", „the other three clear with a token")instructions/ingest-queue.mdgeschrieben, aus demwiki-ingest-Skill (Schritt 1) erreichbar,instructions verifygrün — 21 Instructions statt 20tools/CONTRACT.mdträgt Kommando- und Fehlerkontrakt-Zeilen für alle vierupload-Kommandos;docs verifygrün in beiden Richtungen (55 dokumentierte Kommandos statt 51)raw/CONTRACT.mdbeschreibt die Quarantäne als Stufe vorincoming/;INSTALL-MCP.mdnennt Opt-in (neuer Schritt 7) und Middleware-Anforderung;README.mdunddocs/why-gates-are-code.mdnachgezogen5.0.0-beta.11), Drop-in-Test in beide Richtungen im Eintrag begründetVerifiziert
tools/wikitool docs verify— OK: 55 Kommandos dokumentiert, 11 Ignore-Kanarien klar, keine Issue-Nummern in 65 ausgelieferten Dokumenten, TOCs aktuell auf 37 Referenzdateien.tools/wikitool instructions verify— OK: 21 Instructions und 7 Skills gültig, 14 publizierte Kopien identisch.pytest -qintools/: 1185 passed, davon neu 49 intest_upload.py, 9 intest_upload_cmd.py, 6 intest_mcp_server.py, 3 intest_doctor.py.chemenu/upload.py93.1 %,chemenu/commands/upload_cmd.py90.7 %,chemenu/mcp/server.py100 %.CHEMENU_ROOT: Einreichung →upload list/show→upload acceptohne Token (Exit 42, Manifest und Token gedruckt) → mit Token (Datei inincoming/, Quarantäneverzeichnis fort, beide Ledger-Zeilen) →doctormeldet „submit tool armed".INSTALL-MCP.md§ Verifikation): Tool-Liste zeigt alle sechs Werkzeuge, undsubmitverweigert dort korrekt mangels Identitätsheader (ctx.headersist auf stdioNone) —mcp-upload/entsteht dabei gar nicht.8285218: Läufe 189 (verify) und 190 (release) grün — Tests, Coverage, Verify-Tree, Version-Gate,dist exportund der Frisch-Instanz-Replay des Exports. Lauf 190 hat korrekt kein Release geschnitten:VERSIONträgt-beta.11, jüngstes Release bleibtv4.7.4. Lauf 191 zu5b916c6(reinedocs/-Prosa) lief beim Schließen noch.Abgrenzung (eingehalten)
raw/, keine Konvertierung, keine Umsortierung des Bestands, kein Deployment.Was daraus offen weiterläuft
mcp-upload/braucht dasselbe persistente Volume wie der übrige Checkout, sonst verliert eine ungeprüfte Einreichung ihre Quarantäne. Als Satz inINSTALL-MCP.md§ „Was hier bewusst nicht steht" hinterlegt.incoming/.gitkeep): in dieser Sitzung aufgefallen und getrennt erfasst;mcp-upload/umgeht das Problem, weil nur das Werkzeug dort schreibt.fetch && reset --hardbleiben — eingit clean -xdwürde die Quarantäne löschen. Als Warnung ininstructions/mcp-read-server.mdSchritt 5 hinterlegt; im Baum existiert heute keingit clean.Schließt #32.
Changelog:
prio/waiting→prio/planned. Der im Body benannte Auslöser „#19 muss stehen" ist gefeuert - #19 ist geschlossen (2.4.0).Changelog:
status/blockedgesetzt, an #16 (Triage-Sitzung 2026-09-04). Kein Body-Rewrite, nur das Label — der Body ist inhaltlich weiterhin richtig.Die Vorbedingung #19 ist erfüllt (geschlossen mit 2.4.0, der Server läuft auf beiden Transports), damit fällt der ursprüngliche Auslöser weg. Was bleibt, ist die zweite Abhängigkeit, die der Body schon nennt:
wikitool raw acceptist Geschwister zuraw renameaus #16 und teilt sich dessen Mechanik — eine Datei bewegen, ohne dass eine Referenz zwischendurch ins Leere zeigt. Die zweimal getrennt zu bauen, wäre die zweite Kopie, die auseinanderläuft.In der Abarbeitungsreihenfolge steht #16 deshalb direkt vor diesem Issue.
prio/planned,kind/build,size/M,area/kbunverändert.Aus der Sitzung 2026-09-04 zur Ordnerorganisation: #58 kommt von der anderen Seite auf dieselbe Mechanik. Dort geht es nicht um fremd eingereichtes Material, sondern darum, dass der Nutzer seine eigenen Dateien nicht mehr von Hand nach
articles//documents//notes//assets/einsortieren soll — und dass beim Muster „PDF hochladen, nach Markdown konvertieren" zwei Dateien einer logischen Quelle entstehen, deren Zusammengehörigkeit heute ausschließlich inraw_files:steht und im Dateisystem unsichtbar ist.Ein Mechanismus, zwei Auslöser. Vorgeschlagene Arbeitsteilung: #58 baut
raw accept, dieses Issue konsumiert es und ergänzt, was nur der Fremdeinreichung eigen ist — Auth, Kontingente, Manifest mit Einreicherzurechnung, Ablehnungspfad.Zwei Punkte, die dabei aus diesem Issue heraus zu beachten sind:
incoming/will, dass die Ingest-Sitzung hineinsieht. Auflösung in #58: die Grenze zwischen Sitzung und Pipeline-Kommando ziehen statt zwischen Sitzung und Verzeichnis —sources coverageundlintsehen den Eingang nie (sonst meldet jede unverarbeitete Ablage sofort ein Finding), ein Mensch und seine Sitzung schon. Das ist auch für dieses Issue die richtige Grenze, hier nur strenger begründet.status/blockedbleibt hier korrekt — dieses Issue hängt weiterhin an #19. #58 hängt an nichts davon und ist heute baubar; genau deshalb ist es abgespalten und nicht als Kommentar hier gelandet.torben referenced this issue2026-09-09 09:02:33 +00:00
Changelog: Body vollständig neu geschrieben (Vorbereitungssitzung 2026-09-11),
status/blockedentfernt,size/M→size/L.Fünf Stellen waren überholt und sind korrigiert statt umschrieben: die Blockade auf #19 (geschlossen mit 2.4.0), die Abhängigkeit von #31 (geschlossen), die Erwartung,
raw acceptgemeinsam mit #16 zu bauen (in #58 gebaut, in #67 umgestellt; #16 ist keine Abhängigkeit), die offene Frage nach dem Ort des Eingangs (von #58 Entscheidung 1 mitentschieden) und die Quarantäne-Skizze überincoming/<typ>/(seit #67 istincoming/flach und ein Unterverzeichnis wird toleriert und ignoriert — eine Fremdeinreichung darunter wäre still promotierbar gewesen, was jetzt der Grund für ein eigenes Verzeichnis ist).Vier Operator-Entscheidungen sind neu im Body, alle vom 2026-09-11:
submitals weiteres Tool — aber mit Positiv-Liste. Das ist die inhaltliche Korrektur an #19s Formulierung: „nichts Schreibendes ist importiert" hält nicht mehr, sobald ein Tool schreibt. Die neue Fassung ist ein Nadelöhr im Code — der Prozess darf in genau ein Verzeichnis (mcp-upload/) schreiben, jeder andere aufgelöste Pfad wird verweigert. Die Abwesenheitseigenschaft bleibt daneben bestehen: die Reviewer-Kommandos liegen unterchemenu.commandsund werden vom Server nicht importiert.upload accept, mit--confirm-Token wie beim Mass-Update-Gate — die Prüfung ist damit Code und nicht eine Instruction neben einem Kommando.Zwei Machbarkeitsfragen wurden vorab am Baum geprüft, weil das Design an ihnen hängt:
mcp2.1.1 gibt einem Tool überContext.headersdie HTTP-Header (auf stdioNone), warnt aber ausdrücklich, einen Header nie als Identitätszusicherung zu lesen — daher die Anforderung an die Middleware, den Header zu setzen und eine Client-Kopie zu verwerfen. Und im Baum existiert keingit clean, der dokumentierte Sync (fetch && reset --hard) lässt ignorierte Dateien stehen; die Quarantäne im Baum überlebt ihn, und die Warnung gehört ininstructions/mcp-read-server.mdSchritt 5.kind/buildstattkind/decision: nach diesen vier Antworten ist keine Entscheidung mehr offen. Was im Body unter „Offen, aber nicht blockierend" steht, ist Deployment (#37, Volume für die Quarantäne) und eine Messung bei der Umsetzung (Transportgrenze für große base64-Nutzlasten).size/M→size/L, weil das Paket gegenüber der alten Skizze einen Gate, eine neue Instruction, eine Kommandogruppe und den Doku-Nachzug über acht Dateien enthält.Changelog: Body auf den Endzustand umgeschrieben und geschlossen. Umgesetzt in
5.0.0-beta.11, Commit8285218(+5b916c6fürdocs/pipeline-rationale.md).Gegenüber der Vorbereitungsfassung von heute Morgen geändert: alle 19 Akzeptanzkriterien abgehakt und mit den Testnamen belegt, die sie prüfen; die vier Entscheidungen lesen als entschieden statt als Vorschlag; § „Umsetzungsnotizen" neu — darunter der einzige Fund, der das Design während der Umsetzung korrigiert hat (die base64-Längenprüfung überschätzt um die 0–2 Padding-Zeichen und hätte eine Einreichung exakt an der Grenze fälschlich abgelehnt; sie verweigert jetzt erst bei
approx - 2 > max_bytes, der Nachdekodier-Check bleibt die exakte Durchsetzung). § „Verifiziert" nennt beide End-to-End-Läufe (CLI und echter MCP-stdio-Client) und CI-Lauf 189. § „Offen weiterläuft" verweist auf #37 (Volume für die Quarantäne) und #88.docs/-Veralterung geprüft, ein echter Fund:docs/pipeline-rationale.mdbehauptete, die Vertrauensgrenze sei allein die Beförderung nachraw/undincoming/liege „ganz auf der Nahseite" — das beschreibt mit einem Weg von außen nicht mehr alles, wasincoming/erreicht. Der Absatz benennt jetzt zwei Grenzen.docs/why-gates-are-code.mdträgt den vierten Gate;ownership-and-templates.mdundversion-model.mdsind unverändert gültig.torben referenced this issue2026-09-11 09:34:27 +00:00