diff --git a/deploy/cloudflare-worker/src/index.js b/deploy/cloudflare-worker/src/index.js
index 95b9752..7f090d6 100644
--- a/deploy/cloudflare-worker/src/index.js
+++ b/deploy/cloudflare-worker/src/index.js
@@ -1635,9 +1635,113 @@ function hexFromBytes(bytes) {
return out;
}
+function sanitizeTranscriptEntries(entries) {
+ const sanitized = [];
+ for (const entry of Array.isArray(entries) ? entries : []) {
+ if (!entry || typeof entry !== "object") continue;
+ const next = { ...entry };
+ if (next.kind === "message") {
+ const text = redactInternalContext(next.text).trim();
+ next.text = text === "[internal context omitted]" ? "" : text;
+ const images = Array.isArray(next.images) ? next.images : [];
+ if (!next.text && !images.length && !Number(next.omitted_images || 0)) continue;
+ }
+ if (next.kind === "tool") {
+ next.input_preview = redactInternalContext(next.input_preview);
+ next.output = redactInternalContext(next.output);
+ next.output_bytes = byteLengthInBrowser(next.output || "");
+ }
+ sanitized.push(next);
+ }
+ return sanitized;
+}
+
+function redactInternalContext(text) {
+ if (!text) return "";
+ const visible = [];
+ let inHiddenBlock = false;
+ let silentHiddenBlock = false;
+ let redactedPrevious = false;
+ for (const line of String(text).split("\\n")) {
+ const trimmed = line.trim();
+ if (inHiddenBlock) {
+ if (!silentHiddenBlock && !redactedPrevious) {
+ visible.push("[internal context omitted]");
+ redactedPrevious = true;
+ }
+ if (endsInternalContext(trimmed)) {
+ inHiddenBlock = false;
+ silentHiddenBlock = false;
+ }
+ continue;
+ }
+ if (containsInternalContext(trimmed)) {
+ const silent = silentlyRedactsInternalContext(trimmed);
+ if (!silent && !redactedPrevious) {
+ visible.push("[internal context omitted]");
+ redactedPrevious = true;
+ }
+ if (startsInternalContext(trimmed) && !endsInternalContext(trimmed)) {
+ inHiddenBlock = true;
+ silentHiddenBlock = silent;
+ }
+ continue;
+ }
+ visible.push(line);
+ redactedPrevious = false;
+ }
+ return visible.join("\\n").trim();
+}
+
+function silentlyRedactsInternalContext(text) {
+ return String(text || "").trim().startsWith("");
+}
+
+function containsInternalContext(text) {
+ const value = String(text || "");
+ if (startsInternalContext(value)) return true;
+ return [
+ "# AGENTS.md instructions",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ].some((marker) => value.includes(marker));
+}
+
+function startsInternalContext(text) {
+ const value = String(text || "").trim();
+ return [
+ "# AGENTS.md instructions",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ].some((prefix) => value.startsWith(prefix));
+}
+
+function endsInternalContext(text) {
+ const value = String(text || "").trim();
+ return [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ].some((suffix) => value.includes(suffix));
+}
+
function renderTranscript(transcript, options = {}) {
activeTranscriptSource = String(transcript.source || transcript.source_agent || "");
- const entries = (transcript.entries || []).filter((entry) => !isInternalContextEntry(entry));
+ const entries = sanitizeTranscriptEntries(transcript.entries || []);
const messageCount = entries.filter((entry) => entry.kind === "message").length;
const toolCount = entries.filter((entry) => entry.kind === "tool").length;
const imageCount = entries.reduce((count, entry) => count + (entry.images || []).filter((image) => !image.omitted).length, 0);
@@ -1695,16 +1799,6 @@ function lastAssistantMessageIndex(entries) {
return -1;
}
-function isInternalContextEntry(entry) {
- if (!entry || entry.kind !== "message") return false;
- const text = String(entry.text || "").trim();
- return text.startsWith("# AGENTS.md instructions for ") ||
- text.startsWith("") ||
- text.startsWith("") ||
- text.startsWith("");
-}
-
function messageNode(entry) {
const article = document.createElement("article");
article.className = "message-row " + (entry.role || "");
diff --git a/deploy/cloudflare-worker/test/index.test.js b/deploy/cloudflare-worker/test/index.test.js
index 8eab947..f45f3eb 100644
--- a/deploy/cloudflare-worker/test/index.test.js
+++ b/deploy/cloudflare-worker/test/index.test.js
@@ -257,7 +257,9 @@ test("share page serves human preview shell and agent metadata", async () => {
assert.match(html, /function toolActionNode/);
assert.match(html, /function renderMarkdown/);
assert.match(html, /function imageGallery/);
- assert.match(html, /function isInternalContextEntry/);
+ assert.match(html, /function sanitizeTranscriptEntries/);
+ assert.match(html, /function redactInternalContext/);
+ assert.match(html, /const entries = sanitizeTranscriptEntries\(transcript\.entries \|\| \[\]\)/);
assert.match(html, /skill-chip/);
assert.match(html, /function skillDetailsNode/);
assert.match(html, /function stripSkillInvocation/);
@@ -289,6 +291,95 @@ test("share page serves human preview shell and agent metadata", async () => {
assert.equal(manifestJSON.import.skill_url, "https://github.com/z2z23n0/agent-capsule/tree/main/skills/agent-capsule");
});
+test("share page redacts internal context from full transcript entries", async () => {
+ const env = fakeEnv();
+ const upload = await worker.fetch(new Request(BASE_URL + "/v1/shares", {
+ method: "POST",
+ body: shareForm(new Blob(["hello"]))
+ }), env);
+ assert.equal(upload.status, 201);
+ const created = await upload.json();
+ const html = await (await worker.fetch(new Request(created.share_url), env)).text();
+ const entries = [
+ {
+ kind: "message",
+ role: "user",
+ text: [
+ "",
+ "private plugin inventory",
+ "",
+ "# AGENTS.md instructions",
+ "",
+ "private project rules",
+ "",
+ "",
+ "/Users/private/workspace",
+ ""
+ ].join("\n")
+ },
+ {
+ kind: "message",
+ role: "assistant",
+ text: [
+ "visible final answer",
+ "",
+ "",
+ "",
+ "MEMORY.md:10-12|note=[private provenance]",
+ "",
+ "",
+ "019f523a-60d4-70d2-893e-c93ac9f92f43",
+ "",
+ ""
+ ].join("\n")
+ },
+ {
+ kind: "tool",
+ tool: "exec",
+ input_preview: "visible input before\n\nprivate input\n\nvisible input after",
+ output: "visible output before\n# AGENTS.md instructions\n\nprivate output\n\nvisible output after"
+ }
+ ];
+
+ const sanitized = runSharePageFunction(
+ html,
+ [
+ "sanitizeTranscriptEntries",
+ "redactInternalContext",
+ "containsInternalContext",
+ "startsInternalContext",
+ "endsInternalContext",
+ "silentlyRedactsInternalContext",
+ "byteLengthInBrowser"
+ ],
+ "sanitizeTranscriptEntries(entries)",
+ { entries }
+ );
+ const visible = JSON.stringify(sanitized);
+ for (const leaked of [
+ "recommended_plugins",
+ "private plugin inventory",
+ "AGENTS.md",
+ "private project rules",
+ "environment_context",
+ "/Users/private/workspace",
+ "oai-mem-citation",
+ "citation_entries",
+ "MEMORY.md",
+ "private provenance",
+ "rollout_ids",
+ "019f523a",
+ "private input",
+ "private output"
+ ]) {
+ assert.doesNotMatch(visible, new RegExp(leaked.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
+ }
+ assert.equal(sanitized.length, 2);
+ assert.equal(sanitized[0].text, "visible final answer");
+ assert.match(sanitized[1].input_preview, /visible input before[\s\S]*\[internal context omitted\][\s\S]*visible input after/);
+ assert.match(sanitized[1].output, /visible output before[\s\S]*\[internal context omitted\][\s\S]*visible output after/);
+});
+
test("share page renders a complete Chinese shell only when explicitly requested", async () => {
const env = fakeEnv();
const upload = await worker.fetch(new Request(BASE_URL + "/v1/shares", {
diff --git a/internal/capsule/capsule_test.go b/internal/capsule/capsule_test.go
index 829a006..b8e8953 100644
--- a/internal/capsule/capsule_test.go
+++ b/internal/capsule/capsule_test.go
@@ -646,6 +646,33 @@ func TestPreviewTranscriptRedactsInternalContextAcrossMessageParts(t *testing.T)
}
}
+func TestPreviewTranscriptRedactsMemoryCitationSuffix(t *testing.T) {
+ manifest := Manifest{
+ ThreadID: testThreadID,
+ ThreadTitle: "Preview demo",
+ CreatedAt: "2026-07-23T00:00:00Z",
+ }
+ session := strings.Join([]string{
+ `{"timestamp":"2026-07-23T00:00:01Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"visible final answer\n\n\n\nMEMORY.md:10-12|note=[private provenance]\n\n\n019f523a-60d4-70d2-893e-c93ac9f92f43\n\n"}]}}`,
+ }, "\n") + "\n"
+ transcript := buildPreviewTranscript(manifest, []byte(session))
+ if transcript.MessageCount != 1 {
+ t.Fatalf("message_count = %d", transcript.MessageCount)
+ }
+ if len(transcript.Entries) != 1 || transcript.Entries[0].Text != "visible final answer" {
+ t.Fatalf("visible answer = %#v", transcript.Entries)
+ }
+ preview := previewEntriesText(transcript)
+ if !strings.Contains(preview, "visible final answer") {
+ t.Fatalf("preview lost visible answer:\n%s", preview)
+ }
+ for _, leaked := range []string{"oai-mem-citation", "citation_entries", "MEMORY.md", "private provenance", "rollout_ids", "019f523a"} {
+ if strings.Contains(preview, leaked) {
+ t.Fatalf("memory citation leaked %q into preview:\n%s", leaked, preview)
+ }
+ }
+}
+
func TestPreviewTranscriptAttachesSkillMessages(t *testing.T) {
manifest := Manifest{
ThreadID: testThreadID,
diff --git a/internal/capsule/preview.go b/internal/capsule/preview.go
index 5851916..8e36970 100644
--- a/internal/capsule/preview.go
+++ b/internal/capsule/preview.go
@@ -470,27 +470,31 @@ func previewRedactHiddenContext(text string) string {
}
var b strings.Builder
inHiddenBlock := false
+ silentHiddenBlock := false
redactedPrevious := false
for _, part := range strings.SplitAfter(text, "\n") {
line := strings.TrimRight(part, "\r\n")
trimmed := strings.TrimSpace(line)
if inHiddenBlock {
- if !redactedPrevious {
+ if !silentHiddenBlock && !redactedPrevious {
b.WriteString("[internal context omitted]\n")
redactedPrevious = true
}
if previewEndsHiddenContext(trimmed) {
inHiddenBlock = false
+ silentHiddenBlock = false
}
continue
}
if previewContainsHiddenContext(trimmed) {
- if !redactedPrevious {
+ silent := previewSilentlyRedactsContext(trimmed)
+ if !silent && !redactedPrevious {
b.WriteString("[internal context omitted]\n")
redactedPrevious = true
}
if previewStartsHiddenContext(trimmed) && !previewEndsHiddenContext(trimmed) {
inHiddenBlock = true
+ silentHiddenBlock = silent
}
continue
}
@@ -500,6 +504,10 @@ func previewRedactHiddenContext(text string) string {
return b.String()
}
+func previewSilentlyRedactsContext(text string) bool {
+ return strings.HasPrefix(strings.TrimSpace(text), "")
+}
+
func previewContainsHiddenContext(text string) bool {
if previewStartsHiddenContext(text) {
return true
@@ -511,6 +519,8 @@ func previewContainsHiddenContext(text string) bool {
"",
"",
"",
+ "",
+ "",
} {
if strings.Contains(text, marker) {
return true
@@ -527,6 +537,8 @@ func previewStartsHiddenContext(text string) bool {
"",
"",
"",
+ "",
+ "",
} {
if strings.HasPrefix(text, prefix) {
return true
@@ -542,6 +554,8 @@ func previewEndsHiddenContext(text string) bool {
"",
"",
"",
+ "",
+ "",
} {
if strings.HasPrefix(text, prefix) {
return true