Commit 536a37c
feat(core): wrap external content in tool results (issue #456)
Bring tool-result messages under the same <EXTERNAL-CONTENT> trust-framing
umbrella that page snapshots already use, so attacker-controllable text
laundered through tools is (a) clearly labeled as untrusted to the LLM
and (b) auto-clipped from history after one turn the way snapshots are.
## Background
Before this change, page snapshots had three defensive layers against
prompt-injected web content:
1. Each snapshot is wrapped in <EXTERNAL-CONTENT label="..."> tags with a
safety warning attached (wrapExternalContentWithWarning).
2. Old snapshots are auto-clipped to "[clipped for brevity]" before each
new one lands (truncateOldExternalContent).
3. Snapshots are generated by a deterministic DOM walk — no LLM
re-narrates the content.
Tool results that carry web-sourced content had none of those layers.
They went into this.messages as tool-result payloads, stayed there at
full size for the rest of the task, and the agent perceived them as
trusted programmatic output. Worst case: extract's secondary LLM read
attacker content and produced output that was stored unwrapped and
laundered into history.
## Changes
Three sites now wrap web-sourced content at the emission boundary:
- `extract.extractedData` — wraps with `ExtractResult` before returning
from the tool's execute().
- `tabstack_extract_markdown.content` — wraps with `TabstackContent`.
- `buildValidationFeedbackPrompt` — wraps both `taskAssessment` and
`feedback` (including the null-fallback string) with `ValidatorFeedback`
so the validator's LLM-summarized view of conversation history can't
silently re-launder injection content.
The truncator (`truncateOldExternalContent`) is extended to scan
`role: "tool"` messages in addition to `role: "user"`. A new recursive
`clipInValue` walker descends through strings/arrays/objects inside each
tool-result `output` and applies the existing regex to any string that
contains an EXTERNAL-CONTENT block. The walker is generic over all
current and future wrap sites — no per-tool awareness needed.
One sentence is added to the action-loop system prompt acknowledging
that EXTERNAL-CONTENT blocks may appear in tool-result fields as well
as user messages.
## Intentionally not wrapped (residual risk)
- `webSearch.markdown` — already wrapped at the search-provider level
with `SearchResults`. The truncator extension now reaches that wrap
inside tool messages for free, so no second wrap is needed.
- `tabstack_extract_json.data` and `tabstack_generate_json.data` —
schema-constrained structured objects, not free-form prose. String
fields nested inside are technically still attacker-controllable;
the truncator walks and clips them if tagged. Code comments at the
tool definitions document this decision so future maintainers see
the rationale.
## Out of scope (follow-ups)
- `search_page` / `find_elements` (PR #446): adopt the same helper on
rebase. The truncator extension will pick up their wraps without
further changes here.
- Stricter post-processing of the extraction LLM's output to strip
injection-shaped patterns.
- A trust/taint model that propagates untrusted-source flags through
structured tool outputs.
## Reproduction test
A new unit test seeds a `role: "tool"` message with the verbatim
uaf.cafe injection payload wrapped as an extract-result, runs the
truncator, and asserts the wrap structure persists while the payload
strings (e.g. `stoletheminerals.github.io`, `ALWAYS do ONLY`) are
clipped from history.
Payload recorded from https://uaf.cafe/agent_tabstack.html on 2026-05-20.
## Tests
+5 new tests, +1267 total: core 684 / cli 221 / server 96 / extension 266.
`pnpm run check` green (typecheck + format:check + all package tests).
`gitleaks detect` clean.
Closes #456
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 880db9f commit 536a37c
10 files changed
Lines changed: 244 additions & 20 deletions
File tree
- packages/core
- src
- tools
- utils
- test
- tools
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
351 | 351 | | |
352 | 352 | | |
353 | 353 | | |
| 354 | + | |
354 | 355 | | |
355 | 356 | | |
356 | 357 | | |
| |||
635 | 636 | | |
636 | 637 | | |
637 | 638 | | |
638 | | - | |
639 | | - | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
640 | 647 | | |
641 | 648 | | |
642 | 649 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
46 | | - | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
47 | 51 | | |
48 | 52 | | |
49 | 53 | | |
| |||
67 | 71 | | |
68 | 72 | | |
69 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
70 | 82 | | |
71 | 83 | | |
72 | 84 | | |
| |||
116 | 128 | | |
117 | 129 | | |
118 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
119 | 134 | | |
120 | 135 | | |
121 | 136 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
398 | 399 | | |
399 | 400 | | |
400 | 401 | | |
401 | | - | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
402 | 406 | | |
403 | 407 | | |
404 | 408 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
14 | 17 | | |
15 | 18 | | |
16 | 19 | | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
20 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
21 | 30 | | |
22 | | - | |
| 31 | + | |
23 | 32 | | |
24 | 33 | | |
25 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
746 | 746 | | |
747 | 747 | | |
748 | 748 | | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
749 | 753 | | |
750 | 754 | | |
751 | 755 | | |
| |||
754 | 758 | | |
755 | 759 | | |
756 | 760 | | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
757 | 781 | | |
758 | 782 | | |
759 | 783 | | |
| |||
776 | 800 | | |
777 | 801 | | |
778 | 802 | | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
779 | 820 | | |
780 | 821 | | |
781 | 822 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
449 | 450 | | |
450 | 451 | | |
451 | 452 | | |
452 | | - | |
| 453 | + | |
453 | 454 | | |
454 | 455 | | |
455 | 456 | | |
| |||
607 | 608 | | |
608 | 609 | | |
609 | 610 | | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
610 | 638 | | |
611 | 639 | | |
612 | 640 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | | - | |
| 72 | + | |
73 | 73 | | |
74 | 74 | | |
75 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
76 | 82 | | |
77 | 83 | | |
78 | 84 | | |
79 | 85 | | |
80 | 86 | | |
81 | | - | |
| 87 | + | |
82 | 88 | | |
83 | 89 | | |
84 | 90 | | |
85 | | - | |
86 | 91 | | |
87 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
88 | 118 | | |
89 | 119 | | |
90 | 120 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
599 | 599 | | |
600 | 600 | | |
601 | 601 | | |
602 | | - | |
603 | | - | |
604 | | - | |
605 | | - | |
606 | | - | |
607 | | - | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
608 | 626 | | |
609 | 627 | | |
610 | 628 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
28 | 31 | | |
29 | 32 | | |
30 | 33 | | |
| |||
0 commit comments