Two lifecycle defects in the managed HTML artifact preview landed with #5316 (merged as 512605fd10f1cef0a9f65c1605c15eb5e2cf31fb). Both were reported as P2 on the PR before it merged — by the Qronos review line, and independently confirmed against the source by me — so this issue exists to give them a schedulable home rather than leaving them in review comments on a merged PR.
The endpoint's isolation properties are not in question here: loopback-only binding, the 256-bit bearer path, exact Host and whole-string URL matching, GET/HEAD only and the CSP all hold. These are lifecycle problems.
1. A deleted artifact stays served until its TTL expires
runtime-host-artifacts-ipc-main.ts calls preview.service.revoke(...) after deleteArtifact, and that is the only artifact deletion in apps/desktop/src. Deletion can originate elsewhere, and none of those paths notify ManagedArtifactPreview:
packages/runtime-host/src/server/deep-research-coordinator.ts:91 — an agent deleting its own artifact via deleteOwnedArtifactInSession.
packages/runtime-host/src/server/session-sidecar-purge.ts:36 and session-retirement-coordinator.ts:125 — purgeSessionArtifacts(sessionId) during session teardown.
packages/runtime-host/src/server/artifact-coordinator.ts:70 — the artifact.delete operation itself carries no coupling to the preview service; the revoke lives one layer up, in the Desktop caller.
closeScope does not cover this either: it is keyed on targetEpoch, not sessionId, so purging a session's artifacts leaves that session's leases running. The lease then serves the deleted artifact's bytes for the remainder of its 30-minute TTL.
The disclosure is bounded — the bearer path was already handed to the browser that opened the preview, so no new party gains access. The problem is that deleting a generated document is exactly the action a user takes to stop it being readable.
2. The preview quota is global, so one session can starve every other
apps/desktop/src/main/managed-artifact-preview.ts:71 tests this.leases.size >= MAX_PREVIEWS with no partition by scope or session. With MAX_PREVIEWS = 16, a 30-minute TTL, no eviction, and ArtifactPreview published as a model-callable tool, one session can hold every lease and deny previews to all other sessions for half an hour. The thrown message — "Too many active previews; wait for expiry" — describes the situation accurately: there is no recourse short of waiting.
Suggested directions
For (1), the revoke belongs at the layer that owns deletion rather than at one caller of it, so every path reaches it; alternatively the preview service could key leases by sessionId so session teardown releases them. For (2), partitioning the bound per session, or evicting the oldest lease instead of refusing the newest, would both remove the cross-session denial.
Filed by an automated agent (@kabi-opus) through a shared account, at the request of the review orchestration line. The two defects were first raised by the Qronos review line on #5316; the file and line references above are my own tracing of the source. This does not substitute for independent human review.
Two lifecycle defects in the managed HTML artifact preview landed with #5316 (merged as
512605fd10f1cef0a9f65c1605c15eb5e2cf31fb). Both were reported as P2 on the PR before it merged — by the Qronos review line, and independently confirmed against the source by me — so this issue exists to give them a schedulable home rather than leaving them in review comments on a merged PR.The endpoint's isolation properties are not in question here: loopback-only binding, the 256-bit bearer path, exact
Hostand whole-string URL matching, GET/HEAD only and the CSP all hold. These are lifecycle problems.1. A deleted artifact stays served until its TTL expires
runtime-host-artifacts-ipc-main.tscallspreview.service.revoke(...)afterdeleteArtifact, and that is the only artifact deletion inapps/desktop/src. Deletion can originate elsewhere, and none of those paths notifyManagedArtifactPreview:packages/runtime-host/src/server/deep-research-coordinator.ts:91— an agent deleting its own artifact viadeleteOwnedArtifactInSession.packages/runtime-host/src/server/session-sidecar-purge.ts:36andsession-retirement-coordinator.ts:125—purgeSessionArtifacts(sessionId)during session teardown.packages/runtime-host/src/server/artifact-coordinator.ts:70— theartifact.deleteoperation itself carries no coupling to the preview service; the revoke lives one layer up, in the Desktop caller.closeScopedoes not cover this either: it is keyed ontargetEpoch, notsessionId, so purging a session's artifacts leaves that session's leases running. The lease then serves the deleted artifact's bytes for the remainder of its 30-minute TTL.The disclosure is bounded — the bearer path was already handed to the browser that opened the preview, so no new party gains access. The problem is that deleting a generated document is exactly the action a user takes to stop it being readable.
2. The preview quota is global, so one session can starve every other
apps/desktop/src/main/managed-artifact-preview.ts:71teststhis.leases.size >= MAX_PREVIEWSwith no partition by scope or session. WithMAX_PREVIEWS = 16, a 30-minute TTL, no eviction, andArtifactPreviewpublished as a model-callable tool, one session can hold every lease and deny previews to all other sessions for half an hour. The thrown message — "Too many active previews; wait for expiry" — describes the situation accurately: there is no recourse short of waiting.Suggested directions
For (1), the revoke belongs at the layer that owns deletion rather than at one caller of it, so every path reaches it; alternatively the preview service could key leases by
sessionIdso session teardown releases them. For (2), partitioning the bound per session, or evicting the oldest lease instead of refusing the newest, would both remove the cross-session denial.Filed by an automated agent (
@kabi-opus) through a shared account, at the request of the review orchestration line. The two defects were first raised by the Qronos review line on #5316; the file and line references above are my own tracing of the source. This does not substitute for independent human review.