Releases: HarperZ9/gather
Release list
v2.1.0
Adds local client packages with explicit launch permissions. Client-specific installation
and marketplace acceptance remain separate qualification steps.
- Add a local MCP profile with an explicit readable workspace, typed permission refusals, and process/network denial at launch. The full CLI/MCP remains available separately.
- Add optional exact-origin HTTP retrieval through launch flags, with a separate literal-loopback grant, bounded response size, pinned connection addresses and existing fetch receipts. Refuse redirects, ambient proxies, credentials and process execution.
- Add optional MCPB setup fields for public and local-service origin lists. Empty settings grant no access; malformed settings stop launch before serving tools.
- Add portable source plugins for Claude/Codex-compatible clients and Windows x64 ZIP/MCPB packages containing the runtime, licenses, source hashes and checksums.
- Check actual stdio behavior before packaging, reject untracked or credential-like release payloads, and attach checked client assets to the same product release.
- Keep process-backed intake and persistent-state operations on the full CLI/MCP. Local document reads and explicitly allowed HTTP retrieval are available in the client profile.
v2.0.0
Breaking changes
This release removes public names, so it is a major version. It also changes what video
intake does when YouTube asks for a bot check. What changed, and what to do:
- The
stealthextra is gone.pip install 'gather-engine[stealth]'still installs Gather;
pip warns that the extra does not exist and installs the core. Drop[stealth]from
requirement files and install commands.gather-engine[all]now installslxmland
playwrightonly. - The module
gather.backends_stealthis gone, and importing it raisesImportError.
Remove the import. Nothing replaces it. - The constant
gather.backends.CAP_STEALTHis gone, and importing it raisesImportError.
gather capsno longer listsstealth, even wherecurl_cffiis installed. Remove any
reference to it. - A YouTube bot check ("confirm you're not a bot") is never retried. Builds from main after
1.9.1 retried it with backoff like HTTP 429; 1.9.1 did not retry it either. Gather records
the failure with the codebot-check, and the record starts "YouTube asked for a bot
check, and gather stopped", then gives yt-dlp's own line.gather channelstops the pass
at the first bot check, whatever--max-throttledsays, and exits 1. It settles the entry
that met the check, so a resumed run does not ask for it again. A bot check while listing
a tab stops the run before any entry is gathered. What to do: when you choose to try a
video again, ask for it by name withgather video URL --store DIRand the pass's flags.
--retriesand the backoff flags now cover HTTP 429 and YouTube's session rate limit
only.gather.ytdlp.THROTTLE_CODESno longer holdsbot-check;TERMINAL_CODESdoes.
Removed: the stealth extra and its backend
- The
stealthextra, itscurl_cffidependency, and thestealthcapability backend
(gather.backends_stealth) are removed. That backend impersonated a browser's TLS
fingerprint to get past bot detection. Gather no longer ships bot-detection evasion of any
kind, and nothing replaces it. Gather's own HTTP requests go out with Gather's own
User-Agent. pip install 'gather-engine[stealth]'still installs Gather. pip warns
gather-engine 2.0.0 does not provide the extra 'stealth'and installs the core
without it. Drop[stealth]from requirement files and install commands.gather-engine[all]now installslxmlandplaywrightonly. An upgrade leaves an
installedcurl_cffiin place. Gather's own code no longer uses it, but a yt-dlp in
the same environment can (see below), so runpip uninstall curl_cffiif nothing else
needs it.- Caption downloads no longer pose as a browser. yt-dlp marks every YouTube caption track
for impersonation and keeps the mark in the info JSON Gather saves and hands back for the
caption download. Where yt-dlp could importcurl_cffi, the track went out with a
browser's TLS fingerprint and headers. Gather now removes the mark first. A test on the
saved info runs everywhere, and a test against real yt-dlp, run where yt-dlp is
installed, checks that the caption request carries yt-dlp's own headers. - What yt-dlp still decides: it sends its own default headers, including a desktop Chrome
User-Agent whose version it picks each run, and for some sites other than YouTube its
extractors ask for impersonation while they extract. Gather passes no flag that asks for
either, and yt-dlp has no flag that turns off an extractor's request. That request takes
effect only where yt-dlp can importcurl_cffi. gather capsno longer listsstealth, including on a machine wherecurl_cffiis
installed.- Code that imports
gather.backends_stealthorgather.backends.CAP_STEALTHnow raises
ImportError. Remove those imports. - Tests fail if a dependency or extra names a known fingerprint-impersonation,
patched-browser, or challenge-solving package, if a source file imports or looks one up
or passes yt-dlp--impersonate, or if an installedcurl_cffiregisters a capability.
The package list matches by name, so a new tool under another name still needs review. - The credential strip on a same-host redirect from https to http has its own test for both
redirect handlers, the onehttp_getuses and the one the accountablefetchuses. The
removed stealth tests were the only ones that covered that branch.
Video intake pacing and channel runs
gather channel URL --store DIRlists a channel'svideos,shorts, andstreams
tabs (or one playlist) with--flat-playlistand gathers each entry into the corpus with
bounded concurrency (default 2) and paced entry starts. A per-pass ledger under
DIR/intake/makes the run resumable, andsummary-<pass>.jsoncounts entries per tab,
captions (manual, auto, missing by reason), comments, failures by reason, and retries.
A run killed mid-write leaves an unfinished last row; the next run drops it and gathers
that entry again. Any other unreadable row stops the run, before it calls yt-dlp, with
the line number and exit status 1.- Separate passes:
--no-captionsgathers metadata and comments without touching the
caption endpoint;--captions-onlystores only the transcript item. - A
gather runconfig or an MCPgather.runvideo job takes
"captions": "with" | "skip" | "only"(defaultwith). Any other value is a config error,
raised before any job runs. On MCP the job still needs thevideonetwork grant,
whichever pass it asks for. - The run summary names its files relative to
--store(a--summaryoutside the store by
file name) and records the yt-dlp program by file name and a JS runtime without its
path, so a summary you pass on carries no local path. --timeoutmust be above 0, and--sleep-requestsand--sleep-subtitlesmust be 0 or
more. Any other value exits 2 before yt-dlp starts.- Caption intake downloads exactly one track per video, chosen from the info JSON: manual
first, then the original-language auto-caption (en-orig). The olden.*pattern fetched
every English variant and could pick a machine translation; a translation-only video is
now recorded as missing with the reasontranslation-only. - HTTP 429 and YouTube's session rate limit are retried with exponential backoff and
jitter, bounded by attempts and by total wait. Every retry and final failure is logged and
recorded. A channel run stops starting new entries once an entry spends its whole budget
still throttled, and records the rest as stopped. A bot check is never retried and stops
the run at once (see Breaking changes). - The extraction runs with
--ignore-no-formats-error, so a video whose formats are missing
still yields its metadata and caption tracks. With that flag yt-dlp reports YouTube's
playability reason as a warning and exits 0. When the extraction lists no formats, Gather
reads that warning: a session rate limit is retried like an HTTP 429, a bot check ends
the entry without a retry, and a private, members-only, age-restricted or removed video is
recorded as failed with that reason and settled. A geo-blocked or upcoming video is
recorded as failed and tried again on the next run. None of them stores a metadata item
or a "no captions offered" outcome. - yt-dlp runs with
--js-runtimes nodewhen it can startnode(--js-runtimeoverrides),
and--sleep-requests/--sleep-subtitlespass through. The check uses the same PATH
lookup as every child Gather starts, so anodeonly the working folder holds does not
count. - Failure messages report yt-dlp's
ERRORlines instead of the first 160 characters of
stderr, which was often a version warning. - A timeout, a missing yt-dlp binary, or a refused start is recorded as a failed call
(timeout,tool-missing,tool-refused), not an exception. - Every yt-dlp call (tab listing, extraction, caption download) starts the way every
other tool does, from an absolute path in a private empty folder with an environment
allowlist, and carries--ignore-config, so noyt-dlp.confchanges what it runs.
v1.9.1
Security: file sources and MCP path arguments refuse network and device paths
- On Windows, opening a path such as
\\host\share\doc.mdmakes the SMB client connect to
hostand sign in as the user, which can send the user's NTLM response to whoever runshost.
Thedocs,pdf,ocrandtranscribesources opened any path they were given, and so did
every path argument on the MCP surface. None of these needed a launch grant. A model connected
togather mcp, or text it was asked to read, could name a share ingather.docs, in a
gather.runjob target, config path orstore, or in thegather.context,
gather.federationandgather.pilotpath arguments. Astoreon a share also writes the
gathered text there. Affected: 1.6.0 through 1.9.0, whengather mcpruns on Windows. gather.localpathchecks the path text before anything opens it. It refuses text that starts
with two separators of either kind (UNC,\\?\,\\.\,\\?\UNC\, and mixes such as
/\host), text that starts with\??\, and any component with a reserved device name (CON,
PRN,AUX,NUL,CONIN$,CONOUT$,COM1toCOM9andLPT1toLPT9, including
the superscript 1, 2 and 3 forms), with or without an extension, trailing dots or spaces.
COM0andLPT0are ordinary file names on Windows and still read. Windows rules apply on
Windows and to Windows-style text on every platform. On Windows it then walks the path without
following links and refuses a symbolic link or junction whose target is a network or device
path, before anything opens through it. A relative path is refused when the working folder is
a share.- Where it applies: the four file sources on every surface, including each entry of a
docs
directory walk; a run config's file-source targets, before any job runs; every MCP path
argument; a run config'sstorewhen the config comes through MCP; and a pilot manifest's
local targets, fixtures andallowed_local_roots, where Windows rules apply on every platform
so a manifest means the same on every machine. - The MCP call returns
isError: truewithstructuredContent{"code": "NON_LOCAL_PATH", "retryable": false, "kind": "network" | "device" | "link", "argument": "<name>", "detail": "<fixed sentence>"}. The CLI prints the reason and exits 1. Thegather.docspathand the
gather.rundescriptions now say so. - Changes you may notice: a
\\?\C:\...long path is refused, so give the plain drive path. A
share is refused as a source from the CLI too; copy the files to a local folder. A drive letter
mapped to a share looks local to any check on the text, so Gather cannot see it. The CLI's own
--store,--outputand--statepaths, and thestorein a config run withgather run,
are unchanged. tests/test_nonlocal_paths.pyreplaces the filesystem and child-process layer with a spy and
hands each source and MCP tool one path of each class. On 1.9.0, 81 of its 82 tests failed on
Windows and 76 on Linux, where POSIX-style names such asCONstay ordinary file names by
design.tests/test_localpath.pycovers the classifier, the working-folder case and the link
walk against a fake tree on every platform, and real junctions and symlinks on Windows.
tests/test_device_names.pyasks Windows which bare names it opens as devices, so the
reserved-name list cannot drift from the host. It also checks thatCOM0.mdandLPT0.md
read through thedocssource, the MCPgather.docstool and a pilot manifest.
Security: a PATH entry that reaches the working folder no longer starts a program there
- 1.9.0 skipped
.and every other relative PATH entry when it looked up a child program. An
absolute entry could still reach the working folder: one naming it or a folder below it (a
project'snode_modules/.bin, a venv other than the one running Gather), another spelling of
it (a trailing separator,.., letter case), the same folder in quotes, or a junction or
symlink to it. A program planted there under a tool's name (pdftotext,yt-dlp,
tesseract,whisper, the browser, or asynthesizerorprovenancecommand) then ran in
place of the real one. The child also got those entries on its PATH, so a tool that starts
its own helper by name, asyt-dlpstartsffmpeg, could start a copy planted there. On
Windows a drive-relative command such asC:llmnamed a file in the working folder too.
Affected: every release before 1.9.1. Releases before 1.9.0 started tools by bare name
through the operating system's own search, which follows these entries as well; on Linux,
1.8.3 ran the plant through each absolute-entry, link and child-lookup route. 1.9.0 closed the
current-folder search and.entries (see 1.9.0) and left these routes open. - Gather now vendors safe spawn 1.0.1 (
SAFE_SPAWN_VERSION1.0.1, hash-pinned in
VENDORED.sha256). A PATH entry that reaches the working folder, by name or by file identity
after links are resolved, leaves the lookup and the child's PATH. Each kept entry is searched,
and handed to the child, as its real folder, so a link repointed after the check cannot change
what starts. On Windows PATH is read as cmd.exe reads it, an entry whose folder name holds;
leaves, and a bare command name holding:is refused. On POSIX an entry written with quotes
or a leading space counts as relative, and a child PATH the filter empties becomes
/bin:/usr/bin, since an empty PATH means the current folder there. The folder of the Python
that runs Gather and, on Windows, the Windows,System32andSysWOW64folders always stay. - Where the guard narrows: when Gather runs from a filesystem root, or from the home folder or
a folder above it, only an entry naming that folder itself leaves, because installed tools
live below it. A working folder that is the folder of the Python that runs Gather, or on
Windows the Windows,System32orSysWOW64folder, is not guarded, because Gather already
runs code from there. - Changes you may notice: a tool found only inside the working folder is no longer found by
bare name. Give its absolute path in itsGATHER_<TOOL>variable, or as the command itself
for asynthesizerorprovenancecommand. On Windows, a conda environment created inside
the working folder keeps only its root folder: itsScriptsandLibrary\binfolders leave,
so setGATHER_PDFTOTEXT,GATHER_TESSERACT,GATHER_YT_DLPorGATHER_WHISPERto the
tool's full path, or create the environment outside the project. On Windows, a PATH entry
with an unmatched double quote hides every entry after it, as it does in cmd.exe; remove the
stray quote or set theGATHER_<TOOL>variable. A drive-relative command is refused as not
found.
The child's PATH names real folders, so a version manager'scurrentlink reaches it
resolved. Each tool start now reads every PATH entry and the folders above it. That took
about 20 to 60 ms per start on Windows, and a median of about 1.4 s under WSL, where PATH
inherits the Windows folders. tests/test_spawn_working_folder.pyplants decoys in the working folder and a folder below
it, puts the real tool later on PATH, and reaches the working folder by each route above. On
1.9.0, 13 of its 16 tests failed on Windows and 9 on Linux, each because the planted program
ran. On 1.8.3 on Linux, 7 route tests failed the same way. Its three controls keep a sibling
folder whose name starts with the working folder's, a folder holding the working folder, and
an override inside the working folder; they pass on 1.9.0 and 1.9.1.tests/test_vendored.py
now names a 1.0.0 copy as superseded.- CI now runs the child-spawn tests on Windows too, where the junction, letter-case and
drive-relative routes live. Under CI, a Windows run that cannot build a real.exedecoy
fails these tests instead of skipping them.
v1.9.0
Security: launch-only grants on the MCP surface
gather.runtook a config from tool arguments and ran whateversynthesizeror
provenancecommand it named, fetched any network source it listed, and read any
environment variable named asauth_envand sent its value as a bearer token to the host
in the config. A model, or text a model was asked to read, could run commands and send a
secret off the machine with one tool call.gather.pilothad the same exposure through a
live manifest (auth_env, and abrowseroption naming any executable). Affected: every
release with the MCPgather.runorgather.pilottool, up to and including 1.8.3.- These now need a grant set at launch.
GATHER_ALLOW_EXEC(gather mcp --allow-exec) names
the commands a config may run.GATHER_ALLOW_NETWORK(--allow-network) names the network
sources.GATHER_AUTH_ENV_ALLOW(--auth-env NAME@HOST) binds each credential variable to
the one host it may be sent to, overhttpsonly. A pilot manifest'sbrowseroption other
thanchromium, orno_sandbox, needs that browser named inGATHER_ALLOW_EXEC. - Without the grant the call returns
isError: truewithstructuredContent
{"code": "GRANT_REQUIRED", "retryable": false, "setup": "<VARIABLE>", "detail": "<fixed sentence>"}before anything runs, connects or reads a credential. The server reads grants
once at startup; nothing in a config, manifest or tool call widens them. A pilot refresh
checks the grants on the same read of the stored manifest it captures from. - Tool descriptions changed:
gather.runandgather.pilotnow say which inputs need a launch
grant, so a host sees why a call returnsGRANT_REQUIRED. - Breaking for MCP hosts that relied on the old behavior: add the grant to the server's launch
configuration. The CLI and the Python API run the operator's own config and are unchanged.
Security: child programs start from an absolute path in a private folder
pdftotext,yt-dlp,tesseract,whisperand the headless browser started by bare name
from the server's working folder. On Windows a same-named.exein that folder ran in place
of the real tool, and on any platform a.entry on PATH did the same.yt-dlpalso read a
yt-dlp.conffrom that folder, and a config can carry--exec. Thesynthesizerand
provenancecommands had the same lookup. Affected: every release up to and including 1.8.3.- Every child now starts through
gather.spawn, which calls the vendored safe spawn helper
(SAFE_SPAWN_VERSION1.0.0, hash-pinned inVENDORED.sha256). The program resolves to an
absolute path (GATHER_<TOOL>overrides win; relative and empty PATH entries never count),
runs in a new private empty folder, and sees an environment allowlist instead of Gather's
whole environment.yt-dlpgets--ignore-config. On Windows the child also gets
NoDefaultCurrentDirectoryInExePath=1, and a batch-file target refuses cmd.exe
metacharacters. Output stays bytes, so receipts hash exactly what the tool wrote. - Changes you may notice: a command given as a relative path is refused; a synthesizer that
reads its API key from the environment needsGATHER_CHILD_ENV=KEY_NAME;yt-dlpno longer
reads your useryt-dlp.conf; apython -mprovenance command must be installed, not only
present in the working folder. tests/test_spawn_children.pyplants a decoy named like each child (a real.exeon
Windows) in the caller's folder with.on PATH, a fake API key in the environment and a
yt-dlp.conf. 18 of its 19 first tests failed on the unfixed code. Withyt-dlpinstalled,
a real-tool test shows a plantedyt-dlp.conftaking effect without the fix and having no
effect throughVideoSource.
Release workflow
- The workflow grants nothing by default.
buildreads the repository,publishholds only
id-token: writefor trusted publishing, and a newgithub-releasejob holds only
contents: write. Checkouts drop the token after cloning. - The PyPI upload uses
skip-existing: true, so a re-run after a partial upload completes. - The build checks the tag against the package version and the vendored helper in the wheel
againstVENDORED.sha256, then writesSHA256SUMS.txtfor the wheel and the sdist. The
github-releasejob verifies those sums and creates the GitHub Release with the wheel, the
sdist andSHA256SUMS.txtattached, or refreshes its files on a re-run. Before this, the
release and its checksum file were made by hand.
gather-engine 1.8.3
Catalog line breaks
- Readable context (
inspect_corpus,select_contextand their MCP tools) now splits
catalog.jsonlon LF, CRLF and CR only, the ruleCorpus.rowsreads by. It used
str.splitlines, which also breaks on U+2028, U+2029 and U+0085. The catalog writer leaves
those characters unescaped inside JSON strings, so one title such as a misdecoded
Windows-1252 ellipsis made the context surface refuse a corpus thatverifyaccepts. - A regression test stores titles carrying each of those characters, rewrites the catalog
with LF, CRLF and lone-CR row terminators, and checks thatCorpus.rowsand
inspect_corpusread the same rows. The store newline test now also checks that the
object bytes on disk hash to the catalogsha256.
Presentation parity
- README now exposes the current source version, operator commands, and the
boundary between exact source-byte receipts and optional source adapters.
Install: pip install gather-engine==1.8.3
gather-engine 1.8.2
What's Changed
- Clarify Gather MCP scope filtering by @HarperZ9 in #27
- Release gather-engine 1.8.2 by @HarperZ9 in #28
Full Changelog: v1.8.1...v1.8.2
Gather 1.8.1: stable descriptor admission
Gather 1.8.1 corrects confined corpus reads on WSL Windows-drive 9p/v9fs mounts. Reads now refuse unsupported opened root, descendant-directory and catalog/body descriptors with UNSAFE_PATH, before reading content through unstable authority. Linux classification uses the opened descriptor's procfs mount ID, avoiding native statfs structure assumptions. Missing Linux mount information also refuses the read.
Native Windows and native WSL filesystem descriptor handoff remain available. Other POSIX platforms retain existing no-follow/type checks without a mount-detection claim. CLI and MCP still accept directory strings; retained descriptors remain a Python-only same-process interface.
Validation includes the full Windows suite (671 passed, 12 skipped), focused WSL controls (44 passed, 8 skipped), isolated Windows CLI/MCP/descriptor and tamper checks, native WSL reads, and actual WSL 9p rename/replacement refusal. Nested-directory and leaf refusal controls were instrumented. Live Linux execution covered WSL x86_64.
The release workflow passed. The attached wheel and sdist are the same bytes published to PyPI. Downloaded artifacts were checked directly against the release commit: all 64 wheel Python modules and 133 tracked sdist files matched exactly. Checksums are attached.
Install or upgrade with python -m pip install --upgrade gather-engine==1.8.1.
Gather 1.7.1: Corpus newline integrity
Gather 1.7.1
Gather 1.7.1 is a patch release for corpus newline integrity and readable-context provenance.
It resolves the inherited CR/LF and mixed-newline corpus integrity mismatch documented in 1.7.0 by writing new corpus objects as exact UTF-8 bytes and sealing a versioned storage witness into new catalog rows. Readable context now keeps the exact source-text identity separate from the LF-normalized readable view: sha256, verified_sha256, and source_sha256 bind the exact source text, while view_sha256 and view_codec describe the normalized view used for excerpt display and Python character offsets.
This patch also fixes a writer admission edge case found during independent review: adding a receipt now refuses a corrupt preexisting object at the content-addressed path before appending to the catalog. Valid legacy objects are still reused without rewriting when Gather can reconstruct exactly one matching source text from exact UTF-8 bytes or the old Windows LF-to-CRLF expansion.
Validation for this candidate used the full Gather pytest suite, ruff, mypy, source gather status/doctor, no-index wheel installs on Windows and WSL, CLI/MCP context selection, and tamper refusal checks.
Boundaries: legacy reconstruction verifies source text for an existing receipt, not historical raw object-byte integrity. Storage-witness stripping or codec changes are downgrade-detectable only when a consumer pins or independently verifies the prior corpus digest with --expect-digest / expected_corpus_digest. Readable context is acquired source context; it does not prove source truth, claim support, completeness, downstream model use, caller workspace-parent resolution, or absence of sensitive material in selected source text or URLs.
Gather 1.7.0
Gather 1.7.0
Gather 1.7.0 adds readable corpus context selection. Users can inspect bounded, re-hashed source/comment excerpts from a stored corpus and export selected private context payloads through the CLI, Python API, or MCP gather.context tool.
The new payload records row refs, source refs, full-body hashes, selected ranges, selected text, omissions, and a deterministic selection_digest. Bodies are decoded as UTF-8, CRLF/CR line endings normalize to LF, and selection ranges are Python character offsets over normalized text. verified_sha256 is the full normalized body hash, not a selected-slice hash.
This release also tightens corpus body reads around tamper, missing, unsafe path, oversized body, and read-budget cases; refreshes docs and examples; and keeps pilot fixture evidence portable with LF checkout rules and regenerated sample receipts.
Known inherited limitation: certain literal carriage-return/newline forms in corpus source strings, including CRLF/lone-CR/mixed-newline cases, can fail corpus integrity verification and be refused. This behavior is inherited from 1.6.1 and is not evidence of external tampering by itself. Preserve original corpora for diagnosis; a codec/provenance fix should be handled as follow-up work.
Validation for this candidate was rebuilt from commit d477b4ad5ebed6805ec88bc45eede54cf6f8160b using LF git blob authority. The archive matched tracked blobs byte-for-byte, package checks passed, the clean no-index wheel install exercised CLI and MCP context selection, and tamper selection was refused.
Boundary: readable context is acquired source context. It does not prove source truth, claim support, completeness, downstream model use, or absence of sensitive material in selected source text or URLs.