Skip to content

feat(security): harness pattern extraction — prompt injection defense - #81

Merged
elkaix merged 5 commits into
mainfrom
feat/harness-pattern-extraction
Jun 7, 2026
Merged

feat(security): harness pattern extraction — prompt injection defense#81
elkaix merged 5 commits into
mainfrom
feat/harness-pattern-extraction

Conversation

@elkaix

@elkaix elkaix commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

Implements three architectural patterns from docs/superpowers/specs/2026-06-06-harness-pattern-extraction-design.md as zero-dependency, pure Python modules:

1. Prompt injection defense (utils/trust.py)

  • UntrustedData frozen dataclass wraps external data in <untrusted_data id="NONCE"> tags with closing-tag HTML escaping
  • Applied to all external-content output paths in ReadFile (3 paths) and FetchURL (3 paths)
  • Runtime nonce prevents model from constructing matching opening tag

2. Adversarial verification boundary (utils/artifacts.py)

  • CodingArtifact / VerificationResult — generic coder↔verifier pair
  • VulnerabilityArtifact / AuditVerdict — security-specific finder↔audit verifier pair
  • Verifier receives only typed fields, never coder prose/logs
  • Artifact contract instructions added to coder.yaml and verifier.yaml

3. Recon-first parallel seeding (agents/default/planner.yaml)

  • Read-only planner subagent decomposes tasks into N distinct, non-overlapping seeds
  • Prevents parallel workers from converging on identical solutions
  • Registered in agent.yaml alongside existing subagents

Changes

File Change
src/pythinker_code/utils/trust.py NewUntrustedData with render_for_prompt()
src/pythinker_code/utils/artifacts.py New — 4 boundary-artifact dataclasses
src/pythinker_code/agents/default/planner.yaml New — recon planner subagent spec
src/pythinker_code/tools/file/read.py Wrap all 3 ToolOk output paths
src/pythinker_code/tools/web/fetch.py Wrap all 3 builder.write() paths
src/pythinker_code/agents/default/coder.yaml +14 lines: ARTIFACT_CONTRACT
src/pythinker_code/agents/default/verifier.yaml +11 lines: ARTIFACT_RECEIPT
src/pythinker_code/agents/default/agent.yaml +3 lines: planner registration

Testing

  • 86 tests pass (39 new + 47 updated)
  • make check-pythinker-code clean (ruff + pyright + ty)
  • Design audit greps confirm full UntrustedData coverage, no raw bypasses

Security properties

Threat Defense
Prompt injection in fetched web page UntrustedData nonce wrapping at FetchURL
Prompt injection in reviewed source file UntrustedData nonce wrapping at ReadFile
Coder sycophancy infecting verifier Verifier receives only artifact struct
Parallel workers converging Planner seeds distinct starting angles

Summary by CodeRabbit

  • New Features

    • Added a recon-first planner agent that emits structured parallel task seeds.
    • Coder now emits a structured <coding_artifact> JSON block for handoff to verification.
  • Improvements

    • Introduced frozen, typed artifact models to enforce structured handoffs.
    • File and web tool outputs are wrapped/escaped as untrusted data before use.
    • Verifier now runs artifact tests and reports PASS/FAIL/FLAKY with evidence.
  • Tests

    • Expanded test coverage for wrapping, artifact models, verifier behavior, and agent specs.

…tion defense

Add three architectural patterns from the vulnerability-discovery harness
as zero-dependency, pure Python modules:

- UntrustedData: runtime-nonce wrapping for external data entering LLM
  prompts (ReadFile and FetchURL tool outputs)
- Boundary artifacts: CodingArtifact, VerificationResult,
  VulnerabilityArtifact, AuditVerdict dataclasses enforcing coder↔verifier
  information barrier
- Planner subagent: read-only recon decomposition for parallel worker seeding

Modified tools wrap all external content paths:
- ReadFile: directory listings, _read_forward, _read_tail
- FetchURL: markdown, trafilatura extraction, service path

Updated coder.yaml and verifier.yaml with artifact contract/receipt
instructions. Registered planner subagent in agent.yaml.

86 tests (8 trust + 12 artifacts + 11 wrapping integration + 8 helper
+ 47 updated existing) all pass. make check-pythinker-code clean.
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8ccf1d86-c980-428a-b2f5-4b7bb656c9ba

📥 Commits

Reviewing files that changed from the base of the PR and between f0de900 and 45c6e66.

📒 Files selected for processing (1)
  • tests/core/test_agent_spec.py

📝 Walkthrough

Walkthrough

Adds an UntrustedData envelope for external tool outputs, frozen artifact dataclasses for coder→verifier handoff, a read-only planner subagent and prompt contracts, applies wrapping to ReadFile/FetchURL, and migrates/extends tests and snapshots to validate the new wire formats.

Changes

Prompt Safety & Data Contracts

Layer / File(s) Summary
UntrustedData wrapper and unit tests
src/pythinker_code/utils/trust.py, tests/utils/test_trust.py
Frozen UntrustedData dataclass wraps untrusted strings in <untrusted_data id="..."> tags with a per-render nonce. Escapes embedded closing tags and includes tests for nonce uniqueness, tag format, payload preservation, escaping, and immutability.
Artifact dataclasses for agent communication
src/pythinker_code/utils/artifacts.py, tests/utils/test_artifacts.py
Adds frozen dataclasses used as the coder↔verifier contract: CodingArtifact, VerificationResult, VulnerabilityArtifact, and AuditVerdict, with JSON (de)serialization and unit tests for round-trip, defaults, immutability, and key names.

Agent Configuration & Tool Integration

Layer / File(s) Summary
Subagent configuration and system prompts
src/pythinker_code/agents/default/agent.yaml, src/pythinker_code/agents/default/planner.yaml, src/pythinker_code/agents/default/coder.yaml, src/pythinker_code/agents/default/verifier.yaml, tests/core/*, CHANGELOG.md, docs/en/release-notes/changelog.md
Wires a new planner subagent into the default agent. Adds planner.yaml (read-only recon planner that emits a <recon_seeds> JSON array of 3–5 non-overlapping task seeds, with a restricted allowlist). Extends coder to require a final <coding_artifact> JSON block. Extends verifier instructions to run artifact.test_command, compare to artifact.expected_behavior, adversarially test edge_cases_claimed, and report PASS/FAIL/FLAKY per observed evidence. Updates snapshots and packaging expectations.
ReadFile and FetchURL output wrapping
src/pythinker_code/tools/file/read.py, src/pythinker_code/tools/web/fetch.py
ReadFile now wraps directory listings, forward-read, and tail-read outputs with UntrustedData(...).render_for_prompt(). FetchURL wraps plain/markdown response text, extracted HTML text, and service-decoded content similarly; both add the UntrustedData import.
Test helper functions for unwrapping
tests/tools/_untrusted.py, tests/tools/test_untrusted_helper.py
Adds unwrap_untrusted() and assert_wrapped() to coerce/validate/unwrap envelope strings; tests validate round-trip, payload preservation, malformed-wrapper failure modes, and helper behavior.
Integration tests for tool wrapping
tests/tools/test_untrusted_wrapping.py
Integration tests covering ReadFile and FetchURL wrapper contracts: format, injection containment (embedded </untrusted_data>), nonce uniqueness, success/error-path differences, and a wire-format parity test with UntrustedData.render_for_prompt().
Updates to existing ReadFile and FetchURL tests
tests/tools/test_read_file.py, tests/tools/test_fetch_url.py
All assertions that previously inspected result.output are updated to unwrap the UntrustedData envelope before asserting, across listing, truncation/boundary, tail scenarios, HTML extraction, markdown snapshots, redirects, and service-backed fetch tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.64% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with 'feat' type and 'security' scope, clearly describing the main change: prompt injection defense via harness pattern extraction.
Description check ✅ Passed Description is comprehensive with clear structure, summary of changes, testing results, and security properties. However, it deviates from the template by omitting explicit Related Issue link and checklist sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/harness-pattern-extraction

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/utils/test_artifacts.py (1)

1-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test coverage is comprehensive, but depends on fixing artifacts.py.

The test suite correctly validates round-trips, defaults, immutability, field names, and formatting. However, test_coding_artifact_default_edge_cases() (lines 34-40) and test_verification_result_defaults() (lines 84-90) will fail until the default_factory=list[str] syntax errors in src/pythinker_code/utils/artifacts.py are corrected to default_factory=list.

Once artifacts.py is fixed, these tests provide solid schema validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/test_artifacts.py` around lines 1 - 167, The issue is invalid use
of PEP 695-style generics in dataclass default_factory; in artifacts.py replace
default_factory=list[str] with default_factory=list for the fields that should
default to empty lists (specifically the CodingArtifact.edge_cases_claimed field
and the VerificationResult.discovered_gaps field) so the dataclass definitions
(CodingArtifact, VerificationResult) use default_factory=list and keep their
type annotations as list[str] (or List[str]) for correct typing while ensuring
the defaults are valid at runtime.
src/pythinker_code/agents/default/planner.yaml (1)

24-44: ⚠️ Potential issue | 🟠 Major

Add focused tests for planner.yaml (planner subagent) spec invariants.
tests/core/test_agent_spec.py snapshots default subagents but doesn’t assert the planner entry from src/pythinker_code/agents/default/agent.yaml / src/pythinker_code/agents/default/planner.yaml. Add focused assertions for planner’s when_to_use, allowed_tools/exclude_tools, and the ROLE_ADDITIONAL contract that it must emit a <recon_seeds> JSON block (no trailing extra content).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pythinker_code/agents/default/planner.yaml` around lines 24 - 44, Add
focused unit assertions in tests/core/test_agent_spec.py that validate the
planner subagent spec: load the planner entry from the agent specs (the
planner.yaml content) and assert its when_to_use string equals the expected
paragraph, assert allowed_tools contains the exact list
["pythinker_code.tools.shell:Shell","pythinker_code.tools.file:ReadFile","pythinker_code.tools.file:Glob","pythinker_code.tools.file:Grep","pythinker_code.tools.file:SmartSearch"]
and exclude_tools contains the listed exclusions, and add a contract test for
ROLE_ADDITIONAL that validates outputs must start with a JSON object keyed by
"recon_seeds" with no trailing non-JSON content (i.e., emit only the
<recon_seeds> JSON block). Use the planner subagent spec keys when_to_use,
allowed_tools, exclude_tools and the ROLE_ADDITIONAL contract name to locate the
checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/agents/default/coder.yaml`:
- Around line 35-47: Update the artifact contract text in coder.yaml to
explicitly mark optional fields: state that "edge_cases_claimed" is optional
(and any other fields with defaults) so models don't feel required to emit them;
reference the CodingArtifact dataclass in artifacts.py and ensure the prompt
language clarifies which JSON keys are mandatory vs optional (e.g., list
required keys like "files_changed", "test_command", "expected_behavior" and note
"edge_cases_claimed" is optional/default-empty). Make the change in the block
containing the <coding_artifact> example and keep the final-output format and
prohibition on logs/reasoning unchanged.

In `@src/pythinker_code/utils/artifacts.py`:
- Line 58: The dataclass field discovered_gaps uses an invalid default_factory
(default_factory=list[str]) which will raise TypeError because list[str] is not
callable; change the field to use default_factory=list (e.g., discovered_gaps:
list[str] = field(default_factory=list)) so the factory is a callable and the
type hint remains list[str]; apply the same fix used for the other field
referenced on line 28.

In `@tests/tools/test_untrusted_wrapping.py`:
- Around line 203-231: In
test_fetchurl_injection_payload_in_html_does_not_escape_wrapper, replace the
conditional that skips assertions when extraction fails with explicit assertions
that extraction succeeded: assert not result.is_error and
isinstance(result.output, str) immediately after obtaining result (before
computing opening_count/closing_count), so failures in FetchURL extraction are
flagged; then proceed to compute opening_count and closing_count from
result.output and assert the wrapper integrity as before.

In `@tests/utils/test_trust.py`:
- Around line 32-35: Rename the unused local variable in function _body from
prefix to _ to follow the discard convention and silence the linter;
specifically change the unpacking line "prefix, _, rest =
rendered.partition(...)" to use "_" for the first element so it becomes "_, _,
rest = rendered.partition(...)" and leave the rest of _body unchanged.

---

Outside diff comments:
In `@src/pythinker_code/agents/default/planner.yaml`:
- Around line 24-44: Add focused unit assertions in
tests/core/test_agent_spec.py that validate the planner subagent spec: load the
planner entry from the agent specs (the planner.yaml content) and assert its
when_to_use string equals the expected paragraph, assert allowed_tools contains
the exact list
["pythinker_code.tools.shell:Shell","pythinker_code.tools.file:ReadFile","pythinker_code.tools.file:Glob","pythinker_code.tools.file:Grep","pythinker_code.tools.file:SmartSearch"]
and exclude_tools contains the listed exclusions, and add a contract test for
ROLE_ADDITIONAL that validates outputs must start with a JSON object keyed by
"recon_seeds" with no trailing non-JSON content (i.e., emit only the
<recon_seeds> JSON block). Use the planner subagent spec keys when_to_use,
allowed_tools, exclude_tools and the ROLE_ADDITIONAL contract name to locate the
checks.

In `@tests/utils/test_artifacts.py`:
- Around line 1-167: The issue is invalid use of PEP 695-style generics in
dataclass default_factory; in artifacts.py replace default_factory=list[str]
with default_factory=list for the fields that should default to empty lists
(specifically the CodingArtifact.edge_cases_claimed field and the
VerificationResult.discovered_gaps field) so the dataclass definitions
(CodingArtifact, VerificationResult) use default_factory=list and keep their
type annotations as list[str] (or List[str]) for correct typing while ensuring
the defaults are valid at runtime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 50cd1b5b-7ba3-4d62-b531-3a46fa742306

📥 Commits

Reviewing files that changed from the base of the PR and between e7461b4 and 9f069ea.

📒 Files selected for processing (15)
  • src/pythinker_code/agents/default/agent.yaml
  • src/pythinker_code/agents/default/coder.yaml
  • src/pythinker_code/agents/default/planner.yaml
  • src/pythinker_code/agents/default/verifier.yaml
  • src/pythinker_code/tools/file/read.py
  • src/pythinker_code/tools/web/fetch.py
  • src/pythinker_code/utils/artifacts.py
  • src/pythinker_code/utils/trust.py
  • tests/tools/_untrusted.py
  • tests/tools/test_fetch_url.py
  • tests/tools/test_read_file.py
  • tests/tools/test_untrusted_helper.py
  • tests/tools/test_untrusted_wrapping.py
  • tests/utils/test_artifacts.py
  • tests/utils/test_trust.py

Comment thread src/pythinker_code/agents/default/coder.yaml
Comment thread src/pythinker_code/utils/artifacts.py
Comment thread tests/tools/test_untrusted_wrapping.py
Comment thread tests/utils/test_trust.py
- Add planner to test snapshots in test_agent_spec.py, test_default_agent.py,
  and test_pyinstaller_utils.py
- Update coder ROLE_ADDITIONAL snapshot to include artifact contract block
- Fix _bypass_ssrf_validation fixture scope (non-autouse, explicit parameter)
- Make FetchURL injection test unconditional (assert not is_error)
- Remove redundant _prefix variable in test_trust._body()
- Mark edge_cases_claimed as optional in coder.yaml artifact contract
- Clarify planner.yaml final response contract (seeds-only, no preamble)
- Add Unreleased CHANGELOG entry for the feature and sync docs copy
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Add explicit assertions for planner's when_to_use, allowed_tools,
exclude_tools, and ROLE_ADDITIONAL recon_seeds contract invariants
in test_load_default_agent_spec.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/core/test_agent_spec.py`:
- Around line 441-473: The planner subagent test is missing the same property
assertions as the other subagents; add assertions checking
subagent_specs["planner"].name, subagent_specs["planner"].system_prompt_path
(should equal DEFAULT_AGENT_FILE.parent / "system.md"),
subagent_specs["planner"].model (snapshot(None)), and
subagent_specs["planner"].tools (a snapshot of the final tool list after
applying allowed_tools and exclude_tools) so the planner validates name,
system_prompt_path, system_prompt_args, when_to_use, model, allowed_tools,
exclude_tools, tools, and subagents consistently with the other subagent blocks;
place these checks alongside the existing planner assertions and compute the
expected tools snapshot by taking the combined tool set that results from
allowed_tools minus exclude_tools (and including any default/global tools used
elsewhere in the test suite).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c98ba43a-732e-4547-a970-efae07444885

📥 Commits

Reviewing files that changed from the base of the PR and between e9127b2 and 521408a.

📒 Files selected for processing (1)
  • tests/core/test_agent_spec.py

Comment thread tests/core/test_agent_spec.py
…agent blocks

Add name, system_prompt_path, system_prompt_args, model, and tools snapshot
assertions for the planner subagent, consistent with how coder, explore, and
plan are tested. Replaces the loose string-in checks with a full snapshot of
ROLE_ADDITIONAL so the recon_seeds contract is pinned exactly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/core/test_agent_spec.py`:
- Around line 443-466: Add lightweight semantic invariant assertions after the
existing snapshot assertion in the test for
subagent_specs["planner"].system_prompt_args (ROLE_ADDITIONAL): check that the
ROLE_ADDITIONAL text contains the <recon_seeds> tag, enforces that the final
response must contain only seeds (presence of "ONLY" or phrasing like "no
preamble"), and asserts presence of words requiring distinct/non-overlapping
seeds (e.g., "distinct" and "non-overlapping"); keep the full snapshot assertion
but supplement it with these targeted asserts to guard the protocol contract
without making the test brittle.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6970ecd2-e719-4dbd-bf3d-eae8cde45508

📥 Commits

Reviewing files that changed from the base of the PR and between 521408a and f0de900.

📒 Files selected for processing (1)
  • tests/core/test_agent_spec.py

Comment thread tests/core/test_agent_spec.py
@elkaix
elkaix merged commit e067caf into main Jun 7, 2026
28 checks passed
@elkaix
elkaix deleted the feat/harness-pattern-extraction branch July 17, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant