Build FileChanger
YESreplaces $39/mosaves $468/yrback to the verdict
A personal conversion API whose hard problems are not the conversions, which other people's software does, but discovering what the installed engines can actually do, fencing each invocation so a bad file cannot take the machine with it, and reporting failures in a way that is useful to you and useless to an attacker.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the service, the worker and the MCP server.
Get it python.org, or your version manager. open ↗
Verify
python3 --version prints 3.12 or higher - installfree
Why These four do all the work, and the format list you will advertise is exactly what they report.
Get it Your package manager, or the container image this build ships.
Verify
Each binary prints a version and the startup probe lists its formats - installfree, but the install is large
Why Pandoc does not produce PDF on its own; it hands off to a typesetting engine, and forgetting this is the most common first failure in a converter.
Get it Install a supported typesetting engine and name it in the configuration.
Verify
A markup file converts to a PDF that opens - decidefree
Why The distribution default may already disable the delegates you need, or may not disable ones you would rather not run, and either way it is not your decision until you commit one.
Get it Read the image engine's own security policy documentation and write a policy covering resource limits and which coders are permitted.
Verify
The policy file is committed and the service starts with it - installfree
Why Pinning every engine and its fonts in one image is what makes an upgrade a deliberate act rather than a surprise.
Get it Any container runtime with compose support.
Verify
The image builds and the engine round trips pass inside it
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- engines(name, binary_path, version_string, probed_at) · the version is what explains a conversion that worked last month - formats(engine, direction, format_name, probed_at) · direction is input or output, discovered rather than typed - jobs(id, created_at, source_format, target_format, input_bytes, state, started_at, finished_at, exit_code, failure_kind, output_bytes, output_path) - job_logs(job_id, stream, content) · the command and its error output, stored and never returned over HTTP - api_keys(id, name, hash, created_at, last_used_at, revoked_at) · the plaintext is shown once and never stored - limits(name, value) · timeout seconds, maximum input bytes, maximum output bytes, per-key concurrency - The format matrix is a cache of what the installed binaries said, with the date they said it. An upgrade invalidates it, which is the point.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/jobs.db | Jobs, logs, keys and the format matrix cache. |
WORK_DIR | required | ./data/work | Per-job directories. Deleted on completion and on retention. |
INLINE_MAX_BYTES | required | 8388608 | Above this a conversion becomes a job instead of a response. |
CONVERT_TIMEOUT_SECONDS | required | 120 | Wall clock per conversion. A timeout is a distinct reason class. |
OUTPUT_MAX_BYTES | required | 536870912 | A small input can expand enormously, deliberately or otherwise. |
JOB_RETENTION_HOURS | required | 24 | Job directories are removed after this whether collected or not. |
IMAGEMAGICK_POLICY_PATH | required | ./config/imagemagick-policy.xml | The committed policy, used instead of whatever the distribution shipped. |
WORKER_COUNT | required | 2 | Each worker gets its own office-suite profile directory derived from its index. |
The build, in order
One engine, one round trip, one honest failure
A single conversion works end to end and a failed conversion is reported as failed with a reason class.
Error output is a description of your filesystem.
Files
src/convert/run.pytests/test_no_leak.pyThe failure path is where the leftovers come from.
Files
src/convert/workspace.pyThey have completely different fixes.
Files
src/convert/classify.py
done when · tick each as it passeswatch out- Returning the engine's error output to the caller is the natural debugging shortcut and it hands over absolute paths, installed versions and sometimes fragments of the document itself. Store the command and its error in the job log, return a reason class and a job id, and make the only path to the detail a local command you run yourself.
Ask the binaries what they can do
The supported format matrix comes from the installed engines at startup, with their versions recorded.
The version is the first thing you will want after a surprise.
Files
src/engines/probe.pytests/test_probe.pyFailing after a large upload wastes everybody's time.
Files
src/api/convert.pyA cached answer should say how old it is.
Files
src/api/formats.py
done when · tick each as it passeswatch out- Hard-coding a list of a few hundred format pairs produces a service that accepts a conversion the installed binary can no longer do, wastes the upload and returns an engine error that looks like a bug in your code. Ask the binaries, cache what they said with the date, and treat a version change as a reason to re-probe rather than a curiosity.
Fencing each conversion
One conversion cannot exhaust the machine, reach the network, or be affected by another running at the same time.
A shared profile fails only under concurrency, which is where you use it.
Files
src/engines/office.pytests/test_concurrent_office.pyThe distribution default is a decision somebody else made.
Files
config/imagemagick-policy.xmlA small input can produce an enormous output on purpose.
Files
src/convert/limits.py
done when · tick each as it passeswatch out- Running two office-suite conversions at once with the default shared profile produces a job that exits without an output file and without a useful error, and because it only happens under concurrency it survives every single-file test you write. Give each worker its own profile directory derived from the worker index, and add a concurrency test that runs several conversions of the same type simultaneously.
Big files become jobs
A conversion too large to answer inline becomes a job that survives a restart.
One route, two shapes, is easier to use than two routes.
Files
src/api/convert.pytests/test_threshold.pyA partial output is not a checkpoint.
Files
src/jobs/recover.pyVideo is the reason this matters.
Files
src/api/result.py
done when · tick each as it passeswatch out- Marking an interrupted job as queued again on restart re-runs a conversion whose partial output is still on disk, and the second attempt writes over it while a client is streaming the first, which produces a corrupt download nobody can reproduce. Mark it interrupted, delete the partial output, and let the caller decide whether to ask again.
Keys, agents and the day an upgrade breaks a formatproduct builder
Access is controlled, an agent can call it without HTTP, and an engine upgrade cannot silently change what works.
The one-time display is the whole design.
Files
src/auth/keys.pytests/test_key_storage.pyAn agent should not have to speak HTTP to use this.
Files
src/mcp/server.pyAn upgrade that breaks a format should break the build.
Files
.github/workflows/build.ymltests/test_round_trips.py
done when · tick each as it passeswatch out- Upgrading the container image and deploying it because the tests passed is fine until an engine drops a filter or a font package changes, and then the failure arrives as a single user's broken document weeks later with no way to tell which upgrade did it. Run one real round trip per engine as part of the build, store the engine versions in the image, and compare them against the previous build in the release notes.
That is the whole plan for FileChanger. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Any converter written here.
- Uploads from people you do not know.
- Metering, quotas and billing.
- The long tail of awkward format pairs.
- sandboxing, because your version runs files straight through the same toolchain as everything else on the machine
- the long tail of format pairs, where the fix is usually a missing font, a filter flag or an upstream bug
- queueing and timeouts for large video, which is where the compute bill actually lives
- a hosted MCP server and API keys your other tools can reach from anywhere, not just from localhost
- somebody else being on call when a LibreOffice or LaTeX upgrade quietly breaks PDF output
- A per-pair round trip fixture set, grown one entry at a time whenever something breaks, which becomes the most valuable file in the repository
- A dry-run route reporting which engine and arguments a pair would use, which answers most questions without a conversion
- Font package pinning in the image, since a font change is the most common cause of a document that converts and looks wrong
Need the files? The project pack on the verdict page hands your agent the whole brief · more documents & pdfs.