feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(evaluators): add yelp.detect_secrets contrib evaluator - #196

Open
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator
Open

feat(evaluators): add yelp.detect_secrets contrib evaluator#196
lan17 wants to merge 21 commits into
mainfrom
feature/detect-secrets-evaluator

Conversation

@lan17

@lan17lan17 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

New contrib evaluator that scans selector-selected payloads for potential secrets using
Yelp detect-secrets, wired through the
detect-secrets-async subprocess-pool runtime.

Registered under the entry point yelp.detect_secrets.

Why this matters for agent workflows

Agent payloads move secrets around constantly without anyone meaning to. Concrete leak surfaces
in selector-selected step payloads:

  • LLM tool outputs that echo upstream API responses (auth headers, bearer tokens, session cookies).
  • Config dumps and environment snapshots fetched by a code tool.
  • Log lines emitted by user-facing tools and fed back into the LLM context.
  • Retrieved documents that happened to contain credentials.

Agents chain tool calls, so a leaked token in one step becomes input to the next — that's the
blast radius this evaluator is here to cap. Agent Control's evaluator layer is the natural gate:
it runs after the selector narrows the payload to the relevant field and before the control's
policy decision is committed.

Yelp detect-secrets contributes a battle-tested detector set — AWS keys, GitHub tokens, Basic
auth, private keys, high-entropy blobs, and keyword patterns — with per-request plugin narrowing
when you want fewer false positives on a specific control.

Why a separate async runtime

detect-secrets is synchronous and configures itself via process-global settings, which makes
asyncio.to_thread (theatrical timeouts — the scan keeps running) and a serialising lock (kills
throughput) both poor fits for Agent Control. The external
detect-secrets-async package wraps it in a
bounded pool of long-lived subprocess workers with real timeouts, per-request plugin isolation,
and automatic worker replacement on timeout/crash/cancellation. This PR is the thin Agent Control
adapter on top of that runtime.

What the evaluator does

  1. Normalize the selector payload:
    • None → no match
    • str → scanned directly
    • dict / list → deterministic pretty JSON with RFC 6901 pointer mapping
    • int / float / bool → JSON scalar text
  2. Filter lines matching exclude_lines_regex (blanked, so line numbers stay stable).
  3. Enforce the max_bytes cap on post-filter UTF-8 bytes.
  4. Scan via detect-secrets-async using the shared host-level runtime.
  5. Map findings into EvaluatorResult:
    • str payloads get line_number.
    • Structured payloads get json_pointer, conservatively truncated at any secret-looking
      segment in the path so a token appearing as a key name never leaks through the pointer.
    • Plaintext, snippets, full matching lines, and upstream hashed_secret are never surfaced.

Example

A control fragment that scans the output field for GitHub and AWS credentials, failing open on
evaluator errors:

{
"selector": { "path": "output" },
"evaluator": {
"name": "yelp.detect_secrets",
"config": {
"timeout_ms": 10000,
"on_error": "allow",
"enabled_plugins": ["GitHubTokenDetector", "AWSKeyDetector"]
}
}
}

For a plain-string payload "github_token = 'ghp_abc...'":

result.matched=Trueresult.confidence=1.0result.metadata= {
"findings_count": 1,
"findings": [{"type": "GitHub Token", "line_number": 1}],
"normalized_payload_type": "str",
"detect_secrets_version": "1.5.0",
}

For a structured payload {"response": {"headers": {"authorization": "ghp_abc..."}}}:

result.matched=Trueresult.metadata= {
"findings_count": 1,
"findings": [
{"type": "GitHub Token", "json_pointer": "/response/headers/authorization"}
],
"normalized_payload_type": "dict",
"detect_secrets_version": "1.5.0",
}

A dict keyed by a secret-looking string reports the safe ancestor instead of leaking the key:

# payload: {"ghp_abc...": {"nested": "safe"}}# result.metadata["findings"] == [{"type": "GitHub Token", "json_pointer": ""}]

Config

FieldDefaultPurpose
timeout_ms10_000Full request lifecycle: queue wait + scan.
on_error"allow"Fail-open (allow) or fail-closed (deny) on evaluator failure.
max_bytes1_048_576Max normalized post-filter UTF-8 size.
enabled_pluginsNoneUpstream plugin class names; validated at config parse time via detect_secrets_async.get_runtime_info().available_plugin_names. None uses the pinned upstream default set.
exclude_lines_regex[]RE2 patterns; matching lines are blanked before scanning.

Failure handling

Every failure maps to a stable metadata["failure_mode"]: normalization_error,
payload_too_large, queue_full, queue_timeout, worker_startup_error, worker_timeout,
worker_crash, worker_protocol_error, runtime_error.

on_error controls the fallback in EvaluatorResult:

  • allowmatched=False, metadata["fallback_action"]="allow"
  • denymatched=True, metadata["fallback_action"]="deny"

Consumers should branch on metadata["failure_mode"] + metadata["fallback_action"], not on
matched alone, to distinguish a real finding from a fail-closed evaluator failure.

Dependencies

Validation

make check in evaluators/contrib/detect_secrets:

  • 90 tests pass, covering detection, structured-pointer mapping, RE2 exclusion on both string
    and structured payloads, plugin validation (strip + dedup + unknown rejection), max_bytes
    boundary behavior, recursive / NaN / empty-container / unsupported-type normalization paths,
    timeout short-circuit, every failure_mode path × {allow, deny} (16 runtime + 4
    evaluator-layer combinations), FAILURE_MESSAGES drift pin against the ScanFailureCode enum,
    concurrent dispatch on a cached evaluator instance, and entry-point .load() round-trip.
  • mypy strict clean.
  • ruff check + format clean.
  • Coverage 98% (config.py 100%, evaluator.py 98%, normalization.py 98%).

@codecov

codecovBot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94721% with 7 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
...tor_detect_secrets/detect_secrets/normalization.py98.01%3 Missing ⚠️
...agent_control_evaluator_detect_secrets/__init__.py71.42%2 Missing ⚠️
...aluator_detect_secrets/detect_secrets/evaluator.py98.50%2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17lan17 changed the title feat: add detect-secrets contrib evaluatorfeat(evaluators): add detect-secrets contrib evaluatorApr 22, 2026
@lan17lan17 changed the title feat(evaluators): add detect-secrets contrib evaluatorfeat(evaluators): add yelp.detect_secrets contrib evaluatorApr 22, 2026
lan17 added 4 commits April 22, 2026 17:11
Adds the 10 gap categories flagged in review, with given/when/then
behavioral style:
- parametric failure-mode matrix: every ScanFailureCode x {allow, deny}
plus evaluator-layer failures (normalization_error, payload_too_large)
- FAILURE_MESSAGES drift pin against ScanFailureCode enum
- normalization edge cases: top-level set, NaN/+-inf primitives,
empty dict / list, boolean/None dict keys, tuple dict keys
- runtime-error paths: get_runtime_info failure during non-None
evaluate (previously only reached via None short-circuit),
RuntimeConfigConflictError from get_runtime
- exclude_lines_regex on structured payloads: blanking suppresses
findings on matched lines and preserves pointers for unmatched ones
- max_bytes boundary: exactly-at-limit accepted, one-byte-over rejected
- multi-line string with distinct findings preserves line numbers
- list with scalar element maps pointer to index
- concurrent evaluate() on one cached instance stays correct
- _safe_structured_pointer returns None for missing location
- _key_name_is_secret_like for None and non-identifier/scalar-like keys
- entry-point .load() round-trips to DetectSecretsEvaluator
- config validator edges: explicit None enabled_plugins, whitespace-only
entry rejected, whitespace strip + dedup, positive-int bounds on
timeout_ms / max_bytes, Literal validation on on_error
Coverage: 93% -> 98% (config 96 -> 100, evaluator 94 -> 98,
normalization 92 -> 98). 39 -> 90 passing tests.
Sign up for freeto 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

@lan17