Releases: KingPin/SubPixel
Releases · KingPin/SubPixel
Release list
v0.3.0
Works through a UX and security review of 0.2.0. The themes are the filesystem
authority of an MCP tool, overwrite protection that covers a whole destination
rather than half of one, and telling a caller what actually happened.
Security
- Every path an MCP tool takes from an agent is confined to the project.
out,out_dir,reference_images, andimagewere resolved against the
working directory and never checked, so../../.ssh/id_rsawas a readable
reference and../../../etc/cron.d/x.pngwas a writable destination.
assets.ymlalready had a containment check;within()andrealish()moved
tocore/fsx.tsand every path site now calls the same one.SECURITY.md
states the scope: a path from an agent is in scope, a path typed into a shell
is not. spx init --dry-runno longer prints the merged configuration file. The
preview was built by merging subpixel's entry into the user's existing config
and serialising the result, so a dry run ofspx init claudeprinted back
every other MCP server's API keys. Each writer renders only subpixel's own
entry now.- The sidecar is treated as half of the destination.
writeManifest
replaced<image>.jsonunconditionally, and it ran after the image had
landed: findinghero.pngfree was enough to claim it, and the run then
destroyed a neighbouring JSON nobody passed--overwritefor. The slot is
checked while the destination is still being chosen, and the manifest is then
published withlink()/EEXISTlike the image beside it, so the only file it
can replace is one it has just read and recognised as subpixel's. A slot that
cannot be read at all — a directory, a permission error — is not an empty
slot, and the run steps to a sibling name.
Added
spx generatereports progress on stderr. A multi-image run printed nothing
until it finished. A TTY gets one rewritten line, a pipe gets one line per
event, stdout stays the artifact paths and nothing else, and--quiet
silences it.--jsonreports the variants that were written, the widths that were skipped,
and a format redirect. The payload described only the primary image, so a
caller consumingspx generate --jsoncould not see which variant widths
existed or that its requested format had been changed.
Changed
spx sync --checkreads the bytes back. A cache-key match says the inputs
are unchanged. It says nothing about the file, which a half-finished copy can
truncate and an optimiser can rewrite while the sidecar beside it still
matches. Every artifact is verified, the primary and each variant: a variant
is a file nothing in the cache key describes, so existing was not evidence of
being intact.syncitself deliberately does not, because it is about to
consult the cache and write anyway. A sidecar written before this release
records no digest per variant and is taken on trust rather than reported as
drift.- Bad enum values and impossible dimensions are refused before a request is
sent.--quality ultraand--format gifreached the backend and came back
as a provider error after the wait, and--size 999999999x999999999reached
aspect-ratio arithmetic and threw a rawRangeError. The accepted values are
declared once incore/types.tsand shared by the Commander options, the
manifest validator, the MCP tool schemas, project config, and theassets.yml
schema, so the five cannot drift. Dimensions are bounded at 16384. - A reference image is refused at its
stat, before it is read. The 12 MiB cap
was applied to a buffer that was already resident, so the 3 GiB file someone
pointed at by mistake was in the process before anything objected, and the
objection was an allocation failure. The whole-request budget is threaded
through the set, so the file that breaks the 32 MiB cap is named rather than
reported as a grand total after every remaining file has been read. - A JSONC configuration file is handed the exact entry to paste rather than
pointed at--force. There is still no JSONC parser: the comments belong to
the user and a round-trip would eat them. spx sync --checkhashes its references instead of loading them.
referenceHashesbuilt a base64 data URL for a request body — a second copy
of every file, a third longer than the first — and then read one field off the
result. A check sends nothing anywhere, so all of it was discarded, once per
reference per asset, on every run. The size caps still apply.spx iconspacks the ICO from the PNGs it has already rendered.
buildIconPackresized the source five times for the pack and three more for
the ICO, two of them at 16 and 32 — sizes it had just written out. Eight
resizes become six, and the ICO payloads are byte-identical to the files
beside them.
Fixed
- A partial
syncover MCP no longer discards the report. When some assets
synced and one failed, the server threw and the successful half of the report
went with it. Error metadata travels on aSymbol.for("subpixel.details")
property, so the report survives without collapsing the error taxonomy or the
exit code the way wrapping would.
v0.2.0
Added
spx init --only <ids>writes a chosen subset of the harness targets instead
of the whole default set. Every report line now names the id it wrote, so the
vocabulary the flag takes is readable from a plain run.spx init --globalconfigures a harness for every project rather than one
repository: the skill to~/.claude/skills, the Claude Code MCP entry to
~/.claude.json, and Cursor to~/.cursor/mcp.json.AGENTS.mdhas no
user-scoped form and is reported asunsupported.SECURITY.md: a private advisory channel, and what subpixel touches — the
credential file it reads, the redaction every log goes through, the two hosts
the source talks to, and the filesystem writes that are in scope.
Changed
spx initno longer writes the Claude Code MCP entry by default. The
skill and the server configured the same harness twice, and the server cost
7392 bytes of tool schema in the model's context on every turn against a 4618
byte skill. Ask for it with--only claude-mcp; a default run names the
target it skipped. An entry already in a project's.mcp.jsonis untouched.- The HTTP backend pins
reasoning_efforttolowrather than inheriting
whatever the resolved model defaults to. Three of the five listed models
default tomedium, so a reorder of the Codex model cache could have raised
the cost of a generation with no change here. The one forced
image_generationcall has nothing to buy with the extra effort. - The etag refresh layer for the model catalogue is withdrawn. The captured
/modelsendpoint answers a matchingIf-None-Matchwith 200 and the full
360 KB body, so a conditional refresh does not exist to make; a stale cache
stays reported rather than acted on, andcodexrewrites it on its own
staleness check anyway. - The bundled fallback catalogue carries the slugs and priorities from the
captured response. It was missinggpt-6-astra, which is priority 1.
Fixed
spx init --forcecopies the old bytes to<name>.bak, with the file's own
mode, before writing a file it could not parse. Under--globalthe
unparseable file is Claude Code's session state, and discarding it is not what
the flag is for. The plan says so before it runs.spx initrefuses to write a target that changed between the plan and the
apply, reportingstaleinstead. Claude Code writes~/.claude.jsonwhile it
runs, which is exactly when someone runsspx init --globalfrom inside it.
Re-running merges into what is there now.spx initkeeps the mode of a file it merges into. Updating a 0600
~/.claude.jsonhanded it back at 0644 — one user's session state readable by
every account on the machine, with nothing in the output to say so. A
user-scoped file it creates is 0600 rather than whatever the umask gave.spx init --dry-runreports the size of a file over 16 KB instead of printing
its contents.--global --only claude-mcpmerges one entry into hundreds of
kilobytes of session history, and the preview put all of it on stdout and into
any log that captured the run.spx init --only ''and--only ,are aConfigErrornaming the known
targets. Both parsed to an empty list, which read as "no preference" and
planned the default set — a malformed flag wrote more targets than the flag
exists to narrow to.spx doctorno longer reports.mcp.jsonas pending on a machine whose
default run will never write one.
Documentation
- The README says where the
codexCLI comes from, how thenpxandspx
spellings map onto each other, and which file "the project config" names. It
no longer lists amodelconfig key the loader has never accepted. - The
/modelscapture drops the TLS-interception procedure, which used a live
subscription credential, in favour ofcodex debug models— the findings it
produced are recorded in prose, and they were the part that mattered. Two
unlisted model descriptors are no longer named. - The
initCLI reference stops counting.mcp.jsonamong the files a plain
run always writes, and names thestaleoutcome.
Full changelog: https://github.com/KingPin/SubPixel/blob/v0.2.0/CHANGELOG.md