intake: several configured intake channels (mcp, folder, command) all end in quarantine/ (was mcp-upload/), and upload collect pulls the synced-folder and command channels into it (#213); 10.0.0-beta.11
CI / verify (push) Successful in 8m41s
CI / pwsh (push) Successful in 2m44s
Release / release (push) Successful in 35s
Release / wiki-mirror (push) Skipped

Files changed:
- .gitignore
- CHANGES.md
- VERSION
- docs/explanation/architecture.md
- docs/explanation/pipeline-rationale.md
- docs/how-to/README.md
- docs/how-to/back-up-to-a-synced-folder.md
- docs/how-to/collect-from-a-synced-folder.md
- docs/how-to/ingest-a-source.md
- docs/how-to/set-up-the-mcp-server.md
- docs/reference/commands/README.md
- docs/reference/commands/git.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/raw-material-and-uploads.md
- docs/reference/configuration.md
- instructions/gates.md
- instructions/ingest-queue.md
- instructions/mcp-read-server.md
- instructions/migrations/10.0.0-settings-tiers.md
- instructions/wiki-ingest/SKILL.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli_contract.py
- tools/chemenu/collect.py
- tools/chemenu/commands/backup_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/raw/pending.py
- tools/chemenu/commands/upload_cmd.py
- tools/chemenu/config.py
- tools/chemenu/config_shapes.py
- tools/chemenu/legacy_settings.py
- tools/chemenu/mcp/server.py
- tools/chemenu/reference_doc.py
- tools/chemenu/tests/test_backup.py
- tools/chemenu/tests/test_collect.py
- tools/chemenu/tests/test_config_shapes.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_legacy_settings.py
- tools/chemenu/tests/test_mcp_server.py
- tools/chemenu/tests/test_upload.py
- tools/chemenu/tests/test_upload_cmd.py
- tools/chemenu/upload.py

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Code committed 2026-10-12 22:04:18 +02:00
1 parent 3627168403
commit cceb9a7bfe
44 files changed
+1910 -346

No files matched your search

+2
View File
@@ -30,6 +30,8 @@ Running the instance:
- [Trace and score agent sessions](trace-and-score-sessions.md)
- [Use 1Password for settings](use-1password-for-settings.md) - secrets and private values out
of the tree, on the desktop and in CI
- [Collect documents from a synced folder](collect-from-a-synced-folder.md) - a folder OneDrive and
the like fill, or a program of your own, as an intake channel into the quarantine
- [Back up to a synced folder](back-up-to-a-synced-folder.md) - OneDrive and the like as the
backup of an instance without a git server, and restoring from it
+1 -1
View File
@@ -100,7 +100,7 @@ push.
| `ENVIRONMENT.md` | yes, if present | written by hand |
| Everything in `incoming/` but `.gitkeep` | yes | sources waiting there are backed up nowhere else |
| The `extra` paths | yes | what you named |
| `mcp-upload/` | no | material submitted from outside that nobody has reviewed yet |
| `quarantine/` | no | material that arrived through an intake channel and that nobody has reviewed yet |
| `reports/` | no | regenerated by `lint`; telemetry holds prompts |
| `.wikitool-tools.json`, `tools/.venv/`, the published skills | no | the preflight and `instructions sync` rebuild them |
| The secrets file | **never** | it lies outside the instance, and a backup reads nothing outside it |
+133
View File
@@ -0,0 +1,133 @@
# Collect documents from a synced folder
Documents that are not typed or fetched at this terminal can reach the wiki through an intake
channel. Besides the MCP `submit` tool there are two channels that this checkout *pulls* from: a
folder that a sync client such as OneDrive fills (handler `folder`), and a program of your own
(handler `command`). Both end in the quarantine `quarantine/`, exactly like a submission through the
server, and both wait there for a human to run `wikitool upload accept` - the Upload Review Gate
stands between every channel and `incoming/`.
This guide creates a `folder` channel, explains what `upload collect` leaves alone and why, and shows
a `command` channel with an example script. The field reference is in the
[configuration reference](../reference/configuration.md#intake-channels); the review is in
[ingesting a source](ingest-a-source.md#a-document-submitted-from-outside).
<!-- wikitool:toc -->
## Contents
- [Create the folder channel](#create-the-folder-channel)
- [What stays in the folder, and why](#what-stays-in-the-folder-and-why)
- [`settle_seconds` and what it cannot promise](#settle_seconds-and-what-it-cannot-promise)
- [Deleting in a synced folder deletes everywhere](#deleting-in-a-synced-folder-deletes-everywhere)
- [A command channel](#a-command-channel)
- [Afterwards](#afterwards)
<!-- /wikitool:toc -->
## Create the folder channel
Channels are not written by `config set`: every field of the `intake` section opens a path into the
checkout, so it is yours to edit by hand in `.wikitool-local.json`.
```json
{
"intake": {
"channels": {
"onedrive": {
"handler": "folder",
"path": "/home/you/OneDrive/wiki-inbox",
"settle_seconds": 60,
"max_bytes": 10485760,
"allowed_extensions": [".pdf", ".docx", ".md", ".txt"]
}
}
}
}
```
`path` has to be absolute and lie outside the instance. Check it with `tools/wikitool doctor`, then
pull:
```bash
tools/wikitool upload collect # every pull channel
tools/wikitool upload collect onedrive # only this one
tools/wikitool upload list
```
A collected file is stored unchanged in `quarantine/<id>/` with a manifest that names the channel,
the handler and the original path. A folder knows nobody who dropped a file, so the submitter is
recorded as `unknown`, with the handler as its source: do not trust the name or content of such a
file any more than that of a stranger's.
## What stays in the folder, and why
`upload collect` reads the top level of the folder only, and it removes one kind of thing: a file it
has just stored. Everything else stays where it is and is reported with its reason:
| Entry | Why it stays |
|---|---|
| A subdirectory | Nothing descends into it, and nothing deletes a directory. |
| A name starting with `.` or `~$`, a hidden or system file | Editors and sync clients keep temporary files like these next to the real one. |
| A symbolic link | Following it could leave the folder. |
| An extension outside `allowed_extensions`, or a file above `max_bytes` | The same limits as for `submit`. |
| A file younger than `settle_seconds` | It may still be arriving. |
| A file that changed while it was being read or copied | The source is removed only if it still has the size and sha256 that were stored. |
A file whose sha256 already waits in the quarantine is reported as a duplicate, with the id of the
existing submission, and is removed from the folder.
## `settle_seconds` and what it cannot promise
A sync client writes a file in pieces, so a file that is half there looks like a complete, smaller
one. `settle_seconds` (default 60) skips files whose modification or creation time is younger than
that, and the check that the source did not change during the copy catches the rest. Neither can see
a client that pauses for longer than the interval in the middle of a file and then resumes: such a
file is stored incomplete, and the review is where you notice it. Raise the value if your client
works over a slow link.
## Deleting in a synced folder deletes everywhere
A file removed from a folder that OneDrive syncs is removed on every device that syncs it. That is
what a collected file should do - it is now in the quarantine - but it also means a collect on one
machine empties the folder for all of them. The client's recycle bin keeps the file for a while; the
quarantine copy, which is never synced, is the one to rely on. Collect from one machine only.
## A command channel
A `command` channel runs a program of your own, which fetches documents from somewhere that has no
synced folder - a mailbox, a share, a scanner. The program is named in `commands` and referenced from
the channel; the section stays valid only if every reference resolves.
```json
{
"intake": {
"commands": {
"scan-mail": ["/home/you/bin/fetch-attachments.sh"]
},
"channels": {
"mail": { "handler": "command", "command": "scan-mail" }
}
}
}
```
The program is started without a shell and with stdin closed, with an empty staging directory under
`quarantine/.staging/` appended as its last argument. It has 300 seconds. It leaves the files it
wants to hand over directly in that directory:
```bash
#!/bin/sh
# fetch-attachments.sh <staging-dir>
set -eu
out="$1"
rclone copy remote:wiki-inbox "$out" --max-age 1d
```
A program that exits non-zero, does not start or times out fails its channel: nothing is taken, the
staging directory is removed, and the last 2000 characters of its stderr are shown. Other channels
still run, and `upload collect` exits 1 if any channel failed. With no pull channel configured it
exits 0 with "nothing to collect".
## Afterwards
The agent does the same at the start of an ingest when you name no file: `upload collect`, then
`upload list`, then a report of what waits. It never runs `upload accept` for you.
+4 -1
View File
@@ -93,11 +93,14 @@ Every ten sources, the agent suggests a lint run - see [Maintain the wiki](maint
## A document submitted from outside
When the [MCP read server](set-up-the-mcp-server.md) has its optional `submit` tool turned on,
documents can arrive from another machine. They land in the quarantine `mcp-upload/`, not in
documents can arrive from another machine. A folder a sync client fills and a program of the
checkout are two more ways in ([Collect documents from a synced
folder](collect-from-a-synced-folder.md)). All of them land in the quarantine `quarantine/`, not in
`incoming/`, and nothing else reads them there. Before an ingest can start, a human has to promote
the submission:
```bash
tools/wikitool upload collect # pull the folder and command channels into the quarantine
tools/wikitool upload list # what is waiting
tools/wikitool upload show <id> # one submission's manifest: who sent it, size, checksum
```
+9 -8
View File
@@ -160,18 +160,19 @@ reviewed yet.
## Optional: the submit path
Without this, `submit` does not exist as a tool - not unused, but not registered. An
`upload` section in the served checkout's `.wikitool-local.json` turns it on. Its fields, defaults and what a malformed
section does are in the [configuration reference](../reference/configuration.md#mcp-submissions); a
broken section never means "no limits", it stops the server from starting.
Without this, `submit` does not exist as a tool - not unused, but not registered. A
channel with the handler `mcp` in the `intake` section of the served checkout's
`.wikitool-local.json` turns it on. Its fields, defaults and what a malformed section does are in
the [configuration reference](../reference/configuration.md#intake-channels); a broken section never
means "no limits", it stops the server from starting.
**With `submit` on, the middleware has a second duty.** It has to set the identity header itself, on
every authenticated request, and discard any copy the client sent. The server trusts that header as
the value for `submitter`. A header the client could set itself would be a claim, not an identity.
If `identity_header` names a header the middleware does not set, every submission is refused for
If the channel's `identity_header` names a header the middleware does not set, every submission is refused for
lack of an identity.
Submitted files land in `mcp-upload/<id>/`, which is gitignored and read by no other command. From
Submitted files land in `quarantine/<id>/`, which is gitignored and read by no other command. From
there a human promotes or rejects them, in the same checkout as the server rather than in its
process. That review is part of
[ingesting a source](ingest-a-source.md#a-document-submitted-from-outside).
@@ -228,7 +229,7 @@ the client starts a different Python than the instance's. Give the client the ab
**`"commit": null` in every answer**: the served tree has uncommitted changes. Either the sync is not
running, or something writes into the corpus that has no business there. The five read tools never
write. `submit`, when it is on, writes only to `mcp-upload/`, which is gitignored and so is never a
write. `submit`, when it is on, writes only to `quarantine/`, which is gitignored and so is never a
cause of `"commit": null` either.
**`commit` names an old revision**: the sync timer is not running.
@@ -246,6 +247,6 @@ Deployment specifics - the cluster, ingress hosts, the secret store, the GitOps
private infrastructure, and this repository is public.
There is no container image for the server yet. Until there is, the path above - the venv,
`python -m chemenu.mcp`, a proxy in front - is the complete one. With `submit` on, `mcp-upload/`
`python -m chemenu.mcp`, a proxy in front - is the complete one. With `submit` on, `quarantine/`
has to survive restarts the same way the rest of the checkout does. Otherwise a submitted file that
nobody has reviewed yet loses its quarantine.