Repository navigation
v0.4.0
Gives an agent the controls it needs to spend less, and works through what the
process hands to whoever is on the other end of it. The themes are asking the
cache a question instead of paying to learn the answer, bounding how much can be
spent at once, and confining what a file on disk is allowed to make the tool do.
Security
- A sidecar can no longer replay a reference image from outside the project.
<image>.jsonrecords the reference images a generation used, andspx regen
read those paths back and uploaded them again. A sidecar travels with its image
out of a pull request, a cache, or a colleague's directory, so the paths in one
are not necessarily paths the person runningregenchose. They are now
resolved against the sidecar and confined to the project the image lives in.
The project root is found by walking up from the image, not from the shell,
so a sidecar still replays from any working directory. A reference outside it is
refused with an error that says to copy the file into the project or pass
--imageexplicitly. doctorover MCP no longer returns the ChatGPT account id or absolute
paths. The CLI report is written for a person looking at their own machine;
the same object was handed to whatever model the host runs.accountIdis not a
credential but it is a stable identifier for a paying account, and the paths
carry the user's name and the shape of their disk. The MCP view drops the
account id and reduces paths to basenames, which keeps the whole diagnosis and
withholds only the address. That covers the free-text fields too: both the auth
problem and the install problem are built out of paths, so masking the path
fields alone would have left the path in the sentence beside them.spx doctorandspx modelswrite throughredact(). Every other command
in the CLI already did.doctorreadsauth.jsonand reports what went wrong
with it, which makes it the command with the most to spill, not the least.- MCP argument validation rejects names inherited from
Object.prototype.
The gate usedkey in schema.properties, which is true forconstructor,
toString,valueOfand__proto__.JSON.parsedelivers__proto__as an
ordinary own property, so that one arrived from the wire. Nothing downstream
spread the argument bag, so no live exploit was closed. - A
codex execchild that emitserrorafter forking is killed. The
provider resolved on the error and cleared its kill timer, leaving a live child
that could still finish the generation and bill for it. - The filename slug no longer runs a backtracking-shaped regex over the
prompt.slugifytrimmed-+anchored at the end of a string it had just
built, which is the shape that backtracks polynomially. It only ever stayed
harmless because the collapse above it cannot produce a double dash, and that is
an invariant a later edit could break without noticing. The trim now matches a
single dash. Slugs are unchanged; this removes the shape, not a behaviour. SECURITY.mdnow states whatredact()does not cover. It masks the shapes
subpixel's own credentials take. It is not a general secret scanner: it knows no
third-party vendor prefixes, so a key pasted into a prompt is stored in the
manifest as prompt text.
Added
-
A cache hit says so on the progress line.
cachedreached the caller only
in the result object, which only--jsonpublishes, so an agent watching
progress saw a free hit and a paid generation render identically.done
now carries it and readsdone (cached)on both the CLI's stderr and an MCP
progress notification. -
--cache-only/cache_onlyasks the cache a question instead of spending.
Serve this request if it is already paid for, and exit 7 (CACHE_MISS)
without spending if it is not.CACHE_MISSis an answer, not a breakage, in the
same wayspx sync --checkreports drift, so a CI job can gate a build on
whether its assets are covered. Bytes this project already paid for count as a
hit even when they still need local work: a request whose raw generation is
banked but whose crop failed last run is re-processed locally and reported as a
hit, because serving it spends nothing. -
dry_runongenerate_imageandedit_image. Reports the backend chain,
the driver model and where it came from, the effective prompt, the output
directory and the cache key, then stops. No network call, no quota, nothing
written. The key it reports is the key the real call looks up, so an agent can
tell a hit from a miss without paying to find out. The planner takes no provider
and has no argument that could carry one, so the zero-quota guarantee is a
property of the signature rather than of a branch being correct. -
no_cacheongenerate_imageandedit_image. Earlier releases withheld a
cache bypass from agents on the grounds that an agent given one will use it. An
agent that wants a different picture for the same prompt and has no bypass
appends noise to the prompt instead, which spends exactly the same quota and
leaves an entry under a prompt nobody will type again. It is not a retry: this
tool banks the bytes it paid for before post-processing, so a call that failed
after the image was drawn is often recovered by an ordinary retry at no cost,
andno_cacheskips those banked bytes too.forceandoverwritestay out. -
The MCP server bounds how many spending calls it runs at once. Two by
default,mcp.concurrencyin the project config orSUBPIXEL_MCP_CONCURRENCY
in the environment. Per-call concurrency bounds one batch; this bounds the
process, which is the only thing that knows how many agents are talking to it.
A queued call still gets itsjob_idat the cut-over, so the queue never holds
a host's request open. Calls that cannot spend —dry_run,cache_only,
sync_assetswithcheck, and every read-only tool — never queue.
Changed
spx syncsends its progress to stderr in every mode. README: stdout
carries the path of the written file and nothing else. The default mode put the
per-asset lines on stdout, where a caller reading it to learn what was produced
could not tell a progress line from an answer.--jsonalready had this right.spx doctorkeepsFAILfor the rows that made it exit 1. A missing
codexbinary, an editor withoutspx init, a zero-tool MCP server and a
nearly-spent allowance all printedFAILwhile the command exited 0. None of
them stops an image being generated, so they printwarnnow, and an agent that
grepsFAILsees only the reasons the exit code is non-zero.spx editno longer offers--concurrency. It turns one image into one
image; there was nothing for the number to do.- SSE parsing reads each chunk once instead of rescanning the whole buffered
line per chunk. The old shape was quadratic in the length of a line, so a large
streamed response cost more the longer it got. writeImagehashes the image once per write rather than once per sibling
name tried.- The bare-prompt shorthand needs more than one word.
spx foxwas rewritten
tospx generate fox, so a mistyped subcommand spent a generation instead of
reporting a typo. A single word is now reported as an unknown command.
spx "a red fox"is unchanged, and a genuinely one-word prompt is
spx generate fox.
Fixed
--exact-sizeno longer re-encodes JPEG and WebP at sharp's default
quality. Resizing an image the backend had already returned put it back
through the encoder at whatever default sharp chose, so asking for an exact
size silently cost quality on an image that had already been paid for. The
requested quality is carried through the resize.spx iconsrefuses an occupied destination before it builds the pack,
rather than after. The refusal came after the work, so a run that could never
write its output still spent the time and memory to produce it.- The prompt reaches
codex execbehind--.codex exechas subcommands of
its own, so a prompt ofreviewran the review subcommand, and--imageis
variadic, so a trailing positional could be absorbed into the file list. A
prompt beginning with a flag reached the child's parser, which is where this
provider's sandbox flags are decided. - The child's stderr is bounded, and read from the tail. codex streams
progress and reconnect lines there for as long as a run lasts, so the buffer
grew with the run while only 500 characters were ever reported — and they were
the first 500, which is whatever codex warned about on the way up rather than
the reason the run failed. - A closed pipe ends the run quietly.
spx generate ... | head -1closed
stdout under the process, and the next write raised anerrorevent with no
listener: a stack trace and exit 1 for a run that had already written the image
and printed its path. OnlyEPIPEis swallowed. spx doctorreads the quota of the project it was pointed at. It resolved
.subpixel/quota.jsonagainstprocess.cwd()and ignored the working directory
it was given, so under MCP the quota section reported a directory that was not
the project.