An image with a side above the deployed routes' 2000px many-image bound
could be durably committed by read_image, ride every later request, and
permanently fail the session with provider 400s. Admission now enforces a
configurable maxImageDimension (default 2000) during the full decode, so
read_image surfaces a recoverable tool error naming the limit instead of
poisoning durable history; the Web composer gets dedicated copy for the
new IMAGE_DIMENSION_TOO_LARGE reason.
Fixes#2626
master introduced AttachmentStore.saveImages as the batch admission
(count/aggregate-byte/media-type limits, validate-all-before-save,
ordered commit). admitEncodedImages narrows to the shared wire entry:
canonical-base64 enforcement plus delegation to saveImages, keeping one
home for batch policy while both wire endpoints (prompt RPC and the
command executor) still call one function. Test doubles gain saveImages;
batch-limit error texts follow saveImages' wording.
A claimed slash command consumed only the text half of the composer
submission: /goal with reference images executed, cleared the draft, and
silently stranded the images in the rail. Model-visible attachment intent
had no route through the command plane.
The submission envelope is now modeled end to end. CommandDefinition
input.images declares acceptance; the declaration rides the descriptor to
every client, onto the minted CommandClaim, and into the input machine's
claim snapshot. commands.execute carries the submission's base64 images
and enforces the declaration in the executor: non-declaring commands, a
missing attachment store, and exceeded batch limits settle as logged
error results before the handler runs. Admission reuses the attachment
package's new admitEncodedImages, extracted from api-proxy's prompt path
so both wire endpoints share one limits/validation/commit sequence.
Producers own model visibility: /goal submits one user followup (image
blocks + a fixed reference line) after a successful create/edit so goal
rounds read the images from session history; /plan folds them into its
steered message. Grammar misfits (/goal pause, bare /plan, /plan off)
return direct errors and the composer keeps the images.
On the client, enter adjudication carries a SubmitEnvelope and every
command route that cannot consume images throws a localized refusal that
renders as one composer notice with draft and images retained; the
claimed pre-gate applies the same copy. An accepting claim serializes the
draft images, forwards them to commands.execute, and clears plus releases
them only on a success outcome.
The assembled web test roster gains the ui-input-trigger and ui-commands
plugins, mirroring the shipped composition, so slash submissions exercise
the command plane; a new keyless snapshot pins the refusal banner and the
accepting /goal flow over the built client graph.
Every release member now declares publishConfig.access: public, so the scope no
longer mixes levels: the 221 packages/*/* and apps/* manifests join the vendored
framework and the native packages.
check-workspace-constraints drops the per-sequence expectation and holds every
release member to public, which is what stops a member from drifting back.
Access is a property of the package, not of a version: the dsh packages already
published as restricted become world-readable at their next publication.
A plugin that registered a settings namespace could not reach the browser
configuration page: the api-proxy filtered every read and gated every
write through two hardcoded namespace lists, and the plugin configuration
section rendered an unordered list of cards carrying an opaque id rather
than the namespace they edit. Both gates lived in this repository, so a
user-authored plugin was configurable only by hand-editing settings.yaml.
The proxy now serves whatever ctx.settings.describe() returns and adds no
boundary of its own; a name no registration answers folds into the seam's
own settings-rejected, and the settings-not-exposed code retires. The
settings seam is untouched: which client may read a namespace, and which
page renders it, are facts about consumers.
settings.plugin.item becomes a keyed slot whose key is the namespace a
card edits, following tool.call.toolview. The section reads describe once
and dispatches the intersection of the slot ledger and the served set, so
a namespace another surface owns renders nothing without declaring
anything, and a card for an uncomposed plugin is never dispatched.
Second alignment step for #2248: document-level drag intake behind the new
DropOverlay atom, lightbox close icon and shared dialog mask, DeepSeek Chat
thumbnail rules (single 240px long edge with ratio clamp, 64px tiles, merged
consecutive assistant images), image limits raised to 20/10MiB/100MiB and
published to clients as the imageLimits projection, whole-batch intake
pre-check with product-copy banners, and attachment-error reasons mapped to
localized copy.
Review follow-ups that are logic rather than documentation:
- rpc.schema.ts had lost the credential-rejected branch while api-proxy.ts
still returns that code, so a legitimate business error failed the
client's response parse. Restore the branch and assert it.
- rpc-schemas.spec.ts had lost the workspace list, archiveSession and
insertSessionBefore cases along with the command schemas; those routes
still ship, so restore their coverage.
- An omitted SRC field is now recognized by an absent key instead of an
undefined value, which makes the allowance assertExactArguments already
granted reachable; an explicitly undefined field stays invalid input. A
weak descriptor's undefined result rides the wire as an absent value,
matching the envelope removal.
- The chooser unmounts an already-created backend when the surface entry
fails to load, and no longer reverses the captured id array in place.
`command.execute` no longer exists on the API Proxy, and the fake's own
handler went with it, so the case only reached a 404 body. The same carrier
behaviour is asserted on live routes by its `session.search`,
`subagent.prompt` and `host.pickDirectory` siblings.
`CommandService.list` and `execute` carry the wire contract directly through
`@Remote`, and the Client assembly mounts the generated commands
contribution. The legacy API Proxy route, its schemas, the map rows, the
generated client methods and the fixture's command domain are removed, so the
catalog and the admission call have one owner again.
`Session.command()` keeps a result-shaped public face for parity with the
prompt, cancel and attachment neighbours it sits beside, and reads the
generated namespace through one `SessionRemotes` parameter. The Session
cluster declares that face against the owning business package rather than the
generated contribution: the Host compiler aggregate builds this package, and
it runs before any contribution is emitted.
Migrated calls lose the `title-invalid` class of protocol-only error codes and
report `internal`; no production caller branched on them.
Absence crosses the wire as a missing field: an omitted argument and a void
or undefined result both arrive as an absent JSON member, and the wide RPC
result slot accepts a success response without a value. Parameters declared
optional stay optional in the generated consumer declaration, so a business
signature is never widened to `T | undefined` to suit the wire. The weak SRC
descriptor reads parameter names from a JavaScript signature and cannot see
optionality, so a source-launched Host accepts an absent field and the strict
LIB pass owns rejecting a genuinely missing required parameter.