cb5dfabce276e8d5ea4aed24c96a5ca19a443d26
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb5dfabce2 |
the watcher can authenticate, and the HEVC decoder is declared
TWO BUGS, one in each half. the watcher demanded OFFICER_AUTH_TOKEN and nothing supplied it. the commander's credential is written to `officer.env` beside the script, under OFFICER_API_KEY — a sibling file this never sourced. so a commander created from this template would have died on its first tick with "OFFICER_AUTH_TOKEN is not set", and I had missed it by appending the values by hand while testing, which is exactly how a thing looks like it works. two files rather than one because they have different lifetimes: officer.env is the platform's and can be re-minted, config.env is the owner's answers and must survive that. the name is bridged in the script rather than renamed at either end — a commander sources the platform's file and a task reads the platform's env, and neither is this plugin's to rename. AND libheif-plugin-libde265 IS NOW DECLARED, which cost an hour to find. libheif packages its CODECS SEPARATELY: `libheif1` alone parses a HEIC container and reports 4032x3024, then fails on the pixels with "Unsupported codec". ubuntu ships the AV1 plugin in the common install and not the HEVC one — which is what every iPhone actually writes. observed today: identical libheif 1.17.6 on two hosts, HEIC decoding on fofos and not on officer-dev, the only difference being that package. a plugin that silently half-works on some machines is worse than one that refuses to install. no dnf entry for imagemagick. fedora's package is literally `ImageMagick` with capitals and the manifest validator refuses uppercase — correctly, since these strings reach a root command line as a closed vocabulary. naming `imagemagick` there would name a package that does not exist. left for the conversation the manifest documentation says a case like this wants. the validator caught that on the way through, and discovery refused the plugin until it was fixed. verified after: no problems, no broken plugins, 3 osDeps read back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5b2fe03ffd |
Convert on arrival — the first plugin-shipped commander
watches a folder and converts what lands in it, so a photo uploaded from a phone is
immediately something a browser can show and a thumbnail can be made from.
configurable, which is the point: watch_dir, extensions, output_format, lossless,
delete_source. only watch_dir has no default, so it is the only thing that blocks
arming.
IT CALLS av-club RATHER THAN ffmpeg OR sharp. everything that makes a conversion
safe lives behind that API, and a watcher shelling out itself would have none of it.
it also breaks the usual commander shape deliberately, and says so. the standard is
"wait, then spawn a headless agent to exercise judgement" — but there is no
judgement in "a HEIC arrived and needs converting", and an agent per photograph
costs real money to reach the same answer every time. so the loop calls the API
directly and only involves an agent when something goes wrong.
three things in the watcher that are not obvious and each cost somebody an
afternoon somewhere:
close_write and moved_to, never create — `create` fires when a file is opened for
writing, before it has any bytes
two identical size readings before touching a file — a file still being copied in
converts to a truncated result and gets marked done
a failure is recorded as SEEN — the same file fails identically every tick, and a
watcher retrying forever is the failure mode that costs money. it is in the log
and the owner can clear the line.
the brief tells it to offer the Convert Image task for the BACKLOG before arming
anything. a watcher only sees what arrives, and one armed over a quiet folder looks
broken while working perfectly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
c69f6d12a3 |
Convert Image, and HEIC opens because av-club already knew how
the fourth task. same shape as the other three — beside the original, skip an existing output, skip a file already in the target format, delete only when asked and only after the write. /v1/sharp gets the same path mode /v1/ffmpeg got, for the same reason: the file browser and the commander both already have the file. HEIC IS THE POINT OF THIS ONE, and nothing in the task does anything about it. `decodable.ts` has routed HEIC through ImageMagick since before I touched this plugin, because the libheif inside sharp is built without the HEVC decoder — a HEIC reads its own header fine and then fails on the pixels, which looks exactly like a corrupt file and is not one. the task simply names `.heic` in its trigger and asks. the photographs open. I had diagnosed that gap independently against the file browser's thumbnails and concluded a system decoder would be needed, twice telling the owner these files could not be read. this plugin had already solved it and documented why. worth the note: the answer was in the tree. a path has no stored `format`, so sharp's `metadata()` supplies it — and that only reads the header, which is precisely the part of a HEIC that works, so it correctly identifies the files needing ImageMagick. `rotate` with no argument is in every pipeline: it applies the EXIF orientation and drops the tag. without it a portrait photo from a phone converts to a landscape image that viewers rotate back, until one does not. max_dimension shrinks to fit and never enlarges. quality 82 for jpeg and webp, 55 for avif — where a photograph stops looking better and only gets bigger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7d57deeb3f |
three tasks on the right-click menu, and a skill for the agents
Convert Audio, Extract Audio, Convert Video. ported from the versions that ran on
alpha for months, which is where the rules come from — they are not invented:
skip a file already in the target format (re-encoding mp3 to mp3 loses quality
for nothing)
never overwrite an existing output (the file there may be one somebody made)
delete the source only AFTER the output exists, and only when asked
`inline: ask`, so the modal offers running it now or as a job — a few files inline,
a discography as a job. that was the affordance the old ones had and the reason the
choice exists.
THIN CLIENTS. every one calls /v1/ffmpeg; none of them runs ffmpeg. what to tell
ffmpeg lives once in av-club rather than three times in bash, which is the whole
point of the plugin existing. the three files nevertheless repeat their walk and
their reporting, because the task runner materialises ONLY index.ts and a script
task cannot import a sibling — that duplication is the platform's shape, not a
choice, and each file says so.
CONVERT VIDEO PROBES BEFORE IT DECIDES. if the codecs already fit the container it
asks for `c:v copy, c:a copy` — a remux, lossless, roughly a hundred times faster
than re-encoding. guessing from the extension is how a "fast remux" silently
re-encodes, since plenty of .avi holds h264. that needed a path probe, so
/v1/probe?path= is added beside it, same containment, read-only.
EXTRACT AUDIO HAS NO delete_source, deliberately. extracting is additive; offering
to delete would be offering to trade a video for its soundtrack. a video with no
audio track is reported as a skip rather than a failure — in a folder of screen
recordings that is most of them.
what is NOT here: track selection, subtitle handling, downmixing, per-file
percentage. those belong with jellyfin rather than with basic conversion and are
left where they are rather than half-ported. convert-video's TASK.md says so.
VERIFIED by running each task the way the runner does — temp dir, env vars, argv:
flac→mp3 on a folder; the re-run skipping both files for the two different reasons;
extraction skipping the silent clip and converting the other; mp4→mkv copying h264
and aac through untouched; mp4→webm re-encoding to vp9/opus; delete_source removing
a source after success and LEAVING it after a failure.
the skill tells an agent to use the endpoint and never to shell out to ffmpeg —
everything that makes this safe is on the far side of it — and to offer the
right-click tasks first when somebody is at a screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0836ebfdb8 |
audio joins the vocabulary, and -vn is a flag rather than an option
FORMATS was mp4/webm/mp3/image2 — a web-delivery list, because that is what av-club was first for. a right-click on a .mkv that offers "mp4 or webm" is not a conversion tool, so: mkv, mov for video; m4a, flac, wav, ogg, opus beside mp3 for audio. the KEY is what a caller asks for; `container` is what ffmpeg is told, because ffmpeg's names for two of them are not what anybody calls them — `matroska` for .mkv, `ipod` for .m4a. making a caller know that is making them learn ffmpeg trivia to convert a file. audio encoders too: libmp3lame, libvorbis, flac, alac, pcm_s16le beside aac and libopus, plus q:a for the encoders that want a quality rather than a bitrate, and map_metadata/id3v2_version so tags survive a conversion. A REAL BUG FOUND ON THE WAY, and it had shipped. `an` was declared as an enum whose only value was "1", so it emitted `-an 1` — and `-an` takes no argument, so ffmpeg read the `1` as an output filename and died with "Error opening output files: Invalid argument", naming neither the option nor the problem. `an` had never worked. I reproduced it exactly by adding `vn` the same way. so there is a `flag` kind now: given `true` it emits the option alone, and anything else is refused with a message that says so. `vn: "1"` is a 400 rather than a mystery. verified through the running service: mkv remux, mov, and extraction to all six audio formats — mp3, flac, wav, ogg, opus, m4a — all 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bd8b7b9ad4 |
ffmpeg can work on a file where it already is
adds a path mode to /v1/ffmpeg:
{ input: "as_…" } → stored as a new asset, 201
{ inputPath: "/…", outputPath: "/…" } → written where you said, 200 { path, bytes }
THIS REVISES THE INVARIANT IN docs/SECURITY.md, deliberately, and that document now
says so at the top rather than being quietly contradicted by the code.
it used to read "the caller never supplies a path, for input or output". that was
right for an upload service and av-club is not one: both of its real consumers —
a right-click on ~/Movies/thing.mkv, and a commander watching an upload folder —
already have the file, and obeying the rule meant copying gigabytes in and back out
to convert something that never moved. a rule that expensive gets worked around,
and the workaround is where the hole would have been.
so it is narrower now and says what it was always for. argv never a shell,
-protocol_whitelist file, filters allowlisted, work bounded — all unchanged. what
is gone is only the claim that a path never appears, replaced by one small module
every path goes through:
absolute (a relative path resolves against a cwd the caller cannot see)
realpath FIRST, then check — checking the path as written and opening the
resolved one is the classic way this fails
inside as `root` or `root + sep`, never a bare startsWith, which accepts
/home/user-backup for a root of /home/user
for an output the PARENT is resolved, and must already exist
errors never say which of "outside" or "missing" it was, or this becomes an
oracle for what exists elsewhere on the disk
VERIFIED against the running service, not reasoned about: `..` traversal, an
absolute path outside the root, a write outside the root, a relative path, a
symlink pointing into /etc, and a write THROUGH a symlinked directory — all
refused, nothing written outside the root.
the output is moved into place at the END. ffmpeg encodes into a temp dir, so a job
killed by the timeout or a restart leaves the caller's directory as it was. pointing
ffmpeg at the destination would leave a truncated file where a good one might have
been — and a task that then deleted its source would have destroyed something.
rename first, copy on EXDEV, which is the normal case rather than the error one
since tmp is often tmpfs.
docs/API.md said this endpoint was "designed, not yet built" while it was routed and
working. that is fixed too: it was read as a gap and nearly rebuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e2ed76eac4 |
bind a free port, do not assert one
a plugin sidecar does not choose a port. it binds whatever is free and REPORTS it, which is how every other sidecar here works and why none of them can collide. av-club asserted 3020 because it started life as a standalone service. that assumption cost a deploy the same day it became a plugin: fofos still had a copy of the pre-plugin code running from ~/garbage/av-club, on `main`, holding 3020 — so the plugin's sidecar died on EADDRINUSE in a restart loop while the platform reported "sidecar not available". two things claiming one number, and neither able to say so. AV_CLUB_PORT still pins it, for running outside Officer — the standalone mode fofos/platform was written against, where the other side has to know where to look. inside Officer nothing sets it. the announcement now sends `server.port`, never the requested value. reporting what you asked for rather than what you got is how a sidecar ends up advertising a port it is not on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 |
||
|
|
79ff366802 |
become an Officer plugin
av-club keeps doing exactly what it did — sharp, ffmpeg and ffprobe behind one API, and the invariant that the caller never names a path. What changes is how it is reached and who decides who may reach it. `src/` is `sidecar/`, because that is the name the platform looks for: `sidecar/index.ts` present means "this plugin ships a process", and presence is the declaration for everything structural here. `manifest.ts` at the root is the other half of that rule. api/router.ts is a proxy and nothing else. the platform authenticates, checks the `av-club` permission, and forwards — so the sidecar now accepts a request carrying `X-Officer-User` and no longer demands its own bearer token from callers who came that way. it binds LOOPBACK to make that trustworthy, which is the same boundary caldav's DAV door already relies on. AV_CLUB_TOKEN stays, and still guards anything calling the port directly: fofos/platform is the stated first consumer and is not Officer. THREE THINGS HAD TO GO, and each one broke it in a way worth writing down. package.json and bun.lock: a package.json inside a plugin is a package boundary, so bun stops walking up for the platform's tsconfig and `@@/sidecar/connect` stops resolving. the sidecar died on its first import, fifteen times. npm dependencies go in the manifest instead, where npm-deps.ts intersects them across plugins into one plugins/node_modules — which is also how this surfaced a real conflict: sharp ^0.33.5 (atelier) and ^0.35.3 here do not overlap, since a 0.x caret pins the minor. atelier moved up; it calls one method. tsconfig.json: same failure one layer earlier, shadowing the platform's `@@/*` paths so api/router.ts would not mount. no other plugin has one. no screens, deliberately. this is an API and a body of documentation; its callers are a commander watching a folder, an agent, or curl. a dock tile would advertise a page with nothing on it. verified through /api/av-club with an agent key: a 3000x2000 jpeg uploaded, probed, and resized to a 1200x800 avif — 166 KB to 1 KB — without the caller ever holding av-club's own token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 |
||
|
|
dc4fe758f3 |
Decode HEIC, and transcode video
Two things the platform needed and could not get. HEIC: sharp reported "corrupt header" on every iPhone photograph, which they are not. Two separate causes. libheif caps iref references and burst/depth HEICs exceed it — `unlimited` gets past that, and limitInputPixels is now passed explicitly alongside because unlimited also loosens the bomb guard (verified the pixel limit still rejects a bomb). Then sharp still could not decode the pixels: its bundled libheif is built without libde265, so HEVC-coded HEIF is unreadable to it. The system libheif does have the plugin, so HEIF now routes through ImageMagick to a lossless PNG first. Verified: 1.4 MB HEIC in, 102 KB WebP out. /v1/ffmpeg is built. ffmpeg's own option names, structured filters rather than a -vf string, and four presets — web-1080/720/480 and poster. The presets are where the encoder knowledge lives, so a consumer asks for "web-720" rather than knowing flags, and revising quality is a change here for every site that uses it. Default-deny throughout, same as the sharp surface: filters that read files or load native code are absent from the table rather than blocklisted, protocols are whitelisted to file, and the caller still never names a path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1597fccd75 |
The HTTP service: assets, probe, and the full sharp surface
Exposes sharp and ffprobe with their own vocabulary — operations are named what sharp names them, ffprobe fields come back spelled the way ffprobe spells them — so the tools' own documentation applies and there is no house dialect to learn. Safety comes from one architectural invariant rather than from filtering: the caller never supplies a path, a URL or a protocol. Input is an id we issued, output is a name we chose. That removes the entire class of attack that makes "just pass the flags through" a remote shell, which is precisely what makes it safe to open up everything else. docs/SECURITY.md has the reasoning and the specific attacks. The vocabulary is a data table, so the validator and docs/SHARP.md are generated from one source and cannot drift. Two things are deliberately absent from it: withMetadata, because that is how a client's GPS coordinates get published, and limitInputPixels, because a caller who could raise it could turn a 1 KB PNG into a 40 GB allocation. Verified against a running server, not asserted: traversal ids rejected, unknown operations and typo'd parameters rejected by name, out-of-range values rejected with the bound, a GPS-tagged photo coming out clean end to end, video probe, Range requests, and a rejected upload leaving nothing on disk. Found while building: config threw at import if AV_CLUB_TOKEN was unset, so generating documentation required a production secret. The token is read at boot in server.ts now — still a refusal to start, no longer a module-scope landmine. ffmpeg is designed in docs/API.md and not built yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
81c8e17a8e |
Start av-club: what it is, and a verified installer
Media processing pulled out of the website that first needed it. ffmpeg, ImageMagick, libvips and exiftool are a real installation rather than an npm install, and encoder settings get revisited as browser support moves — neither of which belongs on the same clock as page templates and database schemas. No processing code yet. What is here is the tooling requirement, an installer, and docs/PIPELINE.md writing down what has NOT been decided — how a consumer talks to this, whether processing blocks an upload, where output goes — so those get chosen deliberately instead of by whatever gets written first. verify.sh checks by running: it encodes a video and probes the result against what ffmpeg was told to make, loads sharp under Bun specifically, and asserts a re-encode still strips GPS. That last one guards a real leak — if a future sharp changes its default, this fails a check rather than quietly publishing the coordinates of a client's living room. Confirmed non-vacuous by hiding ffmpeg and watching it exit 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |