Skip to content

v0.4.0

Choose a tag to compare

@KingPin KingPin released this 19 Sep 03:11
· 11 commits to main since this release

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>.json records the reference images a generation used, and spx 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 running regen chose. 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
    --image explicitly.
  • doctor over 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. accountId is 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 doctor and spx models write through redact(). Every other command
    in the CLI already did. doctor reads auth.json and 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 used key in schema.properties, which is true for constructor,
    toString, valueOf and __proto__. JSON.parse delivers __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 exec child that emits error after 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.
    slugify trimmed -+ 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.md now states what redact() 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. cached reached the caller only
    in the result object, which only --json publishes, so an agent watching
    progress saw a free hit and a paid generation render identically. done
    now carries it and reads done (cached) on both the CLI's stderr and an MCP
    progress notification.

  • --cache-only / cache_only asks 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_MISS is an answer, not a breakage, in the
    same way spx sync --check reports 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_run on generate_image and edit_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_cache on generate_image and edit_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,
    and no_cache skips those banked bytes too. force and overwrite stay out.

  • The MCP server bounds how many spending calls it runs at once. Two by
    default, mcp.concurrency in the project config or SUBPIXEL_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 its job_id at the cut-over, so the queue never holds
    a host's request open. Calls that cannot spend — dry_run, cache_only,
    sync_assets with check, and every read-only tool — never queue.

Changed

  • spx sync sends 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. --json already had this right.
  • spx doctor keeps FAIL for the rows that made it exit 1. A missing
    codex binary, an editor without spx init, a zero-tool MCP server and a
    nearly-spent allowance all printed FAIL while the command exited 0. None of
    them stops an image being generated, so they print warn now, and an agent that
    greps FAIL sees only the reasons the exit code is non-zero.
  • spx edit no 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.
  • writeImage hashes the image once per write rather than once per sibling
    name tried.
  • The bare-prompt shorthand needs more than one word. spx fox was rewritten
    to spx 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-size no 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 icons refuses 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 exec behind --. codex exec has subcommands of
    its own, so a prompt of review ran the review subcommand, and --image is
    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 -1 closed
    stdout under the process, and the next write raised an error event with no
    listener: a stack trace and exit 1 for a run that had already written the image
    and printed its path. Only EPIPE is swallowed.
  • spx doctor reads the quota of the project it was pointed at. It resolved
    .subpixel/quota.json against process.cwd() and ignored the working directory
    it was given, so under MCP the quota section reported a directory that was not
    the project.