Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
16710d5
test: expose missing JSX and written HTML artifact detection
teamleaderleo Oct 25, 2026
f051567
feat: render offline interactive chat artifacts
teamleaderleo Oct 25, 2026
b64dd71
Merge feat-cmux-next into nx-artifact-html-rebase-1025
teamleaderleo Oct 26, 2026
7560a4b
test(agent-chat): preserve written artifact save names
teamleaderleo Oct 27, 2026
005860e
fix(agent-chat): keep written artifact filename on save
teamleaderleo Oct 27, 2026
aaee38b
fix(agent-chat): preview artifacts in built gallery frames
teamleaderleo Oct 27, 2026
b864c0e
style(agent-chat): format gallery artifact route regression
teamleaderleo Oct 27, 2026
8928e73
Merge commit 'aa82735368f69b31aafcd6666304502a7716bc62' into nx-artif…
teamleaderleo Oct 27, 2026
2b5c9a7
test: cover safe artifact previews and write revision dedup
teamleaderleo Oct 27, 2026
9e2f9d1
fix: keep automatic artifact previews scriptless
teamleaderleo Oct 27, 2026
fbfd741
test: keep canonical identity across raw artifact overwrites
teamleaderleo Oct 27, 2026
127e59d
fix: preserve canonical paths in raw artifact writes
teamleaderleo Oct 27, 2026
b71fd15
test(cef): deny artifact documents page file authority
teamleaderleo Oct 27, 2026
a47a65d
fix(cef): isolate offline artifact documents on the shared page host
teamleaderleo Oct 27, 2026
a342922
Merge commit 'a47a65d01ca5b1a4e8168c520164ec37d15988d4' into nx-artif…
teamleaderleo Oct 27, 2026
4b60200
test: cover shared WK artifact frame policy
teamleaderleo Oct 27, 2026
bfbb902
fix: isolate WK artifact documents on the shared page host
teamleaderleo Oct 27, 2026
4652f25
fix: require exact WK artifact endpoint URL
teamleaderleo Oct 27, 2026
ebe5aa3
fix: keep passive WK artifact frames from escaping
teamleaderleo Oct 27, 2026
7f52817
test: preserve ordinary page srcdoc navigation
teamleaderleo Oct 27, 2026
91588ec
fix: preserve ordinary page srcdoc navigation
teamleaderleo Oct 27, 2026
22cf4ef
Merge feat-cmux-next into nx-artifacts-wk-1027
teamleaderleo Oct 28, 2026
9151a36
Merge branch 'feat-cmux-next' into nx-artifacts-wk-1027
teamleaderleo Oct 29, 2026
8f90d50
docs(next): link interactive artifact release highlight
teamleaderleo Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/artifact_page.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Offline artifact documents for the shared CEF agent page. No CEF dependency;
// exercised by test-cmux-page-path-cpp.sh alongside the scheme's path policy.
#ifndef CMUX_SHIM_ARTIFACT_PAGE_H_
#define CMUX_SHIM_ARTIFACT_PAGE_H_

#include <string>

namespace cmux_shim {
inline constexpr char kArtifactFrameURL[] = "cmux-page://cmux.agent/__artifact";
inline constexpr char kArtifactCSP[] =
"sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; "
"img-src data: blob:; media-src data: blob:; font-src data:; connect-src 'none'; "
"frame-src 'none'; worker-src 'none'; form-action 'none'; base-uri 'none'; "
"frame-ancestors cmux-page://cmux.agent";

inline bool IsArtifactFrameURL(const std::string& url) {
return url.substr(0, url.find_first_of("?#")) == kArtifactFrameURL;
}

inline bool IsAgentPageURL(const std::string& url) {
const std::string origin = "cmux-page://cmux.agent";
return url.compare(0, origin.size(), origin) == 0 &&
(url.size() == origin.size() || url[origin.size()] == '/' || url[origin.size()] == '?' ||
url[origin.size()] == '#');
}

inline bool ArtifactContextRestricted(const std::string& frame, const std::string& parent) {
return IsArtifactFrameURL(frame) || IsArtifactFrameURL(parent) ||
(frame == "about:srcdoc" && IsAgentPageURL(parent));
}

// Only an empty, direct child of the agent page may load the bootstrap once.
// After commit, self navigation (including refresh), children and srcdoc
// replacement are denied, regardless of the destination's scheme.
inline bool ArtifactNavigationRefused(bool main, const std::string& frame, const std::string& parent,
const std::string& target) {
if (ArtifactContextRestricted(frame, parent)) return true;
if (IsArtifactFrameURL(target)) {
return main || target != kArtifactFrameURL || !IsAgentPageURL(parent) ||
(!frame.empty() && frame != "about:blank");
}
return false;
}

inline bool ArtifactResourceAllowed(const std::string& url, bool image_media_or_font) {
return image_media_or_font && (url.rfind("data:", 0) == 0 || url.rfind("blob:", 0) == 0);
}

// Same one-shot protocol as AgentPaneRenderFrame. The response's sandbox
// makes the origin opaque even if an embedder omits its iframe sandbox. A
// passive preview remains srcdoc with sandbox=""; only Run grants scripts.
inline char kArtifactDocument[] = R"HTML(<!doctype html>
<html><head><meta charset="utf-8"><script>
"use strict";
(function () {
var host = parent;
function size() {
var body = document.body;
host.postMessage({type:"cmux-render-size",height:Math.ceil(Math.max(
document.documentElement.scrollHeight,body ? body.scrollHeight : 0))},"*");
}
function show(event) {
var data = event.data;
if (event.source !== host || !data || data.type !== "cmux-render" || typeof data.html !== "string") return;
removeEventListener("message",show);
document.open(); document.write(data.html); document.close();
var theme = document.createElement("style");
theme.textContent = typeof data.css === "string" ? data.css : "";
var head = document.head || document.documentElement;
head.insertBefore(theme,head.firstChild);
new ResizeObserver(size).observe(document.documentElement);
addEventListener("load",size); size();
}
addEventListener("message",show);
host.postMessage({type:"cmux-render-ready"},"*");
})();
</script></head></html>)HTML";
} // namespace cmux_shim
#endif // CMUX_SHIM_ARTIFACT_PAGE_H_
3 changes: 3 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/cmux_page_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <utility>
#include <vector>

#include "artifact_page.h"

namespace cmux_shim {

inline constexpr char kCmuxPageScheme[] = "cmux-page";
Expand Down Expand Up @@ -201,6 +203,7 @@ inline bool GrantsCmuxPageLocalNetwork(const std::string& requesting_origin, uin
inline bool PageRequestAllowed(bool main_frame, const std::string& frame_url, const std::string& parent_url,
const std::string& domain) {
if (main_frame) return true;
if (ArtifactContextRestricted(frame_url, parent_url)) return false;
const std::string origin = std::string(kCmuxPageScheme) + "://" + domain;
auto same = [&](const std::string& url) {
return url.size() >= origin.size() && url.compare(0, origin.size(), origin) == 0 &&
Expand Down
22 changes: 22 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/shim_client.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "include/cef_parser.h"
#include "include/cef_resource_request_handler.h"
#include "command_line_switches.h"
#include "artifact_page.h"
#include "external_url.h"
#include "local_file_handoff.h"
#include "local_file_scope.h"
Expand Down Expand Up @@ -165,6 +166,17 @@ cef_return_value_t OnBeforeResourceLoad(CefRefPtr<CefBrowser>, CefRefPtr<CefFram
return std::string();
}

// Inspect the whole ancestry: blank/srcdoc children cannot shed the offline
// document's restrictions by inheriting an ancestor's ordinary page URL.
bool RestrictedArtifactFrame(CefRefPtr<CefFrame> frame) {
while (frame) {
CefRefPtr<CefFrame> parent = frame->GetParent();
if (ArtifactContextRestricted(frame->GetURL().ToString(), parent ? parent->GetURL().ToString() : "")) return true;
frame = parent;
}
return false;
}

class Client : public CefClient,
public CefDisplayHandler,
public CefLoadHandler,
Expand Down Expand Up @@ -282,6 +294,10 @@ bool OnChromeCommand(CefRefPtr<CefBrowser> browser, int command_id, cef_window_o
// tab in the other store with the same URL.
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> request,
bool user_gesture, bool is_redirect) override {
CefRefPtr<CefFrame> parent = frame->GetParent();
if (RestrictedArtifactFrame(frame) ||
ArtifactNavigationRefused(frame->IsMain(), frame->GetURL().ToString(),
parent ? parent->GetURL().ToString() : "", request->GetURL().ToString())) return true;
if (!frame->IsMain()) return false;
int id = browser->GetIdentifier();
std::string url = request->GetURL().ToString();
Expand Down Expand Up @@ -311,6 +327,12 @@ bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, Ce
CefRefPtr<CefRequest> request, bool, bool,
const CefString&, bool&) override {
const std::string url = request->GetURL().ToString();
const auto type = request->GetResourceType();
if (RestrictedArtifactFrame(frame) &&
!ArtifactResourceAllowed(url, type == RT_IMAGE || type == RT_MEDIA || type == RT_FONT_RESOURCE)) {
static CefRefPtr<CefResourceRequestHandler> refusal = new LocalFileRefusal();
return refusal;
}
// A request with no frame (a worker, a CefURLRequest) has no folder to
// judge it by: its file: loads are refused.
if (LocalFileSchemeURL(url) && request->GetResourceType() != RT_MAIN_FRAME &&
Expand Down
15 changes: 15 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/src/shim_page_scheme.mm
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ int NextToken() {
frame_url = frame->GetURL().ToString();
if (CefRefPtr<CefFrame> parent = frame->GetParent()) parent_url = parent->GetURL().ToString();
}
const std::string url = request->GetURL().ToString();
if (IsArtifactFrameURL(url)) {
if (domain_ != "cmux.agent" || request->GetMethod().ToString() != "GET" ||
request->GetResourceType() != RT_SUB_FRAME ||
ArtifactNavigationRefused(main_frame, frame_url, parent_url, url)) {
static char kBody[] = "Forbidden";
return Respond(403, "Forbidden", "text/plain", Headers(), kBody, sizeof(kBody) - 1);
}
CefResponse::HeaderMap headers;
headers.emplace("Content-Security-Policy", kArtifactCSP);
headers.emplace("Cache-Control", "no-store");
headers.emplace("X-Content-Type-Options", "nosniff");
headers.emplace("Referrer-Policy", "no-referrer");
return Respond(200, "OK", "text/html", headers, kArtifactDocument, sizeof(kArtifactDocument) - 1);
}
if (!PageRequestAllowed(main_frame, frame_url, parent_url, domain_)) {
static char kBody[] = "Forbidden";
return Respond(403, "Forbidden", "text/plain", Headers(), kBody, sizeof(kBody) - 1);
Expand Down
45 changes: 45 additions & 0 deletions Packages/macOS/CmuxNext/CEFShim/tests/cmux_page_path_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,51 @@ int main() {
// A child frame being navigated: its parent decides.
Allowed(false, "about:blank", "cmux-page://cmux.agent/", "cmux.agent", true);
Allowed(false, "about:blank", "https://evil.test/", "cmux.agent", false);
// Artifact documents must never inherit the page host's file authority.
Allowed(false, "cmux-page://cmux.agent/__artifact", "cmux-page://cmux.agent/", "cmux.agent", false);
Allowed(false, "about:blank", "cmux-page://cmux.agent/__artifact", "cmux.agent", false);
Allowed(false, "about:srcdoc", "cmux-page://cmux.agent/", "cmux.agent", false);
{
const std::string page = "cmux-page://cmux.agent/";
const std::string artifact = cmux_shim::kArtifactFrameURL;
auto navigation = [&](bool main, const std::string& current, const std::string& parent,
const std::string& target, bool refused) {
Expect(cmux_shim::ArtifactNavigationRefused(main, current, parent, target) == refused,
"artifact navigation " + current + " -> " + target + " parent " + parent);
};
navigation(false, "about:blank", page, artifact, false);
navigation(false, "", page, artifact, false);
navigation(true, "about:blank", "", artifact, true);
for (const auto& parent : {"https://evil.test/", "cmux-page://cmux.agent.evil/",
"cmux-page://cmux.history/", "cmux-page://evil@cmux.agent/"}) {
navigation(false, "about:blank", parent, artifact, true);
}
for (const auto& target : {artifact, artifact + "?copy=1", page, std::string("about:srcdoc"),
std::string("https://example.test/"), std::string("file:///tmp/a.html"),
std::string("data:text/html,replace"), std::string("javascript:alert(1)")}) {
navigation(false, artifact, page, target, true);
navigation(false, "about:srcdoc", page, target, true);
navigation(false, "about:blank", artifact, target, true);
}
navigation(false, "about:blank", page, artifact + "?copy=1", true);
navigation(false, page, "", "https://example.test/", false);
navigation(false, "about:blank", page, "http://localhost:3000/", false);
for (const auto& url : {"https://example.test/a.png", "http://127.0.0.1:4000/api", "file:///tmp/a.png",
"cmux-page://cmux.agent/index.html", "cmux-agent://pane/index.html"}) {
Expect(!cmux_shim::ArtifactResourceAllowed(url, true), "offline artifact refuses " + std::string(url));
}
Expect(cmux_shim::ArtifactResourceAllowed("data:image/png;base64,AA", true), "inline image allowed");
Expect(cmux_shim::ArtifactResourceAllowed("blob:null/test", true), "inline media allowed");
Expect(!cmux_shim::ArtifactResourceAllowed("data:text/html,nested", false), "inline document refused");
Expect(!cmux_shim::ArtifactResourceAllowed("blob:null/script", false), "blob script refused");
const std::string policy = cmux_shim::kArtifactCSP;
Expect(policy.find("sandbox allow-scripts;") == 0 && policy.find("allow-same-origin") == std::string::npos,
"artifact response enforces opaque origin");
Expect(policy.find("connect-src 'none'") != std::string::npos &&
policy.find("frame-src 'none'") != std::string::npos &&
policy.find("form-action 'none'") != std::string::npos,
"artifact response refuses connections, children and forms");
}
{
auto headers = cmux_shim::PageResponseHeaders("default-src 'self'");
bool xfo = false, corp = false, nosniff = false, csp = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ final class AgentPaneNavigation: NSObject, WKNavigationDelegate {
for: action.request.url,
source: view.source,
userClicked: action.navigationType == .linkActivated,
mainFrame: action.targetFrame?.isMainFrame ?? true
mainFrame: action.targetFrame?.isMainFrame ?? true,
sourceFrameURL: action.sourceFrame.request.url,
targetFrameURL: action.targetFrame?.request.url
) {
case .allow:
decisionHandler(.allow)
Expand Down Expand Up @@ -69,7 +71,21 @@ final class AgentPaneNavigation: NSObject, WKNavigationDelegate {
case cancel
}

static func decision(for url: URL?, source: AgentPaneSource, userClicked: Bool, mainFrame: Bool = true) -> Decision {
static func decision(for url: URL?, source: AgentPaneSource, userClicked: Bool, mainFrame: Bool = true,
sourceFrameURL: URL? = nil, targetFrameURL: URL? = nil) -> Decision {
// CSP blocks resource loads, but not self-navigation. An artifact may never navigate
// itself, a sibling or the pane, including to the otherwise allowed loopback previews.
if AgentPaneRenderFrame.isArtifact(sourceFrameURL) || AgentPaneRenderFrame.isArtifact(targetFrameURL) {
return .cancel
}
// A passive artifact is a sandboxed srcdoc frame. Its initial about:srcdoc load is allowed,
// but a navigation originating there must not reach an allowed loopback preview or the pane.
if sourceFrameURL?.absoluteString == "about:srcdoc" {
return .cancel
}
if targetFrameURL?.absoluteString == "about:srcdoc", url?.absoluteString != "about:srcdoc" {
return .cancel
}
guard let url else { return .cancel }
if mainFrame, source.isPageDocument(url) { return .allow }
// A preview frame stays on loopback pages; a click inside it does not leave the frame. It
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,28 @@ nonisolated enum AgentPaneRenderFrame {
/// True for the frame document's URL, and nothing else on the render host.
static func isFrame(_ url: URL) -> Bool {
url.scheme?.lowercased() == AgentPaneSource.bundledScheme && url.host?.lowercased() == host
&& url.path == path && url.port == nil && url.user == nil
&& [path, "/artifact"].contains(url.path) && url.port == nil && url.user == nil
}

/// Offline artifacts use their own endpoint; render tools keep their existing library policy.
static func isArtifact(_ url: URL?) -> Bool {
guard let url else { return false }
return isFrame(url) && url.path == "/artifact"
}

static let artifactPolicy = [
"default-src 'none'",
"script-src 'unsafe-inline'",
"style-src 'unsafe-inline'",
"img-src data: blob:",
"media-src data: blob:",
"font-src data:",
"connect-src 'none'",
"frame-src 'none'",
"form-action 'none'",
"base-uri 'none'",
].joined(separator: "; ")

static let policy = [
"default-src 'none'",
"script-src 'unsafe-inline' 'unsafe-eval' \(cdns)",
Expand Down Expand Up @@ -69,11 +88,11 @@ nonisolated enum AgentPaneRenderFrame {
</script></head></html>
"""#.utf8)

static func headers(length: Int) -> [String: String] {
static func headers(length: Int, artifact: Bool = false) -> [String: String] {
[
"Content-Type": "text/html; charset=utf-8",
"Content-Length": String(length),
"Content-Security-Policy": policy,
"Content-Security-Policy": artifact ? artifactPolicy : policy,
"Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class AgentPaneSchemeHandler: NSObject, WKURLSchemeHandler {
if task.request.httpMethod.map({ $0 == "GET" }) ?? true,
let url = task.request.url, AgentPaneRenderFrame.isFrame(url) {
let data = AgentPaneRenderFrame.document
let headers = AgentPaneRenderFrame.headers(length: data.count)
let headers = AgentPaneRenderFrame.headers(length: data.count, artifact: AgentPaneRenderFrame.isArtifact(url))
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: headers)
task.didReceive(response ?? URLResponse(url: url, mimeType: "text/html", expectedContentLength: data.count, textEncodingName: "utf-8"))
task.didReceive(data)
Expand Down
Loading
Loading