OpenAI-Compatible Images
The conversation is the primary entry point for image generation and iterative edits. Open the focused canvas only from a concrete image result to inspect the image and express edit intent. Never expose credentials, Authorization headers, or local absolute paths in prompts.
Project binding
Before calling any project-scoped tool, call bind_imagegen_project with the current Codex task's project root as projectRoot. Preserve the returned projectBindingId and pass it unchanged to every subsequent project tool in this task. The project root must come from the current task workspace, never from the Plugin installation directory, MCP cwd, roots, Git discovery, or other local state.
The first bind without an ID issues a new random binding, so do not repeat an initial bind in the same task. After configuration changes, rebind with the existing projectBindingId and the same projectRoot; rebinding the same project is idempotent and refreshes the configuration digest, while changing roots conflicts. MCP persists only a domain-separated digest of the binding ID, not the raw ID. Never substitute, recover, or guess a binding ID from the transport sessionId, roots, MCP cwd, a recent project, or other local state.
A projectBindingId survives MCP process and server restarts. Stop the current operation on project_binding_required or project_binding_invalid; do not scan old state or guess another ID. If a fresh start is required, create a new isolated binding only from the current task project root and continue with the new ID. Old canvas and submission state does not migrate automatically. App-only tools obtain the same ID from standard tool-input.arguments.projectBindingId, not private host fields.
Authentication routes
Read the binding receipt before the first image operation and keep its defaultAuthMode for the task. apikey uses the existing API image tools and supports generation, editing, batches, delivery, and canvas submissions. chatgpt uses the Codex host image handoff for generation and canvas edits. Both routes accept canvas mask annotations as edit guidance. For generation, call prepare_host_image_import with intent: "generate", invoke the host image generation capability once, pass the exact returned local savedPath to stage_host_image_import, then call finalize_host_image_import and collect the committed artifact for processing or presentation under Results. For a canvas edit, read the latest submission context, call prepare_host_image_import with intent: "edit", the unchanged parentImageId, annotationId, and submissionId, and the complete edit request. The tool returns the clean parent image as model-visible content. Use that image together with the current canvas intent to invoke the host image generation capability once, then stage and finalize the returned output. The committed result is a child version linked to the parent and submission. Each handoff accepts only the image produced by the current host call; do not search session JSONL, caches, or historical output directories.
The ChatGPT host handoff processes one image per operation. API Key tools provide count and independent batch tasks. Keep the selected route and do not change to another route automatically.
Platform runtime
The Plugin supports Windows, macOS, and Linux from one archive. Its Python bridge selects python on Windows and python3 on macOS/Linux and requires Python 3.12 or newer. To choose one explicit executable, set OPENAI_COMPATIBLE_IMAGEGEN_PYTHON. An invalid override or failed preflight stops the operation; never probe another command or silently switch runtimes.
Repository safety uses a platform adapter behind scripts/repository_fs.py: Windows uses the Windows adapter, while macOS/Linux use the POSIX adapter. This adapter is Plugin-only and is not part of the Standalone Skill. macOS/Linux do not provide Show in folder; this does not block image generation, editing, artifact reads, annotations, or canvas work.
Routing
Use the Plugin MCP tools for image work, including when another skill delegates generation or expects local input/output files. Do not scan the installation or execute bundled Python files to find an alternative generation entry point. dist/ is the server's private runtime payload; scripts/imagegen.py belongs to the separately installed Standalone Skill and uses a different configuration contract. The explicit migration command below is the only documented direct script operation in this Plugin.
If a required tool or input capability is unavailable, report that specific limitation and stop the dependent operation. Do not create auth.json, reinterpret a CLI failure as missing Plugin configuration, or switch to Standalone or host generation. Follow the binding's authentication route; a user-approved route change remains subject to that route's Plugin handoff and result-display contract. Pass these entry-point constraints to delegated image workers.
- For API Key generation, call
generate_imageonce with a newsubmissionKey, then follow Asynchronous image jobs below. For ChatGPT generation, use theprepare_host_image_import -> host image generation -> stage_host_image_import -> finalize_host_image_importsequence. For multiple API candidates, passcountin that same call. The runtime executes the same number of ordered single-image requests and publishes the ordered group only when all succeed; any failure aborts the group without storing partial candidates. Do not retry or split the group into multiple calls. For transparent delivery, pass top-leveltransparency, neverbackground=transparent; the runtime resolves the route, enhances the prompt once before the API request, and preserves PNG originals. After ordinary generation or host import succeeds, collect the returned IDs and follow Results before the final response. For transparent generation, calldeliver_imageonce per original, then collect the successful derivatives for final delivery under Results. Do not wait for the user to request display. Submission tools return job receipts, not image bytes. - When the user wants to inspect or annotate an image, use Open canvas on its result card. Do not proactively call
open_image_editorafter a concrete result is already displayed; that tool is reserved for the result widget. - To edit an image with API Key, call
edit_imagewith the parent image ID and a newsubmissionKey, then follow Asynchronous image jobs. Preserve the canvassubmissionIdindependently from this job submission key. For ChatGPT canvas edits, follow the Canvas submission sequence below and useprepare_host_image_importwithintent: "edit"; do not calledit_image. For transparent delivery, also pass top-leveltransparencyon API Key edits and calldeliver_imageonce after success. For an ordinary edit or host import, collect the returned child IDs and follow Results before the final response. Do not callget_image_artifactonly to display them. - For API Key requests containing multiple independent generation or ordinary edit tasks with different parameters, call
batch_imagesonce with a newsubmissionKey. For transparency or other local delivery, pass bothtransparencyanddeliveryon that item. Follow Asynchronous image jobs to read ordereditems[].resultvalues: API originals are inartifacts, and per-image delivery state and derivatives are in optionaldelivery.results. Prefer each source's derivatives withdeliveryReady=true; otherwise retain its successful original for delivery selection and report delivery state. Select and render the final delivery set under Results, in groups of up to 10 IDs; intermediate displays do not replace final delivery. Report item failures without retrying generation. Preservemanifest.batchIdwhenmanifest.manifestReady=true; a missing manifest does not reverse successful publication. Mask or canvas submissions remain one separate selected-route edit operation and never enter a batch. - After obtaining a stable image ID, call
deliver_imageonce for transparency, exact dimensions,containor safe margins, grid splitting, preview boards, or deterministic QA. Consume a saved transparency plan when the original has one; passdelivery.transparencyonly to specify or adjust the route for a historical image. WhendeliveryReady=true, collect the derivative IDs for final delivery under Results. If there is no successful derivative, report QA and include the preserved original when it remains useful to the requested delivery. Do not regenerate to repair presentation or local delivery. - Use
get_image_artifactto read an artifact. It returns data without creating a result card. Callrender_image_resultsafter reading when the user needs to view a historical image or it belongs in the final delivery set. Do not repeat an already complete final presentation without a new viewing need. - Use
get_image_batch_manifestwithbatchIdto inspect a batch. It reads an immutable record and does not display images. To display a historical image from it, read by stable image ID and callrender_image_results. - Use
get_image_delivery_receiptwithdeliveryReceiptIdto inspect local delivery or QA. It reads an immutable receipt and does not display images. To display a derivative, render the stable image ID recorded in the receipt. - Use
list_image_modelsto inspect model capabilities. - On an explicit canvas submission, the widget calls
prepare_image_edit_submissiononce to save annotations and obtain a server-issuedsubmissionId. Never call it for unsubmitted annotations.save_image_annotationsis not part of this atomic path.
Asynchronous image jobs
API Key generate_image, edit_image, and batch_images persist a job and return jobId before image work completes. Every intended submission needs a submissionKey (1–128 ASCII letters, digits, dots, underscores, or hyphens, starting with a letter or digit). Choose and retain it before the call. The same key and request in the same project/artifact repository return the same job, including after a lost reply or MCP restart. A changed request with the same key returns image_job_conflict; do not invent another key to retry an uncertain generation. A new key is for a new user-authorized generation intent.
- Preserve
projectBindingId,submissionKey, andjobId. If the submission reply was lost, repeat only the identical submission with its original key to find the job. This lookup does not repeat generation. OncejobIdis known, use status queries. - Call
get_image_jobuntildone=true. For a bounded wait, pass the lastrevisionasafterRevisionandwaitMs=20000; the query waits at most 20 seconds. A query timeout or cancellation stops only that wait. Query the same job again; do not split or resubmit it. Continue working until the requested results are delivered, or a real interrupted/unknown state requires action; returningjobIdalone is not task completion. - Each response contains a page of up to 10 items in input order, with
nextOffsetfor the next page. An offset is an input index, not an event cursor: revisit unfinished items after later revisions. Read every final page, collect successful images and their original/derivative relationships, then select and present the final delivery set under Results. Do not treat the first page or first 10 images as the whole batch. summaryseparates queued, running, succeeded, failed, unknown, cancelled, and localFailed items.items[].artifactIdsmay contain saved originals even when local processing or presentation failed.unknownmeans the provider outcome was not confirmed; it never authorizes automatic regeneration. A failure to return image metadata does not prove no image was produced or billed.- On explicit user cancellation, call
cancel_image_job. It cancels queued items and preserves images. A running item, including an atomic candidate group, can still finish and incur charges. Cancelling a wait does not cancel the job. - After
status=interruptedorlocalFailed>0,resume_image_jobcan continue never-dispatched items and local processing with saved originals. Pass the currentrevisionasexpectedRevision, and reuse that value if the resume reply is lost. It never repeats failed or unknown image requests. If the user cancelled the task, resume only when they ask to continue. Recovery requires the original configuration; do not change models, providers, routes, or authentication to resume it.
Execution belongs to the MCP server process; no persistent OS service is installed. Jobs, checkpoints, and results survive a server restart, but an in-flight request without a saved checkpoint becomes unknown after its execution lease expires (up to 60 seconds). A process crash between image publication and checkpoint persistence can leave an image without a confirmed job result; do not regenerate it automatically. The executor shares eight active item slots across its jobs and honors each batch's smaller concurrency limit. Separate MCP processes do not share that slot limit, but durable ownership prevents them from executing the same job concurrently. Do not create extra processes or submissions to bypass concurrency.
Local image files
For an existing project image, call import_local_image with its project-relative sourcePath (forward slashes). It returns artifact.id, preserves the original bytes, and creates an independent immutable artifact without a provider request. Import works with either configured authentication route.
For API Key edits, pass the imported ID as edit_image.parentImageId; import additional references separately and pass their IDs in referenceImageIds. Check that the selected profile supports edit and, when needed, multi_reference. The Atlas protocol does not support edits. For ChatGPT edits, call render_image_results with the imported ID so the user can open its canvas and submit the edit; continue through the Canvas submission sequence only after that submission. Never substitute a host handoff for importing a local file.
When another skill or script needs a file, call export_image_artifact with the selected stable imageId and a project-relative destinationPath, such as decoded/idle.png. Use the returned relative path under the bound project root for subsequent local processing. Export copies the original bytes, creates missing parent directories, and returns byte length and SHA-256. It never overwrites files or transcodes formats: match the filename extension to the artifact MIME type. Export the selected successful delivery derivative when the workflow needs a resized or transparent result.
Both paths must stay inside the bound project and outside its artifact repository. Absolute paths, traversal, symbolic links, junctions, and other reparse points are rejected. Imports are limited to 64 MiB and 100 million pixels. For files outside the project, explain this boundary instead of scanning or switching entry points. Do not use exported files to overwrite managed artifact storage. Import/export do not replace the required result rendering after generation, editing, or delivery. Apart from the ChatGPT canvas entry described above, render an imported reference only when the user needs to inspect it.
Batch tasks
batch_images accepts 1 to 64 independent items with unique requestId values. Each count is 1 to 16, the sum of all counts is at most 64, and concurrency is 1 to 8. Omitted concurrency uses the configured default. Each item is an ordinary generate or edit. An OpenAI-compatible advanced batch item with count=N uses one provider request with n=N; Atlas collects one response per candidate before group publication. Same-prompt conversation candidates still use one generate_image(count=N), preserving N independent single-image requests and atomic group publication; never replace that route with advanced batch.
Job item results preserve input order and allow partial success per task and per returned image. result.ok=true means at least one deeply validated API original was published. result.apiDelivery records provider count, published count, and safe issue codes. Delivery success is result.delivery.deliveryReady. A delivery failure preserves originals and is reported as local_failed; it does not reclassify the result as generation failure or trigger model, endpoint, protocol, or API retries. Once manifest.manifestReady=true, use manifest.batchId to read the immutable manifest. A recording failure does not reverse image publication.
Local delivery
Use deliverySize for exact dimensions; fit=contain with safeMargin to preserve aspect ratio and padding; grid, expectedCount, and per-cell deliverySize for a known sheet layout; preview.sizes and preview.backgrounds for multi-size and multi-background inspection; qa=true for technical checks; and components=true when connected-component metrics are needed.
deliver_image reads one stable source image. The original stays immutable. Successful resized images, grid cells, and preview boards are stored with operation=derive, separate stable IDs, and a derivedFrom relationship outside the edit-version tree. Pass derivative IDs to render_image_results only when deliveryReady=true. When false, report qa and warnings; do not invent derivatives, transcode automatically, change format or model, or retry. Current local transforms accept PNG sources only. Preserve complete originals in other formats and report delivery as not ready.
For transparent delivery, choose transparency.route: chroma-matting for ordinary isolated subjects; emissive-alpha for black-background glow, fire, or particles; mask-alpha with a stable maskImageId when a deliberate mask image exists. Choose prompt-alpha only when configured prompt_only_allow exactly matches model, operation, and size. Otherwise preserve the original prompt and inspect the API original's alpha instead of claiming success. Never guess or generate maskImageId, and never substitute a canvas edit mask for a transparency-delivery mask.
Ordinary API requests omit background by default. Pass auto or opaque only when the user explicitly requests that API option. If an explicit background option is rejected, explain the failure and ask before submitting a new request without it; do not infer parameter rejection from a timeout or an ambiguous error. The configured, limited retry for native transparency remains unchanged.
For API Key generation or editing with native transparency, use transparency.default_route or pass transparency.route: "native-alpha". This requires transparency.native.enabled=true and the openai-compatible protocol; Atlas rejects native transparency. The runtime sends background: "transparent" and PNG output, with a prompt requiring a real alpha channel. transparency.native.model_ids is an optional user-maintained capability declaration, not a code whitelist; the provider decides whether the configured model supports the parameter. If the provider rejects the transparency parameter with a transparency-related HTTP 400/422, the default behavior is one retry with that parameter removed, using the same model, provider, endpoint, prompt, size, quality, references, and mask. The retry can be disabled with transparency.native.retry_without_parameter=false. A successful retry, or an opaque native response, enters the configured local fallback_route only when local processing is enabled; otherwise it inspects source alpha without changing pixels. Keep the API original immutable and retain validated derivatives when processing succeeds. Do not enable local fallback merely because API retry is enabled. These provider settings do not control ChatGPT host generation.
Transparency runs before resizing, grids, and previews. If it fails, stop later derivatives and report that the original remains usable. Every transparent result records whether the native parameter was sent, rejected, retried, the final route, API attempt count, warnings, and QA status. The final response must explain the route and any fallback. When parameters.transparency.llm_assisted.enabled=true, perform at most max_attempts local redelivery attempts, adjusting options or changing local routes only when the corresponding switches allow it.
Even if policy contains allow_api_retry, do not request the image API again through that setting. The native-parameter retry described above is the only configured API retry and is limited to one same-request fallback.
Canvas submission
On a canvas submission message, read the latest model context fields: projectBindingId, submissionId, imageId, annotationId, prompt, annotationCount, intents, and requestText. When the task contains multiple canvas contexts, use the newest submissionId associated with the current user message and never merge earlier submissions. For the API Key route, pass projectBindingId unchanged to edit_image, pass submissionId unchanged as edit_image.submissionId, and pass imageId as edit_image.parentImageId. For the ChatGPT route, call prepare_host_image_import with route: "chatgpt", intent: "edit", parentImageId: imageId, annotationId (including null), and submissionId, then use the returned clean parent image with the current edit intent for the host image generation call. Do not call prepare_image_edit_submission or save_image_annotations again.
Combine the annotation preview, per-region instructions, and additional request into one prompt describing the complete target image, then call the selected edit route once. Mask annotations always remain part of that semantic edit request. For API Key edits, MCP sends the dedicated mask parameter only when the selected model profile declares capabilities.mask: true; otherwise it sends the normal edit with the same annotation and submission relationship. The ChatGPT route receives the semantic annotations and clean parent image as separate model-visible inputs. For protected content, name what must remain and what lighting or shading may adapt naturally to the scene. Treat adherence to marked regions as model-dependent. Relay only the user's target; never author, append, or override MASK_GUARD_V2_BY_STRATEGY. Stop if the image ID, submissionId, or edit intent is missing. Never guess an ID or switch to generate_image or another route.
After the selected edit route succeeds, collect the child image ID and parent-child version relationship. Follow Results for intermediate previews and final delivery in the current conversation. The new result retains its Open canvas entry.
Canvas lifecycle
Treat the editorSessionId returned by open_image_editor as the active canvas session for this task.
- Call
destroy_image_editorwhen the user explicitly asks to destroy the canvas. - Call it when the task has clearly moved to another objective and the current image will no longer be viewed, annotated, or edited.
- Do not destroy the canvas when the user only hides or closes the side panel, discusses another topic temporarily, waits for another generation, or may continue working on the image.
- Never guess an unknown active
editorSessionIdor call the destroy tool without it.
The canvas Destroy canvas button and destroy_image_editor share one lifecycle. Destruction ends all active canvas sessions for that image within the current project binding and permanently removes its reopen entry for that binding. The state survives MCP process restarts; other project bindings are unaffected, and image artifacts and version relationships remain. After destruction, do not call open_image_editor or render_image_results for the same image to restore the entry.
The active profile and model ID come from user configuration. Model capabilities come from the configuration declaration and provider response; do not infer them from a model name. After failure, do not switch model, endpoint, provider, or edit route.
Configuration
For API Key requests, quality accepts auto, low, medium, high, xhigh, and max. GPT Image 2.5 Sunburst and Flare support the new levels; other models and providers may support only a subset. Atlas still accepts only low, medium, and high. Preserve explicitly requested quality and report unsupported values without downgrading or switching models. ChatGPT host generation does not expose API model or quality selection. list_image_models returns activeProfile alongside models; resolve the active model by this ID, never by a fixed profile name or the first catalog entry.
The Plugin resolves configuration only from:
- User configuration:
~/.codex/openai-compatible-imagegen/config.json - Optional project overrides:
<project-root>/.codex/openai-compatible-imagegen/config.json
The user configuration must exist and provides the trusted baseline with config_version: 1, auth_mode, defaults, post-processing, and storage. API Key configuration also declares an active profile, provider, user-selected model ID, and transparency policy; ChatGPT-only configuration may omit those API fields. The project file is validated independently before reading the user file. It may override only defaults.size, defaults.quality, defaults.output_format, and storage.output_directory. It cannot declare or indirectly change profiles, models, provider, endpoint, authentication, credential environment variables, timeout, concurrency, or route permissions. Invalid or excessive project configuration fails without being ignored or falling back, and before user credentials or network requests are accessed.
An optional user-level provider proxy.url routes generation, edit, and provider-returned image URL requests through one HTTP proxy. It must be a complete http:// or https:// URL without credentials, path, query, or fragment; SOCKS proxies are unsupported. Project configuration cannot declare or override it. Configuration tools and runtime errors may report that a proxy is configured but must not expose its URL. After a proxy change, rebind the project so the new configuration digest becomes effective.
Effective priority is explicit tool arguments, then allowlisted project overrides, user defaults, and built-in defaults. When user configuration is missing, stop image requests and use the configuration tools below to initialize the selected route; references/config.example.json is the manual setup alternative. Legacy auth.json and older Plugin configuration are never read, copied, merged, deleted, or overwritten automatically. Migration requires an explicit user command and never reveals API keys.
Use MCP configuration tools for the complete Plugin flow. Call initialize_image_config to create the fixed user template when missing; it always creates a .gitignore containing only * in the user configuration directory and, when given a project root, protects the project configuration directory without changing the project root .gitignore. Use inspect_image_config to read configuration and update_image_config to change it, which adds the same directory protection before writing. Prefer api_key_env. When the user explicitly requests local plaintext credentials, user-level api_key may be written but never returned. Project scope forbids credentials and remains limited to size, quality, output format, and storage.output_directory. Rebind the image project after updates.
Explicit migration
Proceed only when the user explicitly requests migration and provides the source path and source type. Resolve <plugin-root> by going two levels above this SKILL.md; never infer it from MCP cwd, a project Git root, or another installation cache.
Run a redacted dry run for a legacy Standalone configuration.
Windows PowerShell:
python "<plugin-root>/dist/scripts/migrate_image_config.py" --source "<legacy-config>" --source-kind standalone
macOS or Linux shell:
python3 "<plugin-root>/dist/scripts/migrate_image_config.py" --source "<legacy-config>" --source-kind standalone
For an older Plugin configuration, use the retained compatibility value --source-kind development-plugin. Add --include-project-overrides --project-root "<project-root>" to both dry-run and write only when the user explicitly wants allowlisted defaults and output directory written to the current project.
Report sourceKind, sourceSha256, userTarget, projectTarget, readyToWrite, and the redacted preview. After user confirmation, preserve the source path, source type, user directory, and project override arguments, then run the matching platform command.
Windows PowerShell:
python "<plugin-root>/dist/scripts/migrate_image_config.py" --source "<legacy-config>" --source-kind standalone --write --expected-source-sha256 "<sourceSha256>"
macOS or Linux shell:
python3 "<plugin-root>/dist/scripts/migrate_image_config.py" --source "<legacy-config>" --source-kind standalone --write --expected-source-sha256 "<sourceSha256>"
If readyToWrite=false requests plaintext-key authorization, stop. Add --allow-plaintext-api-key only after the user separately approves plaintext-key migration. On digest mismatch, existing target, incompatible schema, or write failure, report the original migration error and stop. Do not change source, target, route, or authentication method. Preserve the source file after success; never delete or rename it automatically.
Optional storage.output_directory must be a safe project-relative path. When absent, use <project-root>/output/imagegen/. Reject the project root itself, paths outside the project, files, symbolic links, junctions, and other reparse points. Project binding creates or verifies a .gitignore containing only * in the artifact directory. Add a missing rule; stop without overwriting when the rule is wrong or the path unsafe. Binding freezes configuration; rebind the same project explicitly after changes.
The artifact root selects one active repository. Once an override is effective, images, versions, annotations, masks, submission recovery, and Show in folder read only that directory. Never scan, merge, migrate, or copy older artifacts from the default directory. After removing the override and rebinding, older artifacts in the default directory become available again.
Without provider proxy.url, image API requests and URL downloads retain the environment proxy behavior. With it, all three request types use the configured proxy. Set provider url_download.proxy_mode to direct only after explicit user approval; this overrides the configured or environment proxy only for provider-returned image URL downloads. Never change proxy settings or retry through another network route after TLS, proxy, or network failure.
Results
Image production and task delivery have different boundaries. Apply this workflow across generation, edits, imports, batches, and derivatives, even when other work happens between image operations or the task ends by asking the user to choose a direction.
- Collect stable IDs from successful operations throughout the task. Keep the intended deliverable or candidate, order, original/child/derivative relationship, processing status, and any intermediate presentation associated with each ID in the task context. Do not infer the collection from the latest artifact, scan project history, or create a separate project-wide image ledger.
- Use intermediate result cards when the user needs to inspect progress, choose a direction, or explicitly view an image. Otherwise collect results while work continues. One successful tool operation does not require an immediate card and does not mean the overall task is complete.
- Before a final response, including partial completion or a request for user selection, reconcile the delivery set with the user request. Include requested independent deliverables and all candidates still needed for comparison. Prefer the final edited version over a superseded draft and delivery-ready derivatives over their source, unless the source or comparison is also requested. Exclude reference-only images, probes, rejected candidates, and superseded versions unless needed for the requested outcome. Never discard a requested deliverable merely because of aesthetic preference.
- Render that ordered delivery set with
render_image_results, at most 10 distinct IDs per call. Group larger sets by purpose and keep numbering and names unambiguous across groups. An intermediate display does not satisfy final delivery: previously displayed IDs may be included again to provide a complete final set. Reuse existing IDs without regeneration, copying, or re-import. If a complete final set has just been rendered and no delivery decision changed, do not render it again merely to end the turn. - Check expected deliverables against successful IDs, selected versions, and final render arguments before describing completion. Report missing or failed items accurately; keep unknown outcomes pending under their original request rather than regenerating. If local delivery failed but the original remains useful, include it and explain the limitation. A render error requires resolving the presentation step, not repeating image production. A successful tool response confirms preparation of the result content, not that the host visibly rendered every image; never invent UI verification.
For example, images generated at different stages are collected into one final set; an edited child replaces its draft unless comparison is needed; a candidate shown midway may appear again beside later candidates; partial success includes usable results with an accurate failure explanation. When no image belongs to the current delivery, do not add a result card just because an image was used internally.
render_image_results is the sole result-widget entry. It accepts one or more stable image IDs, returns model-visible image content and safe metadata in input order, and provides a separate Open canvas entry for each canvas that has not been destroyed. The selected final delivery set must be presented automatically before the final response; the user should never need to ask for the generated image to be shown. Destroyed images remain visible, but their entry reads Canvas destroyed and cannot be used.
The result widget treats only standard ui/notifications/tool-input.arguments.imageIds from render_image_results as image identity. It never guesses from result text, private metadata, or the newest artifact. Each image is read once through App-only read_image_artifact_data. Stop that card on mismatched request ID, public artifact ID, private widget data ID, or MIME. Do not call get_image_artifact or switch read routes.
Return the result images, stable IDs, corresponding canvas entries, version relationships, and safe error summaries to the user. Editing creates a new version and never overwrites its parent. Images and annotations stay in the project artifact root resolved from the binding configuration. That local path never enters tool results, the widget, or model context.