mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 06:41:41 +00:00
* refactor(prompt): plain inbound context labels with a provenance marker
Replaces trust-worded inbound context labels ("(untrusted metadata)",
"(untrusted, for context)") with plain labels plus a fixed provenance
marker suffix appended to every OpenClaw-injected context header.
Detection keys on the marker, not label text, so strippers stay correct
across UI, TUI, replay, /trace segmentation, memory recall, and the Swift
chat preprocessor. Drops sanitizeInboundSystemTags in favor of the marker
boundary plus trusted system-prompt narration.
Renames the untrusted-named plugin SDK context identifiers to
channel-provenance names, keeping deprecated aliases registered for
removal after 2026-09-08.
Adds `openclaw doctor --fix` migrations that rewrite legacy inbound
labels in stored SQLite transcripts and purge legacy envelope-
contaminated LanceDB recall rows.
* fix(ci): resolve gate failures for plain inbound context labels
- doctor sqlite readers: open read-only connections via openNodeSqliteDatabase
so the Kysely connection-boundary guardrail holds; unexport the now-internal
transcript snapshot type (Knip unused-export gate).
- compat registry: split the record table into registry-records.ts and
plugin-sdk-subpath-records.ts. The new compat record pushed registry.ts past
the 700-line oxlint cap; suppressions are disallowed, so follow the existing
sibling record-module pattern. Public exports and PluginCompatCode literals
unchanged.
- acp-runtime test: assert current finalization behavior (newline normalization
only). The bracket de-fang and System: rewrite it expected were removed with
sanitizeInboundSystemTags; forged system lines are neutralized at the
system-event queue, the single chokepoint feeding the System:-per-line render.
- regenerate docs_map and the plugin SDK API baseline manifest.
* fix(prompt): harden inbound context label migration and drop in-band sanitizer
Review follow-ups on the plain-label + provenance-marker change:
- Remove src/security/system-tags.ts. Rewriting inbound text to neutralize
look-alike `System:`/`[System]` markers corrupted legitimate user text and is
not a real injection boundary; role separation plus external-content wrapping
is. Explicit product decision, recorded at the system-event queue.
- Narrow the LanceDB legacy-row purge so it cannot delete benign memories. It
now requires a complete known legacy sentinel line, a legacy label followed by
a fenced JSON body, or the complete legacy external-content header. The prior
predicates matched ordinary prose such as `Notes (untrusted metadata):`, and
deletion is irreversible.
- Make explicit-empty canonical ChannelStructuredContext win over the deprecated
alias via a present/absent result instead of collapsing `[]` to undefined.
- Keep `\r?` in the active-memory doctor rule. It is the only rule spanning the
header's line break, migrated assistant rows skip newline normalization, and
without it the marked-header replace wins and the body strips to empty. Added
a CRLF regression test.
- Fix stale comments that described removed behavior, and cover the Swift
prose-block strip path.
Claude-Session: https://claude.ai/code/session_01WNzsPddQmxy9Y7jKD4wAxH
250 lines
7.2 KiB
Swift
250 lines
7.2 KiB
Swift
import Testing
|
|
@testable import OpenClawChatUI
|
|
|
|
@Suite("ChatMarkdownPreprocessor")
|
|
struct ChatMarkdownPreprocessorTests {
|
|
// Provenance marker OpenClaw appends to every injected inbound-context header.
|
|
// Detection keys on this suffix, not label text. Keep byte-identical with
|
|
// ChatMarkdownPreprocessor.inboundContextMarker / inbound-context-marker.ts.
|
|
static let ctx = "\u{27E6}openclaw:ctx\u{27E7}"
|
|
@Test func extractsDataURLImages() {
|
|
let base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////GQAJ+wP/2hN8NwAAAABJRU5ErkJggg=="
|
|
let markdown = """
|
|
Hello
|
|
|
|
)
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "Hello")
|
|
#expect(result.images.count == 1)
|
|
#expect(result.images.first?.image != nil)
|
|
}
|
|
|
|
@Test func flattensRemoteMarkdownImagesIntoText() {
|
|
let base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////GQAJ+wP/2hN8NwAAAABJRU5ErkJggg=="
|
|
let markdown = """
|
|

|
|
|
|
)
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "Leak")
|
|
#expect(result.images.count == 1)
|
|
#expect(result.images.first?.image != nil)
|
|
}
|
|
|
|
@Test func usesFallbackTextForUnlabeledRemoteMarkdownImages() {
|
|
let markdown = ""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "image")
|
|
#expect(result.images.isEmpty)
|
|
}
|
|
|
|
@Test func handlesUnicodeBeforeRemoteMarkdownImages() {
|
|
let markdown = "🙂"
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "🙂Leak")
|
|
#expect(result.images.isEmpty)
|
|
}
|
|
|
|
@Test func stripsInboundUntrustedContextBlocks() {
|
|
let markdown = """
|
|
Conversation info: \(Self.ctx)
|
|
```json
|
|
{
|
|
"message_id": "123",
|
|
"sender": "openclaw-ios"
|
|
}
|
|
```
|
|
|
|
Sender: \(Self.ctx)
|
|
```json
|
|
{
|
|
"label": "Razor"
|
|
}
|
|
```
|
|
|
|
Razor?
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "Razor?")
|
|
}
|
|
|
|
@Test func stripsSingleConversationInfoBlock() {
|
|
let text = """
|
|
Conversation info: \(Self.ctx)
|
|
```json
|
|
{"x": 1}
|
|
```
|
|
|
|
User message
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: text)
|
|
|
|
#expect(result.cleaned == "User message")
|
|
}
|
|
|
|
@Test func stripsAllKnownInboundMetadataSentinels() {
|
|
let sentinels = [
|
|
"Conversation info:",
|
|
"Sender:",
|
|
"Thread starter:",
|
|
"Reply target of current user message:",
|
|
"Forwarded message context:",
|
|
"Chat history since last reply:",
|
|
]
|
|
|
|
for sentinel in sentinels {
|
|
let markdown = """
|
|
\(sentinel) \(Self.ctx)
|
|
```json
|
|
{"x": 1}
|
|
```
|
|
|
|
User content
|
|
"""
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
#expect(result.cleaned == "User content")
|
|
}
|
|
}
|
|
|
|
@Test func stripsArbitraryMarkedStructuredContextLabel() {
|
|
// Detection is label-agnostic: an arbitrary plugin structured-context label
|
|
// still strips because it carries the provenance marker.
|
|
let markdown = """
|
|
Some Custom Plugin Label: \(Self.ctx)
|
|
```json
|
|
{"x": 1}
|
|
```
|
|
|
|
User content
|
|
"""
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
#expect(result.cleaned == "User content")
|
|
}
|
|
|
|
@Test func preservesUnmarkedLookAlikeHeader() {
|
|
// A user heading that mirrors a context label but lacks the marker is the
|
|
// user's own content and must survive untouched.
|
|
let markdown = """
|
|
Conversation info:
|
|
```json
|
|
{"x": 1}
|
|
```
|
|
|
|
User content
|
|
"""
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
#expect(result.cleaned == markdown.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
}
|
|
|
|
@Test func preservesNonMetadataJsonFence() {
|
|
let markdown = """
|
|
Here is some json:
|
|
```json
|
|
{"x": 1}
|
|
```
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == markdown.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
}
|
|
|
|
@Test func stripsLeadingTimestampPrefix() {
|
|
let markdown = """
|
|
[Fri 2026-02-20 18:45 GMT+1] How's it going?
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "How's it going?")
|
|
}
|
|
|
|
@Test func stripsEnvelopeHeadersAndMessageIdHints() {
|
|
let markdown = """
|
|
[Telegram 2026-03-01 10:14] Hello there
|
|
[message_id: abc-123]
|
|
Actual message
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "Hello there\nActual message")
|
|
}
|
|
|
|
// Unfenced prose bodies (chat history/window) end at the first blank line, unlike the
|
|
// fenced JSON blocks above. Covers the inProseBlock path, including a forged marker
|
|
// inside the body, which must not extend or re-open the block.
|
|
@Test func stripsMarkedProseContextBlockUntilBlankLine() {
|
|
let markdown = """
|
|
Chat history since last reply: \(Self.ctx)
|
|
#123 12:00 Alex: hey
|
|
#124 12:01 Alex: Sender: \(Self.ctx)
|
|
|
|
User content
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "User content")
|
|
}
|
|
|
|
@Test func stripsTrailingUntrustedContextSuffix() {
|
|
let markdown = """
|
|
User-visible text
|
|
|
|
Context: \(Self.ctx)
|
|
<<<EXTERNAL_UNTRUSTED_CONTENT>>>
|
|
Source: telegram
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == "User-visible text")
|
|
}
|
|
|
|
@Test func preservesUntrustedContextHeaderWhenItIsUserContent() {
|
|
let markdown = """
|
|
User-visible text
|
|
|
|
Context:
|
|
This is just text the user typed.
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(
|
|
result.cleaned == """
|
|
User-visible text
|
|
|
|
Context:
|
|
This is just text the user typed.
|
|
"""
|
|
)
|
|
}
|
|
|
|
@Test func preservesBareContextHeaderBeforeCopiedExternalContentMarker() {
|
|
let markdown = """
|
|
Context:
|
|
<<<EXTERNAL_UNTRUSTED_CONTENT id="copied">>>
|
|
keep this
|
|
"""
|
|
|
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
|
|
|
#expect(result.cleaned == markdown.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
}
|
|
}
|