Eingangskanäle generalisieren: mehrere konfigurierte Kanäle mit Handler, alle münden in die Quarantäne (MCP-submit einer davon, OneDrive-Ordner ein anderer) #213

Closed
opened 2026-10-12 16:28:27 +00:00 by torben · 5 comments
Owner

Abgespalten aus der Diskussion zu #186 (2026-10-12).

Stand: erledigt (2026-10-13). Gebaut, veröffentlicht, mit Release 10.0.0 ausgeliefert und von Hand auf dem Windows-Arbeitsrechner geprüft.

  • Veröffentlicht als 10.0.0-beta.11 in Commit cceb9a7, CI-Läufe 678 und 679 grün.
  • Nachzug beim Abschluss als 10.0.0-beta.12 in Commit ecefe0a, CI-Läufe 680 und 681 grün. AGENTS.md § Gates und instructions/mcp-read-server.md nannten noch nur submit bzw. die Sektion upload.
  • Prüfung von Hand (Betreiber, 2026-10-13, Release 10.0.0, GitHub Copilot CLI): collect aus einem OneDrive-Ordner mit Dateien vom Handy, beide offenen Punkte aus Entscheidung 7 beantwortet, wiki-ingest ohne Argument holt ab und lässt das Gate beim Menschen. Ergebnis in Entscheidung 7 und im letzten Kriterium.
  • Das Ergebnis zu Entscheidung 7 steht jetzt auch im How-to (docs/how-to/collect-from-a-synced-folder.md § settle_seconds, Commit f2f251f, reine docs/-Prosa ohne Bump, CI-Lauf 691 grün).
  • Folgepakete aus der Prüfung: #216 (ein folder-Kanal wird nicht gegen backup.target geprüft; Export und Eingang angleichen) und #219 (Stub: wiki-ingest ohne Argument arbeitet alle wartenden Dateien in einem Kontext ab).

Anlass

Eine berufliche Instanz entsteht gerade, auf einem Windows-Rechner (pwsh 7). Dort soll man Dokumente von überall her über einen OneDrive-Ordner in die Instanz bringen können:

  • vom Handy,
  • von einem anderen Rechner,
  • später auch aus Automationen (Power Automate, Microsoft-365-Agenten).

Heute schreibt vor allem der Betreiber selbst in den Ordner. Das muss aber offen bleiben, und was dort ankommt, gilt nicht automatisch als vertrauenswürdig.

Vor diesem Paket gab es genau einen Kanal von außen: das opt-in MCP-Werkzeug submit. Es schrieb in die Quarantäne mcp-upload/. Ins Wiki kam das Material nur über das Upload Review Gate (upload accept, Exit 42).

Befunde aus dem Baum (Stand vor dem Bau)

Die Fundstellen sind mit cceb9a7/ecefe0a nachgezogen. mcp-upload nennen heute nur noch CHANGES.md, die Migrationsanleitung und die Erkennung des alten Verzeichnisses (config.LEGACY_UPLOAD_DIRNAME, upload.py, upload_cmd.py, doctor.py, raw/CONTRACT.md § Altverzeichnis).

  • Die Sektion upload war unveröffentlicht.
    • Es gab sie nur im laufenden Kandidaten 10.0.0 (Settings-Tiers, #207, f6aaadc, 2026-10-10). 9.0.0 las noch .wikitool-upload.json.
    • config migrate überführte diese Datei in die Sektion upload.
    • config migrate kann nur aus den vier Altdateien umziehen. Sektionen baut es nicht um.
  • Der Kandidat hatte die Grenze schon überschritten (9 → 10). version bump wertet jeden Bump dieses Kandidaten als Überschreitung gegen das letzte Release (version_cmd.py, crossing). Ein weiteres --breaking wird angehängt.
  • Ein neuer Verzeichnisname hätte eine .gitignore-Lücke gerissen. Die Wurzel-.gitignore ignorierte nur /mcp-upload/. Ohne passenden Eintrag hätte publish (git add -A) ein neues Verzeichnis mit committet. Deshalb trägt quarantine/ heute eine eigene .gitignore (Entscheidung 5).
  • mcp-upload kam an vielen Stellen vor: in Code (config.py, upload.py, upload_cmd.py, mcp/server.py, raw/pending.py, doctor.py, backup.py/backup_cmd.py, docs_verify.py), in Instructions, in Docs und in Tests. Siehe „Betroffene Dateien“.
  • Der MCP-Server bleibt abgeschottet. upload collect liegt unter chemenu.commands und in chemenu/collect.py und ist damit, wie accept/reject, aus dem MCP-Serverprozess nicht importierbar.
  • Bestehende Mechanik, die für jeden Kanal gilt: Dedupe per sha256 (digest_to_pending_id), Ledger und confirm_token. Die Quote (_check_quota) hängt an submitter und gilt nur für mcp.
  • Vorbild für Programmaufrufe: fetch_routes.run_fetcher ruft ohne Shell auf, schließt stdin, setzt ein Timeout und gibt bei einem Fehler das Ende von stderr aus. Der Handler command folgt dem Muster.

Entscheidungen

  1. Was ein Kanal liefert, landet immer in der Quarantäne, nie direkt in incoming/. incoming/ hält laut raw/CONTRACT.md nur Material, das ein Mensch an der Tastatur bewusst eingeworfen hat. In die Pipeline kommt Material nur über upload accept (Gate, Mensch).

  2. Verworfen: incoming/ als NTFS-Junction oder Symlink in den OneDrive-Ordner.

    • Das würde die Quarantäne umgehen.
    • incoming/.gitkeep ist eingecheckt. Ein Link an der Stelle des Verzeichnisses kann dazu führen, dass publish das Löschen von .gitkeep oder einen Link-Eintrag committet.
    • raw accept verschiebt mit rename/os.replace. Das scheitert über Laufwerksgrenzen.
  3. Mehrere Kanäle, jeder mit einem Handler. MCP-submit ist ein Kanal (mcp), der OneDrive-Ordner ein anderer (folder). Weitere Quellen schließt der Betreiber über command an, ohne Stack-Änderung.

  4. Abholen gibt kein Vertrauen, deshalb darf der Agent es auslösen. upload collect [<kanal>...] holt aus den Pull-Kanälen in die Quarantäne. Aus der Quarantäne heraus führt nur upload accept mit Gate.

  5. E1: Die Quarantäne heißt quarantine/, und der Bruch kommt mit 10.0.0.

    • Eigene .gitignore: Wer die Quarantäne anlegt, legt darin quarantine/.gitignore mit dem Inhalt * an. So wird sie nie committet, auch nicht mit einer veralteten Wurzel-.gitignore. Die Wurzel-.gitignore trägt /quarantine/ statt /mcp-upload/.

    • Altes Verzeichnis: Existiert noch ein mcp-upload/ mit Inhalt, dann:

      • meldet doctor FAIL unter upload-intake mit dem Verschiebebefehl,
      • enden upload list/show/accept/reject/collect mit Exit 1, bevor sie etwas tun, und nennen denselben Befehl,
      • startet der MCP-Server nicht.

      Es wird nichts automatisch verschoben.

    • Migration: instructions/migrations/10.0.0-settings-tiers.md hat einen Schritt für das Verschieben, mit Befehl für POSIX und pwsh.

    • Sicherung: Die Maschinensicherung (#186) schließt quarantine/ aus.

  6. E2: Ein Handler ist nur eine Quelle. Er liefert Dateien unverändert. Konvertieren und Ableiten bleiben Sache von raw derive nach upload accept. Material wird nie verändert, bevor ein Mensch es gesichtet hat.

  7. E3: Wann eine Datei als fertig synchronisiert gilt (folder). Übernommen wird eine Datei nur, wenn beide Bedingungen gelten:

    • ihre mtime und ihre Erstellungszeit (st_birthtime, wo vorhanden, sonst nur die mtime) sind älter als settle_seconds;
    • Größe und sha256 der Quelle sind vor und nach dem Kopieren gleich.

    Sonst bleibt sie liegen und wird als „noch in Bewegung“ gemeldet.

    Belegt durch die Prüfung von Hand (2026-10-13, OneDrive for Business unter Windows 11):

    • OneDrive behält die ursprünglichen Zeiten. Eine vom Handy hochgeladene PDF lag im Ordner mit CreationTime 07.10.2026 17:30:03 und LastWriteTime 07.10.2026 17:30:04, also mit dem Stand der Datei, nicht mit dem Zeitpunkt der Ankunft (13.10.). settle_seconds hält eine solche Datei also nicht zurück.
    • Ein Platzhalter von Files On Demand wird beim Lesen heruntergeladen. Die Datei trug vor dem Abholen die Attribute Archive, ReparsePoint (nur online). collect hat sie gelesen, damit heruntergeladen und übernommen. Die Kopie in der Quarantäne trägt die Zeit des Abholens.
    • Folge: Gegen eine unvollständig angekommene Datei schützt bei OneDrive der Vergleich von Größe und sha256, nicht settle_seconds. Das How-to nannte diese Grenze schon und nennt jetzt auch den beobachteten Fall. Eine Codeänderung folgt daraus nicht, wie vorab entschieden.
  8. E4: wiki-ingest ohne Argument holt zuerst ab.

    • Ablauf in Schritt 1, Zweig „The user named nothing“, vor raw pending:
      1. upload collect (alle Pull-Kanäle),
      2. upload list,
      3. Meldung „N Einreichungen warten auf Freigabe“ mit ids und dem Weg über upload show/upload accept (instructions/ingest-queue.md).
    • Kein Pull-Kanal konfiguriert: collect endet mit Exit 0 und „nothing to collect“.
    • Ein Kanal scheitert: Der Lauf meldet es und fährt mit incoming/ fort. Er wiederholt nichts.
    • Was nie passiert: Der Agent ruft upload accept nicht auf und gibt kein Token selbst ein. Das Gate bleibt beim Menschen.
    • incoming/ ist leer und Einreichungen warten: Der Lauf endet mit der Liste.
    • Ausdrücklich auslösen dürfen collect Betreiber und Agent.
  9. E5: keine Quoten für folder/command. Es gilt max_bytes je Datei, und eine Flut von Dateien fällt am Gate auf. Eine Höchstzahl je Lauf wäre ein eigenes Paket.

  10. E6: Die Sektion upload erscheint nie in einem Release. Erfüllt: 10.0.0 ist mit intake erschienen.

    • Sie ist durch intake ersetzt.
    • config migrate schreibt .wikitool-upload.json direkt nach intake.channels.mcp mit handler: "mcp". quota.* wird dabei flach zu quota_*.
    • Es gibt kein Lesen beider Formen und keine WARN für eine alte Sektion.

Konfiguration

Sektion intake in .wikitool-local.json (config_shapes.INTAKE, ersetzt config_shapes.UPLOAD). Das Vorbild ist fetch.routes/fetch.fetchers. Die erzeugte Referenz steht in docs/reference/configuration.md § Intake channels.

{
  "intake": {
    "channels": {
      "mcp": {
        "handler": "mcp",
        "identity_header": "X-Forwarded-User",
        "max_bytes": 10485760,
        "allowed_extensions": [".md", ".pdf"],
        "quota_submissions_per_day": 20,
        "quota_bytes_per_day": 52428800
      },
      "onedrive": {
        "handler": "folder",
        "path": "C:\\Users\\<user>\\OneDrive - <Firma>\\Chemenu-Eingang",
        "max_bytes": 52428800,
        "allowed_extensions": [".pdf", ".docx", ".msg", ".eml", ".md"],
        "settle_seconds": 60
      },
      "scanner": {
        "handler": "command",
        "command": "scanner-pull",
        "max_bytes": 52428800,
        "allowed_extensions": [".pdf"]
      }
    },
    "commands": {
      "scanner-pull": ["pwsh", "-NoProfile", "-File", "C:\\tools\\scanner-pull.ps1"]
    }
  }
}
Feld Tier Pflicht Bedeutung
channels checkout, gate ja Kanalname ([a-z0-9-]+) → Kanal. Der Name kommt ins Manifest und ist das Argument von upload collect.
channels.<n>.handler checkout, gate ja mcp, folder oder command. Höchstens ein Kanal mit mcp. Ein zweiter ist ein Fehler der Sektion.
max_bytes checkout, gate ja Größte einzelne Datei, für jeden Handler
allowed_extensions checkout, gate ja Mit ., ohne Rücksicht auf Groß-/Kleinschreibung
identity_header checkout, gate nein Nur mcp, Default X-Forwarded-User
quota_submissions_per_day, quota_bytes_per_day checkout, gate ja bei mcp Nur mcp
path checkout, gate ja bei folder Absolut, außerhalb der Instanz, nicht rekursiv. (Gegen backup.target wird nicht geprüft: #216.)
settle_seconds checkout, gate nein Nur folder, Default 60, ganze Zahl ≥ 0
command checkout, gate ja bei command Name eines Eintrags unter commands
commands checkout, names a command nein Name → Programm und Argumente als Array

Wie der Handler command läuft: Das Programm wird ohne Shell gestartet, stdin ist geschlossen, das Timeout beträgt 300 s. Als letztes Argument bekommt es ein leeres Staging-Verzeichnis unter quarantine/.staging/<zufall>/.

  • Endet es mit Exit 0, wird jede reguläre Datei im Staging-Verzeichnis (oberste Ebene) eine Einreichung, sofern Endung und Größe passen. Alles andere wird gemeldet und verworfen.
  • Bei jedem anderen Ende (Exit ≠ 0, Timeout, Start scheitert) wird nichts übernommen. Gemeldet wird das Ende von stderr (bis 2000 Zeichen).
  • Das Staging-Verzeichnis wird in jedem Fall entfernt.

Regeln für Fehler und Fehlen:

  • Feldnamen, die nicht zum Handler passen, sind ein Fehler der Sektion. Das gilt auch für einen command-Verweis ohne Eintrag unter commands.
  • Ohne Sektion gibt es weder submit noch einen Pull-Kanal. Das ist ein gültiger Zustand, doctor meldet OK (absent).
  • Eine fehlerhafte Sektion wird nie als „kein Kanal“ gelesen:
    • der MCP-Server startet nicht,
    • upload collect endet mit Exit 1,
    • doctor meldet FAIL.
  • Alle Felder sind gate bzw. names a command, deshalb lehnt config set sie mit Exit 42 ab.

Was gebaut wurde

  1. config_shapes.INTAKE ersetzt UPLOAD. upload.read_config liest die Kanäle. Der MCP-Server registriert submit genau dann, wenn ein Kanal mit handler: mcp existiert, und liest dessen Grenzen.
  2. Die Quarantäne heißt quarantine/ (config.UPLOAD_DIR). Alle Fundstellen sind nachgezogen, inklusive der eigenen .gitignore und der Erkennung des alten Verzeichnisses (Entscheidung 5).
  3. Das Manifest hat neue Felder: channel, handler und origin (Pfad der Quelldatei, command:<name> oder mcp:submit). Bei folder/command ist submitter = "unknown" und submitter_source = der Handler. Ledger-Einträge tragen channel.
  4. wikitool upload collect [<kanal>...] (schreibend; cli_contract-Eintrag in der Gruppe „Raw material and uploads“; Code in chemenu/collect.py):
    • Kanäle: Ohne Argument werden alle Pull-Kanäle abgeholt. Ein unbekannter Name oder ein mcp-Kanal als Argument ist Exit 1, bevor etwas geschieht.
    • Unabhängigkeit: Jeder Kanal wird unabhängig abgeholt. Scheitert einer, laufen die anderen trotzdem. Exit 1, wenn mindestens einer gescheitert ist, sonst Exit 0.
    • Ausgabe: je Kanal übernommen (mit id), übersprungen (mit Grund) und gescheitert.
    • Dubletten: Eine Datei, deren sha256 schon als wartende Einreichung existiert, wird nicht ein zweites Mal angelegt, sondern als Dublette (mit der id) gemeldet. Bei folder wird sie an der Quelle trotzdem entfernt, weil der Inhalt in der Quarantäne gesichert ist.
  5. doctor upload-intake: Gemeldet werden die Kanäle mit ihrem Handler, die Zahl der wartenden Einreichungen und das alte mcp-upload/ (FAIL). Bei folder gibt es WARN, wenn path nicht existiert.
  6. config migrate: Das Ziel von .wikitool-upload.json ist intake.channels.mcp.
  7. wiki-ingest Schritt 1 nach Entscheidung 8. instructions/ingest-queue.md nennt collect und die Herkunftsfelder des Manifests. Bei submitter: unknown gibt es keinen Absender, dem man trauen könnte.
  8. How-to docs/how-to/collect-from-a-synced-folder.md:
    • Kanal folder von Hand anlegen,
    • was liegen bleibt und warum,
    • settle_seconds,
    • die Grenze aus Entscheidung 7, seit f2f251f mit dem in der Prüfung beobachteten Verhalten von OneDrive,
    • dass Löschen in OneDrive auf allen Geräten wirkt (Papierkorb),
    • Kanal command mit einem Beispielskript.

Was ein Abholen garantiert

  • Gelöscht wird an der Quelle nur nach gesicherter Kopie (folder). Die Quelldatei wird erst entfernt, wenn ihre Kopie in der Quarantäne vollständig geschrieben ist (atomar über _write_atomic_within) und Größe und sha256 der Quelle nach dem Kopieren noch denen der Kopie entsprechen. Andernfalls bleibt die Quelle unverändert, und die halbe Einreichung wird entfernt.
  • Außerhalb der Quarantäne verändert collect nur eins: Es entfernt übernommene Dateien direkt in path. Es schreibt nie dorthin, löscht nie ein Verzeichnis und verfolgt keinen Symlink. Ein Symlink in path wird übersprungen und gemeldet.
  • Fremdes bleibt unberührt und wird gemeldet: Unterverzeichnisse, versteckte Dateien und nicht erlaubte Endungen. „Versteckt“ heißt: Der Name beginnt mit . oder ~$, oder unter Windows ist das Attribut hidden oder system gesetzt. Ebenso bleibt eine Datei über max_bytes liegen.
  • Was eine Datei in der Quarantäne heißt, bestimmt sanitize_filename. Ein Name, den es ablehnt, bleibt an der Quelle und wird gemeldet.
  • Nichts aus einem Kanal erreicht incoming/ oder raw/ außer über upload accept.

Akzeptanzkriterien

Tests in test_upload.py/test_upload_cmd.py/test_mcp_server.py/test_config_shapes.py/test_legacy_settings.py, soweit nicht anders genannt. Die Tests des Abholens (folder, command, upload collect, .gitignore der Quarantäne) stehen in test_collect.py.

  • Leer abgeholt: Nach upload collect gegen einen folder-Kanal mit zwei erlaubten Dateien liegen in quarantine/ zwei Einreichungen, deren sha256 den Quelldateien entspricht. Der Quellordner ist danach leer. Die Manifeste tragen channel, handler: folder, origin und submitter: unknown.
  • Unberührt geblieben: Ein Unterverzeichnis, eine Datei .x.pdf, eine Datei ~$a.docx, eine Datei mit nicht erlaubter Endung, eine Datei über max_bytes und ein Symlink im Quellordner sind nach collect byte-gleich vorhanden und erscheinen in der Ausgabe als übersprungen.
  • settle_seconds: Eine Datei mit mtime jünger als settle_seconds bleibt byte-gleich liegen und wird nicht eingereicht.
  • Änderung beim Kopieren: Ändert sich die Quelle zwischen Lesen und Löschprüfung (Test über Monkeypatch von collect.source_matches), bleibt die Quelle byte-gleich, und in quarantine/ entsteht keine Einreichung.
  • Dublette: Eine Datei, deren sha256 schon wartet, erzeugt keine zweite Einreichung und wird mit der vorhandenen id gemeldet.
  • command, Erfolg: Ein Programm, das zwei Dateien ins Staging-Verzeichnis legt und mit Exit 0 endet, ergibt zwei Einreichungen mit origin: command:<name>.
  • command, Misserfolg: Exit ≠ 0 bzw. Timeout ergibt null Einreichungen. quarantine/.staging/ ist danach leer bzw. fehlt, und die Ausgabe enthält das Ende von stderr.
  • Mehrere Kanäle: Scheitert ein Kanal, werden die anderen trotzdem abgeholt, und der Exit-Code ist 1. Ohne Pull-Kanal endet collect mit Exit 0 und „nothing to collect“. Ein unbekannter Kanalname oder ein mcp-Kanal als Argument ergibt Exit 1 ohne Wirkung.
  • .gitignore der Quarantäne: Nach der ersten Einreichung (über submit und über collect) existiert quarantine/.gitignore mit dem Inhalt *. In einem Test-Repo ohne Eintrag in der Wurzel-.gitignore zeigt git status --porcelain --untracked-files=all nichts unter quarantine/.
  • Altes Verzeichnis: Mit einem mcp-upload/ mit Inhalt enden upload list/show/accept/reject/collect mit Exit 1 und nennen den Verschiebebefehl. doctor meldet upload-intake FAIL, und mcp-upload/ ist danach byte-gleich.
  • Konfiguration: Zwei mcp-Kanäle, ein Feld, das nicht zum Handler passt, ein command-Verweis ohne Eintrag, ein relativer path und ein path innerhalb der Instanz sind jeweils ein Fehler der Sektion. config set intake.… endet mit Exit 42.
  • MCP: Mit mcp-Kanal registriert der Server submit mit dessen Grenzen. Ohne mcp-Kanal, aber mit folder-Kanal, ist submit nicht registriert. Eine submit-Einreichung trägt channel: mcp. Die bestehenden Tests zu Quote und Identität bleiben grün.
  • Migration der Einstellungen: config migrate mit einer .wikitool-upload.json (verschachtelte quota) ergibt intake.channels.mcp mit handler: mcp und den flachen quota_*-Feldern, und es entsteht keine Sektion upload.
  • Sicherung: Nach backup run liegt im Ziel keine Datei aus quarantine/ (test_backup.py).
  • Doku: Das How-to existiert und steht in docs/how-to/README.md. raw/CONTRACT.md, instructions/gates.md, instructions/ingest-queue.md, instructions/wiki-ingest/SKILL.md, instructions/mcp-read-server.md, docs/how-to/set-up-the-mcp-server.md, back-up-to-a-synced-folder.md und die Migration nennen quarantine/ und intake. Außerhalb von CHANGES.md, der Migrationsanleitung und der Erkennung des alten Verzeichnisses nennt kein Dokument mcp-upload/ mehr als aktuellen Ort (grep).
    • Beim Abschluss nachgezogen (ecefe0a): AGENTS.md § Gates (das Upload Review Gate gilt für jeden Kanal), instructions/mcp-read-server.md (Sektion intake statt upload), der Einleitungssatz in docs/reference/configuration.md und der Docstring von doctor.
  • Prüfläufe:
    • cceb9a7: lokal pytest (2997 bestanden, 4 übersprungen), docs verify und instructions verify grün; CI-Lauf 678 (Jobs verify und pwsh) und Lauf 679 success.
    • ecefe0a: lokal dieselben drei Prüfungen grün (2997 bestanden, 4 übersprungen); CI-Läufe 680 und 681 success.
    • f2f251f (How-to, Ergebnis von Entscheidung 7): docs verify lokal grün, CI-Lauf 691 success.
    • docs contract/docs reference sind neu erzeugt. Die Sektion intake steht in docs/reference/configuration.md, die Sektion upload nicht mehr.
  • Von Hand auf dem Windows-Arbeitsrechner (Betreiber, 2026-10-13, Release 10.0.0, GitHub Copilot CLI):
    • collect gegen einen Ordner unter OneDrive - <Firma> mit vom Handy hochgeladenen Dateien: übernommen in die Quarantäne, Quelle danach entfernt, Freigabe über upload accept sauber. doctor meldet upload-intake mit dem Kanal onedrive (folder).
    • Die beiden offenen Punkte aus Entscheidung 7 sind beantwortet und dort eingetragen: Die ursprünglichen Zeiten bleiben erhalten, und ein Platzhalter wird beim Lesen heruntergeladen.
    • wiki-ingest ohne Argument holt aus dem Ordner in die Quarantäne, zeigt die wartenden Einreichungen und lässt die Freigabe beim Betreiber. Nebenbefund: Danach arbeitet es mehrere wartende Dateien in einem Kontext ab → #219.

Versionsteil

  • 10.0.0-beta.11: MAJOR auf dem laufenden Kandidaten, --impact high.
    • --breaking "Die Quarantäne heißt quarantine/ statt mcp-upload/. Eine Instanz mit wartenden Einreichungen verschiebt das Verzeichnis einmal von Hand (Migrationsschritt); bis dahin enden die upload-Kommandos mit Exit 1."
    • Die Migration bleibt required (instructions/migrations/10.0.0-settings-tiers.md, ergänzt um den Schritt).
  • 10.0.0-beta.12: PATCH für den Nachzug beim Abschluss, --impact low.
  • Der How-to-Nachtrag f2f251f ist reine docs/-Prosa außerhalb des Version-Gates, ohne Bump.

Dass upload durch intake ersetzt wird, ist gegenüber 9.0.0 kein eigener Bruch, denn der Weg aus .wikitool-upload.json endet nur an anderer Stelle.

Betroffene Dateien und Flächen

  • Code: tools/chemenu/upload.py, tools/chemenu/collect.py (neu, damit der MCP-Serverprozess Abholen, Löschen und Programmstart nicht importieren kann), tools/chemenu/commands/upload_cmd.py (neu: collect), tools/chemenu/mcp/server.py, tools/chemenu/config_shapes.py (UPLOAD → INTAKE), tools/chemenu/legacy_settings.py, tools/chemenu/config.py (UPLOAD_DIR), tools/chemenu/commands/doctor.py (check_upload_intake), tools/chemenu/commands/raw/pending.py, tools/chemenu/commands/backup_cmd.py, tools/chemenu/commands/docs_verify.py, tools/chemenu/reference_doc.py (Region der Sektion), tools/chemenu/cli_contract.py (Gruppe).
  • Konfiguration: .gitignore.
  • Steuerebene und Instructions: AGENTS.md § Gates, raw/CONTRACT.md § Getting a file in from outside, instructions/gates.md, instructions/ingest-queue.md, instructions/wiki-ingest/SKILL.md, instructions/mcp-read-server.md, instructions/migrations/10.0.0-settings-tiers.md.
  • Docs: docs/reference/configuration.md, docs/how-to/set-up-the-mcp-server.md, docs/how-to/back-up-to-a-synced-folder.md, docs/how-to/ingest-a-source.md, docs/how-to/collect-from-a-synced-folder.md (neu; Nachtrag f2f251f), docs/how-to/README.md, docs/explanation/architecture.md, docs/explanation/pipeline-rationale.md, tools/README.md.
  • Generiert: tools/CONTRACT.md, docs/reference/commands/{README,git,instance-health,raw-material-and-uploads}.md, die Region in docs/reference/configuration.md.
  • Tests: test_upload.py, test_upload_cmd.py, test_mcp_server.py, test_config_shapes.py, test_legacy_settings.py, test_backup.py, test_doctor.py, test_collect.py (neu, 22 Tests).
  • CHANGES.md über version bump.

Kette

Kette „OneDrive/Microsoft 365“

Abgespalten aus der Diskussion zu #186 (2026-10-12). **Stand: erledigt (2026-10-13).** Gebaut, veröffentlicht, mit Release 10.0.0 ausgeliefert und von Hand auf dem Windows-Arbeitsrechner geprüft. - Veröffentlicht als `10.0.0-beta.11` in Commit `cceb9a7`, CI-Läufe 678 und 679 grün. - Nachzug beim Abschluss als `10.0.0-beta.12` in Commit `ecefe0a`, CI-Läufe 680 und 681 grün. `AGENTS.md` § Gates und `instructions/mcp-read-server.md` nannten noch nur `submit` bzw. die Sektion `upload`. - **Prüfung von Hand** (Betreiber, 2026-10-13, Release 10.0.0, GitHub Copilot CLI): `collect` aus einem OneDrive-Ordner mit Dateien vom Handy, beide offenen Punkte aus Entscheidung 7 beantwortet, `wiki-ingest` ohne Argument holt ab und lässt das Gate beim Menschen. Ergebnis in Entscheidung 7 und im letzten Kriterium. - Das Ergebnis zu Entscheidung 7 steht jetzt auch im How-to (`docs/how-to/collect-from-a-synced-folder.md` § `settle_seconds`, Commit `f2f251f`, reine `docs/`-Prosa ohne Bump, CI-Lauf 691 grün). - Folgepakete aus der Prüfung: #216 (ein `folder`-Kanal wird nicht gegen `backup.target` geprüft; Export und Eingang angleichen) und #219 (Stub: `wiki-ingest` ohne Argument arbeitet alle wartenden Dateien in einem Kontext ab). ## Anlass Eine berufliche Instanz entsteht gerade, auf einem Windows-Rechner (pwsh 7). Dort soll man Dokumente **von überall her** über einen OneDrive-Ordner in die Instanz bringen können: - vom Handy, - von einem anderen Rechner, - später auch aus Automationen (Power Automate, Microsoft-365-Agenten). Heute schreibt vor allem der Betreiber selbst in den Ordner. Das **muss aber offen bleiben**, und was dort ankommt, gilt **nicht automatisch als vertrauenswürdig**. Vor diesem Paket gab es genau einen Kanal von außen: das opt-in MCP-Werkzeug `submit`. Es schrieb in die Quarantäne `mcp-upload/`. Ins Wiki kam das Material nur über das Upload Review Gate (`upload accept`, Exit 42). ## Befunde aus dem Baum (Stand vor dem Bau) Die Fundstellen sind mit `cceb9a7`/`ecefe0a` nachgezogen. `mcp-upload` nennen heute nur noch `CHANGES.md`, die Migrationsanleitung und die Erkennung des alten Verzeichnisses (`config.LEGACY_UPLOAD_DIRNAME`, `upload.py`, `upload_cmd.py`, `doctor.py`, `raw/CONTRACT.md` § Altverzeichnis). - **Die Sektion `upload` war unveröffentlicht.** - Es gab sie nur im laufenden Kandidaten `10.0.0` (Settings-Tiers, #207, `f6aaadc`, 2026-10-10). 9.0.0 las noch `.wikitool-upload.json`. - `config migrate` überführte diese Datei in die Sektion `upload`. - `config migrate` kann nur aus den vier Altdateien umziehen. Sektionen baut es nicht um. - **Der Kandidat hatte die Grenze schon überschritten** (9 → 10). `version bump` wertet jeden Bump dieses Kandidaten als Überschreitung gegen das letzte Release (`version_cmd.py`, `crossing`). Ein weiteres `--breaking` wird angehängt. - **Ein neuer Verzeichnisname hätte eine `.gitignore`-Lücke gerissen.** Die Wurzel-`.gitignore` ignorierte nur `/mcp-upload/`. Ohne passenden Eintrag hätte `publish` (`git add -A`) ein neues Verzeichnis mit committet. Deshalb trägt `quarantine/` heute eine eigene `.gitignore` (Entscheidung 5). - **`mcp-upload` kam an vielen Stellen vor:** in Code (`config.py`, `upload.py`, `upload_cmd.py`, `mcp/server.py`, `raw/pending.py`, `doctor.py`, `backup.py`/`backup_cmd.py`, `docs_verify.py`), in Instructions, in Docs und in Tests. Siehe „Betroffene Dateien“. - **Der MCP-Server bleibt abgeschottet.** `upload collect` liegt unter `chemenu.commands` und in `chemenu/collect.py` und ist damit, wie `accept`/`reject`, aus dem MCP-Serverprozess nicht importierbar. - **Bestehende Mechanik, die für jeden Kanal gilt:** Dedupe per sha256 (`digest_to_pending_id`), Ledger und `confirm_token`. Die Quote (`_check_quota`) hängt an `submitter` und gilt nur für `mcp`. - **Vorbild für Programmaufrufe:** `fetch_routes.run_fetcher` ruft ohne Shell auf, schließt stdin, setzt ein Timeout und gibt bei einem Fehler das Ende von stderr aus. Der Handler `command` folgt dem Muster. ## Entscheidungen 1. **Was ein Kanal liefert, landet immer in der Quarantäne, nie direkt in `incoming/`.** `incoming/` hält laut `raw/CONTRACT.md` nur Material, das ein Mensch an der Tastatur bewusst eingeworfen hat. In die Pipeline kommt Material nur über `upload accept` (Gate, Mensch). 2. **Verworfen: `incoming/` als NTFS-Junction oder Symlink in den OneDrive-Ordner.** - Das würde die Quarantäne umgehen. - `incoming/.gitkeep` ist eingecheckt. Ein Link an der Stelle des Verzeichnisses kann dazu führen, dass `publish` das Löschen von `.gitkeep` oder einen Link-Eintrag committet. - `raw accept` verschiebt mit `rename`/`os.replace`. Das scheitert über Laufwerksgrenzen. 3. **Mehrere Kanäle, jeder mit einem Handler.** MCP-`submit` ist ein Kanal (`mcp`), der OneDrive-Ordner ein anderer (`folder`). Weitere Quellen schließt der Betreiber über `command` an, ohne Stack-Änderung. 4. **Abholen gibt kein Vertrauen, deshalb darf der Agent es auslösen.** `upload collect [<kanal>...]` holt aus den Pull-Kanälen in die Quarantäne. Aus der Quarantäne heraus führt nur `upload accept` mit Gate. 5. **E1: Die Quarantäne heißt `quarantine/`, und der Bruch kommt mit 10.0.0.** - **Eigene `.gitignore`:** Wer die Quarantäne anlegt, legt darin `quarantine/.gitignore` mit dem Inhalt `*` an. So wird sie nie committet, auch nicht mit einer veralteten Wurzel-`.gitignore`. Die Wurzel-`.gitignore` trägt `/quarantine/` statt `/mcp-upload/`. - **Altes Verzeichnis:** Existiert noch ein `mcp-upload/` mit Inhalt, dann: - meldet `doctor` `FAIL` unter `upload-intake` mit dem Verschiebebefehl, - enden `upload list/show/accept/reject/collect` mit Exit 1, bevor sie etwas tun, und nennen denselben Befehl, - startet der MCP-Server nicht. Es wird nichts automatisch verschoben. - **Migration:** `instructions/migrations/10.0.0-settings-tiers.md` hat einen Schritt für das Verschieben, mit Befehl für POSIX und pwsh. - **Sicherung:** Die Maschinensicherung (#186) schließt `quarantine/` aus. 6. **E2: Ein Handler ist nur eine Quelle.** Er liefert Dateien unverändert. Konvertieren und Ableiten bleiben Sache von `raw derive` nach `upload accept`. Material wird nie verändert, bevor ein Mensch es gesichtet hat. 7. **E3: Wann eine Datei als fertig synchronisiert gilt (`folder`).** Übernommen wird eine Datei nur, wenn beide Bedingungen gelten: - ihre mtime **und** ihre Erstellungszeit (`st_birthtime`, wo vorhanden, sonst nur die mtime) sind älter als `settle_seconds`; - Größe und sha256 der Quelle sind vor und nach dem Kopieren gleich. Sonst bleibt sie liegen und wird als „noch in Bewegung“ gemeldet. **Belegt durch die Prüfung von Hand (2026-10-13, OneDrive for Business unter Windows 11):** - **OneDrive behält die ursprünglichen Zeiten.** Eine vom Handy hochgeladene PDF lag im Ordner mit `CreationTime 07.10.2026 17:30:03` und `LastWriteTime 07.10.2026 17:30:04`, also mit dem Stand der Datei, nicht mit dem Zeitpunkt der Ankunft (13.10.). `settle_seconds` hält eine solche Datei also nicht zurück. - **Ein Platzhalter von Files On Demand wird beim Lesen heruntergeladen.** Die Datei trug vor dem Abholen die Attribute `Archive, ReparsePoint` (nur online). `collect` hat sie gelesen, damit heruntergeladen und übernommen. Die Kopie in der Quarantäne trägt die Zeit des Abholens. - **Folge:** Gegen eine unvollständig angekommene Datei schützt bei OneDrive der Vergleich von Größe und sha256, nicht `settle_seconds`. Das How-to nannte diese Grenze schon und nennt jetzt auch den beobachteten Fall. Eine Codeänderung folgt daraus nicht, wie vorab entschieden. 8. **E4: `wiki-ingest` ohne Argument holt zuerst ab.** - Ablauf in Schritt 1, Zweig „The user named nothing“, vor `raw pending`: 1. `upload collect` (alle Pull-Kanäle), 2. `upload list`, 3. Meldung „N Einreichungen warten auf Freigabe“ mit ids und dem Weg über `upload show`/`upload accept` (`instructions/ingest-queue.md`). - **Kein Pull-Kanal konfiguriert:** `collect` endet mit Exit 0 und „nothing to collect“. - **Ein Kanal scheitert:** Der Lauf meldet es und fährt mit `incoming/` fort. Er wiederholt nichts. - **Was nie passiert:** Der Agent ruft `upload accept` nicht auf und gibt kein Token selbst ein. Das Gate bleibt beim Menschen. - **`incoming/` ist leer und Einreichungen warten:** Der Lauf endet mit der Liste. - Ausdrücklich auslösen dürfen `collect` Betreiber und Agent. 9. **E5: keine Quoten für `folder`/`command`.** Es gilt `max_bytes` je Datei, und eine Flut von Dateien fällt am Gate auf. Eine Höchstzahl je Lauf wäre ein eigenes Paket. 10. **E6: Die Sektion `upload` erscheint nie in einem Release.** Erfüllt: 10.0.0 ist mit `intake` erschienen. - Sie ist durch `intake` ersetzt. - `config migrate` schreibt `.wikitool-upload.json` direkt nach `intake.channels.mcp` mit `handler: "mcp"`. `quota.*` wird dabei flach zu `quota_*`. - Es gibt kein Lesen beider Formen und keine `WARN` für eine alte Sektion. ## Konfiguration Sektion `intake` in `.wikitool-local.json` (`config_shapes.INTAKE`, ersetzt `config_shapes.UPLOAD`). Das Vorbild ist `fetch.routes`/`fetch.fetchers`. Die erzeugte Referenz steht in `docs/reference/configuration.md` § Intake channels. ```json { "intake": { "channels": { "mcp": { "handler": "mcp", "identity_header": "X-Forwarded-User", "max_bytes": 10485760, "allowed_extensions": [".md", ".pdf"], "quota_submissions_per_day": 20, "quota_bytes_per_day": 52428800 }, "onedrive": { "handler": "folder", "path": "C:\\Users\\<user>\\OneDrive - <Firma>\\Chemenu-Eingang", "max_bytes": 52428800, "allowed_extensions": [".pdf", ".docx", ".msg", ".eml", ".md"], "settle_seconds": 60 }, "scanner": { "handler": "command", "command": "scanner-pull", "max_bytes": 52428800, "allowed_extensions": [".pdf"] } }, "commands": { "scanner-pull": ["pwsh", "-NoProfile", "-File", "C:\\tools\\scanner-pull.ps1"] } } } ``` | Feld | Tier | Pflicht | Bedeutung | |---|---|---|---| | `channels` | checkout, gate | ja | Kanalname (`[a-z0-9-]+`) → Kanal. Der Name kommt ins Manifest und ist das Argument von `upload collect`. | | `channels.<n>.handler` | checkout, gate | ja | `mcp`, `folder` oder `command`. **Höchstens ein Kanal mit `mcp`.** Ein zweiter ist ein Fehler der Sektion. | | `max_bytes` | checkout, gate | ja | Größte einzelne Datei, für jeden Handler | | `allowed_extensions` | checkout, gate | ja | Mit `.`, ohne Rücksicht auf Groß-/Kleinschreibung | | `identity_header` | checkout, gate | nein | Nur `mcp`, Default `X-Forwarded-User` | | `quota_submissions_per_day`, `quota_bytes_per_day` | checkout, gate | ja bei `mcp` | Nur `mcp` | | `path` | checkout, gate | ja bei `folder` | Absolut, außerhalb der Instanz, nicht rekursiv. (Gegen `backup.target` wird nicht geprüft: #216.) | | `settle_seconds` | checkout, gate | nein | Nur `folder`, Default 60, ganze Zahl ≥ 0 | | `command` | checkout, gate | ja bei `command` | Name eines Eintrags unter `commands` | | `commands` | checkout, names a command | nein | Name → Programm und Argumente als Array | **Wie der Handler `command` läuft:** Das Programm wird ohne Shell gestartet, stdin ist geschlossen, das Timeout beträgt 300 s. Als letztes Argument bekommt es ein leeres Staging-Verzeichnis unter `quarantine/.staging/<zufall>/`. - Endet es mit Exit 0, wird jede reguläre Datei im Staging-Verzeichnis (oberste Ebene) eine Einreichung, sofern Endung und Größe passen. Alles andere wird gemeldet und verworfen. - Bei jedem anderen Ende (Exit ≠ 0, Timeout, Start scheitert) wird nichts übernommen. Gemeldet wird das Ende von stderr (bis 2000 Zeichen). - Das Staging-Verzeichnis wird in jedem Fall entfernt. **Regeln für Fehler und Fehlen:** - Feldnamen, die nicht zum Handler passen, sind ein Fehler der Sektion. Das gilt auch für einen `command`-Verweis ohne Eintrag unter `commands`. - Ohne Sektion gibt es weder `submit` noch einen Pull-Kanal. Das ist ein gültiger Zustand, `doctor` meldet `OK` (absent). - Eine fehlerhafte Sektion wird nie als „kein Kanal“ gelesen: - der MCP-Server startet nicht, - `upload collect` endet mit Exit 1, - `doctor` meldet `FAIL`. - Alle Felder sind `gate` bzw. `names a command`, deshalb lehnt `config set` sie mit Exit 42 ab. ## Was gebaut wurde 1. **`config_shapes.INTAKE`** ersetzt `UPLOAD`. `upload.read_config` liest die Kanäle. Der MCP-Server registriert `submit` genau dann, wenn ein Kanal mit `handler: mcp` existiert, und liest dessen Grenzen. 2. **Die Quarantäne heißt `quarantine/`** (`config.UPLOAD_DIR`). Alle Fundstellen sind nachgezogen, inklusive der eigenen `.gitignore` und der Erkennung des alten Verzeichnisses (Entscheidung 5). 3. **Das Manifest hat neue Felder:** `channel`, `handler` und `origin` (Pfad der Quelldatei, `command:<name>` oder `mcp:submit`). Bei `folder`/`command` ist `submitter` = `"unknown"` und `submitter_source` = der Handler. Ledger-Einträge tragen `channel`. 4. **`wikitool upload collect [<kanal>...]`** (schreibend; `cli_contract`-Eintrag in der Gruppe „Raw material and uploads“; Code in `chemenu/collect.py`): - **Kanäle:** Ohne Argument werden alle Pull-Kanäle abgeholt. Ein unbekannter Name oder ein `mcp`-Kanal als Argument ist Exit 1, bevor etwas geschieht. - **Unabhängigkeit:** Jeder Kanal wird unabhängig abgeholt. Scheitert einer, laufen die anderen trotzdem. Exit 1, wenn mindestens einer gescheitert ist, sonst Exit 0. - **Ausgabe:** je Kanal übernommen (mit id), übersprungen (mit Grund) und gescheitert. - **Dubletten:** Eine Datei, deren sha256 schon als wartende Einreichung existiert, wird nicht ein zweites Mal angelegt, sondern als Dublette (mit der id) gemeldet. Bei `folder` wird sie an der Quelle trotzdem entfernt, weil der Inhalt in der Quarantäne gesichert ist. 5. **`doctor` `upload-intake`:** Gemeldet werden die Kanäle mit ihrem Handler, die Zahl der wartenden Einreichungen und das alte `mcp-upload/` (`FAIL`). Bei `folder` gibt es `WARN`, wenn `path` nicht existiert. 6. **`config migrate`:** Das Ziel von `.wikitool-upload.json` ist `intake.channels.mcp`. 7. **`wiki-ingest`** Schritt 1 nach Entscheidung 8. `instructions/ingest-queue.md` nennt `collect` und die Herkunftsfelder des Manifests. Bei `submitter: unknown` gibt es keinen Absender, dem man trauen könnte. 8. **How-to `docs/how-to/collect-from-a-synced-folder.md`:** - Kanal `folder` von Hand anlegen, - was liegen bleibt und warum, - `settle_seconds`, - die Grenze aus Entscheidung 7, seit `f2f251f` mit dem in der Prüfung beobachteten Verhalten von OneDrive, - dass Löschen in OneDrive auf allen Geräten wirkt (Papierkorb), - Kanal `command` mit einem Beispielskript. ## Was ein Abholen garantiert - **Gelöscht wird an der Quelle nur nach gesicherter Kopie (`folder`).** Die Quelldatei wird erst entfernt, wenn ihre Kopie in der Quarantäne vollständig geschrieben ist (atomar über `_write_atomic_within`) **und** Größe und sha256 der Quelle nach dem Kopieren noch denen der Kopie entsprechen. Andernfalls bleibt die Quelle unverändert, und die halbe Einreichung wird entfernt. - **Außerhalb der Quarantäne verändert `collect` nur eins:** Es entfernt übernommene Dateien direkt in `path`. Es schreibt nie dorthin, löscht nie ein Verzeichnis und verfolgt keinen Symlink. Ein Symlink in `path` wird übersprungen und gemeldet. - **Fremdes bleibt unberührt und wird gemeldet:** Unterverzeichnisse, versteckte Dateien und nicht erlaubte Endungen. „Versteckt“ heißt: Der Name beginnt mit `.` oder `~$`, oder unter Windows ist das Attribut hidden oder system gesetzt. Ebenso bleibt eine Datei über `max_bytes` liegen. - **Was eine Datei in der Quarantäne heißt, bestimmt `sanitize_filename`.** Ein Name, den es ablehnt, bleibt an der Quelle und wird gemeldet. - **Nichts aus einem Kanal erreicht `incoming/` oder `raw/`** außer über `upload accept`. ## Akzeptanzkriterien Tests in `test_upload.py`/`test_upload_cmd.py`/`test_mcp_server.py`/`test_config_shapes.py`/`test_legacy_settings.py`, soweit nicht anders genannt. Die Tests des Abholens (`folder`, `command`, `upload collect`, `.gitignore` der Quarantäne) stehen in `test_collect.py`. - [x] **Leer abgeholt:** Nach `upload collect` gegen einen `folder`-Kanal mit zwei erlaubten Dateien liegen in `quarantine/` zwei Einreichungen, deren sha256 den Quelldateien entspricht. Der Quellordner ist danach leer. Die Manifeste tragen `channel`, `handler: folder`, `origin` und `submitter: unknown`. - [x] **Unberührt geblieben:** Ein Unterverzeichnis, eine Datei `.x.pdf`, eine Datei `~$a.docx`, eine Datei mit nicht erlaubter Endung, eine Datei über `max_bytes` und ein Symlink im Quellordner sind nach `collect` byte-gleich vorhanden und erscheinen in der Ausgabe als übersprungen. - [x] **`settle_seconds`:** Eine Datei mit mtime jünger als `settle_seconds` bleibt byte-gleich liegen und wird nicht eingereicht. - [x] **Änderung beim Kopieren:** Ändert sich die Quelle zwischen Lesen und Löschprüfung (Test über Monkeypatch von `collect.source_matches`), bleibt die Quelle byte-gleich, und in `quarantine/` entsteht keine Einreichung. - [x] **Dublette:** Eine Datei, deren sha256 schon wartet, erzeugt keine zweite Einreichung und wird mit der vorhandenen id gemeldet. - [x] **`command`, Erfolg:** Ein Programm, das zwei Dateien ins Staging-Verzeichnis legt und mit Exit 0 endet, ergibt zwei Einreichungen mit `origin: command:<name>`. - [x] **`command`, Misserfolg:** Exit ≠ 0 bzw. Timeout ergibt null Einreichungen. `quarantine/.staging/` ist danach leer bzw. fehlt, und die Ausgabe enthält das Ende von stderr. - [x] **Mehrere Kanäle:** Scheitert ein Kanal, werden die anderen trotzdem abgeholt, und der Exit-Code ist 1. Ohne Pull-Kanal endet `collect` mit Exit 0 und „nothing to collect“. Ein unbekannter Kanalname oder ein `mcp`-Kanal als Argument ergibt Exit 1 ohne Wirkung. - [x] **`.gitignore` der Quarantäne:** Nach der ersten Einreichung (über `submit` und über `collect`) existiert `quarantine/.gitignore` mit dem Inhalt `*`. In einem Test-Repo **ohne** Eintrag in der Wurzel-`.gitignore` zeigt `git status --porcelain --untracked-files=all` nichts unter `quarantine/`. - [x] **Altes Verzeichnis:** Mit einem `mcp-upload/` mit Inhalt enden `upload list/show/accept/reject/collect` mit Exit 1 und nennen den Verschiebebefehl. `doctor` meldet `upload-intake` `FAIL`, und `mcp-upload/` ist danach byte-gleich. - [x] **Konfiguration:** Zwei `mcp`-Kanäle, ein Feld, das nicht zum Handler passt, ein `command`-Verweis ohne Eintrag, ein relativer `path` und ein `path` innerhalb der Instanz sind jeweils ein Fehler der Sektion. `config set intake.…` endet mit Exit 42. - [x] **MCP:** Mit `mcp`-Kanal registriert der Server `submit` mit dessen Grenzen. Ohne `mcp`-Kanal, aber mit `folder`-Kanal, ist `submit` nicht registriert. Eine `submit`-Einreichung trägt `channel: mcp`. Die bestehenden Tests zu Quote und Identität bleiben grün. - [x] **Migration der Einstellungen:** `config migrate` mit einer `.wikitool-upload.json` (verschachtelte `quota`) ergibt `intake.channels.mcp` mit `handler: mcp` und den flachen `quota_*`-Feldern, und es entsteht keine Sektion `upload`. - [x] **Sicherung:** Nach `backup run` liegt im Ziel keine Datei aus `quarantine/` (`test_backup.py`). - [x] **Doku:** Das How-to existiert und steht in `docs/how-to/README.md`. `raw/CONTRACT.md`, `instructions/gates.md`, `instructions/ingest-queue.md`, `instructions/wiki-ingest/SKILL.md`, `instructions/mcp-read-server.md`, `docs/how-to/set-up-the-mcp-server.md`, `back-up-to-a-synced-folder.md` und die Migration nennen `quarantine/` und `intake`. Außerhalb von `CHANGES.md`, der Migrationsanleitung und der Erkennung des alten Verzeichnisses nennt kein Dokument `mcp-upload/` mehr als aktuellen Ort (`grep`). - Beim Abschluss nachgezogen (`ecefe0a`): `AGENTS.md` § Gates (das Upload Review Gate gilt für jeden Kanal), `instructions/mcp-read-server.md` (Sektion `intake` statt `upload`), der Einleitungssatz in `docs/reference/configuration.md` und der Docstring von `doctor`. - [x] **Prüfläufe:** - `cceb9a7`: lokal `pytest` (2997 bestanden, 4 übersprungen), `docs verify` und `instructions verify` grün; CI-Lauf 678 (Jobs `verify` und `pwsh`) und Lauf 679 `success`. - `ecefe0a`: lokal dieselben drei Prüfungen grün (2997 bestanden, 4 übersprungen); CI-Läufe 680 und 681 `success`. - `f2f251f` (How-to, Ergebnis von Entscheidung 7): `docs verify` lokal grün, CI-Lauf 691 `success`. - `docs contract`/`docs reference` sind neu erzeugt. Die Sektion `intake` steht in `docs/reference/configuration.md`, die Sektion `upload` nicht mehr. - [x] **Von Hand auf dem Windows-Arbeitsrechner** (Betreiber, 2026-10-13, Release 10.0.0, GitHub Copilot CLI): - `collect` gegen einen Ordner unter `OneDrive - <Firma>` mit vom Handy hochgeladenen Dateien: übernommen in die Quarantäne, Quelle danach entfernt, Freigabe über `upload accept` sauber. `doctor` meldet `upload-intake` mit dem Kanal `onedrive (folder)`. - Die beiden offenen Punkte aus Entscheidung 7 sind beantwortet und dort eingetragen: Die ursprünglichen Zeiten bleiben erhalten, und ein Platzhalter wird beim Lesen heruntergeladen. - `wiki-ingest` ohne Argument holt aus dem Ordner in die Quarantäne, zeigt die wartenden Einreichungen und lässt die Freigabe beim Betreiber. Nebenbefund: Danach arbeitet es mehrere wartende Dateien in einem Kontext ab → #219. ## Versionsteil - **`10.0.0-beta.11`: MAJOR auf dem laufenden Kandidaten**, `--impact high`. - `--breaking "Die Quarantäne heißt quarantine/ statt mcp-upload/. Eine Instanz mit wartenden Einreichungen verschiebt das Verzeichnis einmal von Hand (Migrationsschritt); bis dahin enden die upload-Kommandos mit Exit 1."` - Die Migration bleibt `required` (`instructions/migrations/10.0.0-settings-tiers.md`, ergänzt um den Schritt). - **`10.0.0-beta.12`: PATCH für den Nachzug beim Abschluss**, `--impact low`. - Der How-to-Nachtrag `f2f251f` ist reine `docs/`-Prosa außerhalb des Version-Gates, ohne Bump. Dass `upload` durch `intake` ersetzt wird, ist gegenüber 9.0.0 kein eigener Bruch, denn der Weg aus `.wikitool-upload.json` endet nur an anderer Stelle. ## Betroffene Dateien und Flächen - **Code:** `tools/chemenu/upload.py`, `tools/chemenu/collect.py` (neu, damit der MCP-Serverprozess Abholen, Löschen und Programmstart nicht importieren kann), `tools/chemenu/commands/upload_cmd.py` (neu: `collect`), `tools/chemenu/mcp/server.py`, `tools/chemenu/config_shapes.py` (`UPLOAD` → `INTAKE`), `tools/chemenu/legacy_settings.py`, `tools/chemenu/config.py` (`UPLOAD_DIR`), `tools/chemenu/commands/doctor.py` (`check_upload_intake`), `tools/chemenu/commands/raw/pending.py`, `tools/chemenu/commands/backup_cmd.py`, `tools/chemenu/commands/docs_verify.py`, `tools/chemenu/reference_doc.py` (Region der Sektion), `tools/chemenu/cli_contract.py` (Gruppe). - **Konfiguration:** `.gitignore`. - **Steuerebene und Instructions:** `AGENTS.md` § Gates, `raw/CONTRACT.md` § Getting a file in from outside, `instructions/gates.md`, `instructions/ingest-queue.md`, `instructions/wiki-ingest/SKILL.md`, `instructions/mcp-read-server.md`, `instructions/migrations/10.0.0-settings-tiers.md`. - **Docs:** `docs/reference/configuration.md`, `docs/how-to/set-up-the-mcp-server.md`, `docs/how-to/back-up-to-a-synced-folder.md`, `docs/how-to/ingest-a-source.md`, `docs/how-to/collect-from-a-synced-folder.md` (neu; Nachtrag `f2f251f`), `docs/how-to/README.md`, `docs/explanation/architecture.md`, `docs/explanation/pipeline-rationale.md`, `tools/README.md`. - **Generiert:** `tools/CONTRACT.md`, `docs/reference/commands/{README,git,instance-health,raw-material-and-uploads}.md`, die Region in `docs/reference/configuration.md`. - **Tests:** `test_upload.py`, `test_upload_cmd.py`, `test_mcp_server.py`, `test_config_shapes.py`, `test_legacy_settings.py`, `test_backup.py`, `test_doctor.py`, `test_collect.py` (neu, 22 Tests). - `CHANGES.md` über `version bump`. ## Kette Kette „OneDrive/Microsoft 365“ - Vorgänger: #186 - Nachfolger: #214
torben added the prio/plannedsize/Larea/kbkind/decision labels 2026-10-12 16:28:27 +00:00
Author
Owner

Changelog: Body gegen den Baum geprüft. Neuer Abschnitt „Befunde aus dem Baum“:

  • Die Sektion upload gibt es erst im unveröffentlichten Kandidaten 10.0.0.
  • Der Kandidat hat die Grenze schon überschritten.
  • Ein neuer Verzeichnisname reißt eine .gitignore-Lücke.
  • Die Fundstellen von mcp-upload sind aufgelistet.

Entscheidung 5 ist gestrichen, weil ihre Prämisse nicht trägt, und durch die offene Frage E6 ersetzt. Die offenen Fragen heißen jetzt E1–E6, jede mit einer Empfehlung. E3 ist als nicht blockierende Regel formuliert. Neue Festlegungen in Konfiguration und Garantien:

  • höchstens ein mcp-Kanal
  • Kanalnamen [a-z0-9-]+
  • path absolut und außerhalb der Instanz
  • was „versteckt“ heißt

Der Versionsteil ist an E1/E6 gekoppelt, die Liste der betroffenen Dateien ist vervollständigt.

**Changelog:** Body gegen den Baum geprüft. Neuer Abschnitt „Befunde aus dem Baum“: - Die Sektion `upload` gibt es erst im unveröffentlichten Kandidaten 10.0.0. - Der Kandidat hat die Grenze schon überschritten. - Ein neuer Verzeichnisname reißt eine `.gitignore`-Lücke. - Die Fundstellen von `mcp-upload` sind aufgelistet. Entscheidung 5 ist gestrichen, weil ihre Prämisse nicht trägt, und durch die offene Frage E6 ersetzt. Die offenen Fragen heißen jetzt E1–E6, jede mit einer Empfehlung. E3 ist als nicht blockierende Regel formuliert. Neue Festlegungen in Konfiguration und Garantien: - höchstens ein `mcp`-Kanal - Kanalnamen `[a-z0-9-]+` - `path` absolut und außerhalb der Instanz - was „versteckt“ heißt Der Versionsteil ist an E1/E6 gekoppelt, die Liste der betroffenen Dateien ist vervollständigt.
torben added kind/build and removed kind/decision labels 2026-10-12 19:33:47 +00:00
Author
Owner

Changelog: Der Betreiber hat E1–E6 wie empfohlen entschieden (2026-10-12, „alles ok“). Sie stehen jetzt als Entscheidungen 5–10 im Body.

Neu im Body:

  • Was gebaut wird: INTAKE-Shape, die Umbenennung in quarantine/ mit eigener .gitignore und Erkennung des alten Verzeichnisses, Herkunftsfelder im Manifest, upload collect, doctor, config migrate, wiki-ingest, How-to.
  • Ablauf des command-Handlers: nach dem Muster von run_fetcher, mit 300 s Timeout.
  • Akzeptanzkriterien als prüfbare Eigenschaften.
  • Versionsteil: festgelegt (MAJOR auf dem Kandidaten 10.0.0 mit einem weiteren --breaking, Migrationsschritt).

Labels: kind/decision → kind/build, weil keine Betreiberentscheidung mehr offen ist. size/L bleibt, denn das Paket braucht mehrere Sitzungen.

Folge für das Release: 10.0.0 wird erst nach diesem Paket veröffentlicht.

**Changelog:** Der Betreiber hat E1–E6 wie empfohlen entschieden (2026-10-12, „alles ok“). Sie stehen jetzt als Entscheidungen 5–10 im Body. Neu im Body: - **Was gebaut wird:** `INTAKE`-Shape, die Umbenennung in `quarantine/` mit eigener `.gitignore` und Erkennung des alten Verzeichnisses, Herkunftsfelder im Manifest, `upload collect`, `doctor`, `config migrate`, `wiki-ingest`, How-to. - **Ablauf des `command`-Handlers:** nach dem Muster von `run_fetcher`, mit 300 s Timeout. - **Akzeptanzkriterien** als prüfbare Eigenschaften. - **Versionsteil:** festgelegt (MAJOR auf dem Kandidaten 10.0.0 mit einem weiteren `--breaking`, Migrationsschritt). **Labels:** `kind/decision` → `kind/build`, weil keine Betreiberentscheidung mehr offen ist. `size/L` bleibt, denn das Paket braucht mehrere Sitzungen. **Folge für das Release:** 10.0.0 wird erst nach diesem Paket veröffentlicht.
Author
Owner

Gebaut und veröffentlicht als 10.0.0-beta.11 (Commit cceb9a7), CI grün (Läufe 678 und 679).

  • Mehrere Eingangskanäle: Die neue Sektion intake in .wikitool-local.json ersetzt upload. Kanäle haben die Handler mcp, folder und command.
  • Quarantäne: Alle Kanäle münden in quarantine/ (vorher mcp-upload/). Das Verzeichnis trägt eine eigene .gitignore. Ein noch vorhandenes mcp-upload/ führt zu Exit 1 in den upload-Kommandos und zu FAIL in doctor. Der Migrationsschritt dafür steht in instructions/migrations/10.0.0-settings-tiers.md.
  • wikitool upload collect [<kanal>...]: Holt aus den Pull-Kanälen folder und command in die Quarantäne. Die Quelldatei wird erst nach gesicherter Kopie entfernt. Fremdes bleibt liegen und wird gemeldet. Ein scheiternder Kanal stoppt die anderen nicht.
  • Manifeste tragen channel, handler und origin.
  • wiki-ingest holt ohne Argument zuerst ab und meldet wartende Einreichungen. upload accept und das Gate bleiben beim Menschen.
  • config migrate schreibt .wikitool-upload.json direkt nach intake.channels.mcp.
  • Doku: neues How-to docs/how-to/collect-from-a-synced-folder.md, Referenz und Verträge nachgezogen.
  • Version: MAJOR auf dem Kandidaten 10.0.0 mit --breaking (Verzeichnisname).

Offen: die Prüfung von Hand auf dem Windows-Arbeitsrechner (OneDrive-Ordner, die beiden unbelegten Punkte aus Entscheidung 7, wiki-ingest ohne Argument).

Gebaut und veröffentlicht als `10.0.0-beta.11` (Commit `cceb9a7`), CI grün (Läufe 678 und 679). - **Mehrere Eingangskanäle:** Die neue Sektion `intake` in `.wikitool-local.json` ersetzt `upload`. Kanäle haben die Handler `mcp`, `folder` und `command`. - **Quarantäne:** Alle Kanäle münden in `quarantine/` (vorher `mcp-upload/`). Das Verzeichnis trägt eine eigene `.gitignore`. Ein noch vorhandenes `mcp-upload/` führt zu Exit 1 in den `upload`-Kommandos und zu `FAIL` in `doctor`. Der Migrationsschritt dafür steht in `instructions/migrations/10.0.0-settings-tiers.md`. - **`wikitool upload collect [<kanal>...]`:** Holt aus den Pull-Kanälen `folder` und `command` in die Quarantäne. Die Quelldatei wird erst nach gesicherter Kopie entfernt. Fremdes bleibt liegen und wird gemeldet. Ein scheiternder Kanal stoppt die anderen nicht. - **Manifeste** tragen `channel`, `handler` und `origin`. - **`wiki-ingest`** holt ohne Argument zuerst ab und meldet wartende Einreichungen. `upload accept` und das Gate bleiben beim Menschen. - **`config migrate`** schreibt `.wikitool-upload.json` direkt nach `intake.channels.mcp`. - **Doku:** neues How-to `docs/how-to/collect-from-a-synced-folder.md`, Referenz und Verträge nachgezogen. - **Version:** MAJOR auf dem Kandidaten 10.0.0 mit `--breaking` (Verzeichnisname). Offen: die Prüfung von Hand auf dem Windows-Arbeitsrechner (OneDrive-Ordner, die beiden unbelegten Punkte aus Entscheidung 7, `wiki-ingest` ohne Argument).
Author
Owner

Changelog:

  • Abschlussprüfung: Vier Stellen beschrieben noch submit als einzigen Kanal bzw. die Sektion upload: AGENTS.md § Gates, instructions/mcp-read-server.md, die Einleitung von docs/reference/configuration.md und der Docstring von doctor. Sie sind als 10.0.0-beta.12 nachgezogen (Commit ecefe0a), CI-Läufe 680 und 681 grün.
  • Body:
    • „Anlass“ und „Befunde“ beschreiben jetzt den Stand vor dem Bau.
    • „Was gebaut wird“ heißt jetzt „Was gebaut wurde“.
    • E6 ist als erfüllt markiert.
    • Prüfläufe und Versionsteil nennen beide Commits.
  • Betreiberentscheidung: #213 bleibt offen bis zur Prüfung von Hand auf Windows. Ihr Ergebnis kommt in E3 und ins letzte Kriterium, danach /stack-close.
**Changelog:** - **Abschlussprüfung:** Vier Stellen beschrieben noch `submit` als einzigen Kanal bzw. die Sektion `upload`: `AGENTS.md` § Gates, `instructions/mcp-read-server.md`, die Einleitung von `docs/reference/configuration.md` und der Docstring von `doctor`. Sie sind als `10.0.0-beta.12` nachgezogen (Commit `ecefe0a`), CI-Läufe 680 und 681 grün. - **Body:** - „Anlass“ und „Befunde“ beschreiben jetzt den Stand vor dem Bau. - „Was gebaut wird“ heißt jetzt „Was gebaut wurde“. - E6 ist als erfüllt markiert. - Prüfläufe und Versionsteil nennen beide Commits. - **Betreiberentscheidung:** #213 bleibt offen bis zur Prüfung von Hand auf Windows. Ihr Ergebnis kommt in E3 und ins letzte Kriterium, danach `/stack-close`.
Author
Owner

Changelog: Abschluss nach der Prüfung von Hand (2026-10-13, Release 10.0.0, Windows mit OneDrive for Business). Das letzte Kriterium ist abgehakt. Entscheidung 7 ist belegt: OneDrive behält die ursprünglichen Zeiten, und ein Platzhalter wird beim Lesen heruntergeladen. Bei OneDrive schützt also der Vergleich von Größe und sha256, nicht settle_seconds. Das How-to ist in f2f251f nachgezogen, CI-Lauf 691 grün. Folgepakete: #216 und #219. Changelog-Zeile: Eingangskanäle (mcp, folder, command) münden in quarantine/; upload collect holt aus Sync-Ordner und Programm ab, mit 10.0.0 ausgeliefert und auf Windows mit OneDrive bestätigt.

**Changelog:** Abschluss nach der Prüfung von Hand (2026-10-13, Release 10.0.0, Windows mit OneDrive for Business). Das letzte Kriterium ist abgehakt. Entscheidung 7 ist belegt: OneDrive behält die ursprünglichen Zeiten, und ein Platzhalter wird beim Lesen heruntergeladen. Bei OneDrive schützt also der Vergleich von Größe und sha256, nicht `settle_seconds`. Das How-to ist in `f2f251f` nachgezogen, CI-Lauf 691 grün. Folgepakete: #216 und #219. Changelog-Zeile: Eingangskanäle (`mcp`, `folder`, `command`) münden in `quarantine/`; `upload collect` holt aus Sync-Ordner und Programm ab, mit 10.0.0 ausgeliefert und auf Windows mit OneDrive bestätigt.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#213