Skip to content

fix(agent-os): stop the adapter SSN pattern hard-blocking bare nine-digit numbers - #3591

Merged
MohammadHaroonAbuomar merged 6 commits into
microsoft:mainfrom
artificialvirus:fix/adapter-ssn-pattern-bare-nine-digits
Sep 14, 2026
Merged

MohammadHaroonAbuomar merged 6 commits into
microsoft:mainfrom
artificialvirus:fix/adapter-ssn-pattern-bare-nine-digits

Conversation

@artificialvirus

Copy link
Copy Markdown
Contributor

Description

PII_PATTERNS[0] in agent_os/integrations/base.py was
\b\d{3}[\s.-]?\d{2}[\s.-]?\d{4}\b — the separator is optional, so it matched
any bare nine-digit run. Unlike the detection-only Rego pattern, this constant
drives blocking call sites: autogen_adapter DropMessage,
PolicyViolationError on state updates, and bedrock_adapter (which aliases
the same tuple through _PII_RE). A tracking number, ABA routing number, or
ZIP+4 in ordinary message content produced a hard deny.

This is the adapter-side residual of the gateway-DoS that #3531 fixed on the MCP
path, tracked as #3532.

Fix

Ports #3531's pattern: (?<![A-Za-z0-9])\d{3}[\s.-]\d{2}[\s.-]\d{4}(?![A-Za-z0-9]).
A separator is required, and lookarounds replace \b so an SSN glued to _
(employee_123-45-6789) is still detected. bedrock_adapter._PII_RE already
aliases PII_PATTERNS, so one change covers both copies.

Note on #2635

This intentionally narrows the pattern that #2635 broadened. The space and dot
forms that change was about are retained; only the no-separator form is dropped,
and only on this blocking path. policy-engine/policy/lib/patterns.rego keeps
the looser form, which is correct there — that path reports rather than blocks.
Two assertions in test_pii_patterns.py that required the bare form to match
are updated accordingly, and the module docstring now records the tradeoff.

Testing

Added both-direction coverage per the issue: separated forms and the
underscore-glued form must match; Tracking: 123456789, ZIP 12345-6789,
ABA 021000021, and ten-digit runs must not.

Mutation-tested: restoring the loose pattern fails 4 of the new tests. Full
agent-os suite run against pristine main and this branch — failure sets are
identical (pre-existing, from optional deps and an agent_control_specification
version mismatch locally), with 4 additional passing tests. ruff check reports
no new findings.

Prior art / related projects

Pattern and test structure ported from #3531 (agent_os/credential_redactor.py,
"US SSN") in this repository.

Fixes #3532

…igit numbers

PII_PATTERNS[0] made the separator optional, so it matched any bare nine-digit
run. These patterns drive blocking call sites (autogen_adapter DropMessage,
PolicyViolationError on state updates, bedrock_adapter), so a tracking number,
ABA routing number, or ZIP+4 in ordinary content became a hard deny.

Ports the separator-required pattern from microsoft#3531, which closed the same
gateway-DoS on the MCP gateway copy in credential_redactor.py. Lookarounds
rather than \b keep an SSN glued to '_' detectable.

This narrows the pattern broadened in microsoft#2635: the space and dot forms that
change was about are retained, only the no-separator form is dropped. The
detection-only pattern in policy/lib/patterns.rego stays loose on purpose.

Fixes microsoft#3532

Signed-off-by: Alper <olperander@outlook.com>
Copilot AI lite review requested due to automatic review settings August 3, 2026 21:43
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions github-actions Bot added tests size/M Medium PR (< 200 lines) labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

Copilot AI 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.

Pull request overview

TL;DR: 0 blockers, 1 warning. This ships; warning is fine as a follow-up.

This PR aligns the adapter-side SSN blocking regex with the gateway-side fix from #3531 by requiring a separator and using lookaround anchors, preventing false-positive hard blocks on bare nine-digit runs (e.g., tracking numbers / ABA routing numbers) while still detecting underscore-glued SSNs.

Changes:

  • Tighten agent_os.integrations.base.PII_PATTERNS[0] SSN regex to require separators and use lookarounds instead of \b.
  • Update and extend test_pii_patterns.py to cover separator variants, underscore-glued detection, and common false-positive cases that must not trigger blocking.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
agent-governance-python/agent-os/src/agent_os/integrations/base.py Replace SSN pattern with separator-required + lookaround-anchored regex to avoid hard-blocking bare nine-digit runs.
agent-governance-python/agent-os/tests/test_pii_patterns.py Update SSN variant expectations and add negative-coverage tests for prior false-positive cases on blocking paths.

Comment thread agent-governance-python/agent-os/tests/test_pii_patterns.py Outdated
Review feedback: _SSN_FALSE_POSITIVES is not limited to bare nine-digit runs.
ZIP+4 is separated, and two cases carry ten and eight digits; those guard the
digit grouping rather than the separator requirement. Rename the test and say
so in the docstring.

Signed-off-by: Alper <olperander@outlook.com>
Copilot AI review requested due to automatic review settings August 3, 2026 21:48

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment thread agent-governance-python/agent-os/src/agent_os/integrations/base.py Outdated
The comment said policy-engine/policy/lib/patterns.rego keeps the loose
form because that path is detection-only. It is not: patterns.rego defines
deny_if_pattern, which returns a deny verdict, and agt_default.rego calls
it, so the same over-block is reachable on the OPA path. patterns.rego also
declares that it tracks base.py PII_PATTERNS, which this change makes
untrue, and the wheel-shipped copy under _stock_rego keeps the loose form
as well.

Record the divergence as a known gap instead, including why the Rego copies
cannot take this pattern verbatim: RE2 has no lookarounds, so an equivalent
must anchor with (^|[^A-Za-z0-9]) / ([^A-Za-z0-9]|$), which consumes a
character and shifts the span offset deny_if_pattern reports.

The test module docstring carried the same claim; corrected there too.

Signed-off-by: Alper <olperander@outlook.com>
Copilot AI review requested due to automatic review settings August 5, 2026 07:08

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

agent-governance-python/agent-os/tests/test_pii_patterns.py:35

  • The comment above _SSN_FALSE_POSITIVES says “Nine-digit content…”, but the list also includes a ZIP+4 (with a dash), a 10-digit run, and an 8-digit build tag. Consider broadening the wording so it matches what the test data actually covers.
# Nine-digit content that a blocking path must let through (#3532).
_SSN_FALSE_POSITIVES = ('Tracking: 123456789', 'order_123456789', 'ZIP 12345-6789', 'ABA 021000021', 'order 1234567890 shipped', 'build 12-34-5678 tagged')

agent-governance-python/agent-os/src/agent_os/integrations/base.py:34

  • The comment says both Rego copies “declare that they track this constant”, but only policy-engine/policy/lib/patterns.rego actually documents that relationship. agt-policies/src/agt/cli/_stock_rego/patterns.rego currently has no such note, so this is misleading.
    # NOT yet in step with the two Rego copies, which still carry the loose
    # form: policy-engine/policy/lib/patterns.rego and agt-policies
    # .../cli/_stock_rego/patterns.rego. Both declare that they track this
    # constant, and the first is reachable as a hard block through

Only policy-engine/policy/lib/patterns.rego declares that it tracks
base.py PII_PATTERNS; the wheel-shipped _stock_rego copy carries the
pattern with no such note. The previous wording claimed both did.

_SSN_FALSE_POSITIVES was described as nine-digit content, but two of the
six cases are a ten-digit run and an eight-digit build tag, which guard
the digit grouping rather than the separator requirement.

Signed-off-by: Alper <olperander@outlook.com>
Copilot AI review requested due to automatic review settings August 5, 2026 07:21

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

agent-governance-python/agent-os/src/agent_os/integrations/base.py:37

  • The comment says the policy-engine Rego pattern "declares that it tracks this constant" and contrasts it with the wheel-shipped copy, but the evidence is the opposite: the stock rego file documents that it tracks base.py::PII_PATTERNS, while policy-engine/policy/lib/patterns.rego does not. This makes the new comment misleading.
    # .../cli/_stock_rego/patterns.rego. The first declares that it tracks
    # this constant and is reachable as a hard block through
    # patterns.deny_if_pattern (agt_default.rego), so the same over-block
    # survives on the OPA path; the wheel-shipped copy carries the pattern
    # with no such note. They cannot take this pattern verbatim: RE2

agent-governance-python/agent-os/tests/test_pii_patterns.py:25

  • This module docstring says there are "two adapters" with a live boundary test in this module, but the file only exercises Bedrock via _scan_pii (Autogen is only checked for importing the shared constant). Update the wording to avoid overstating the coverage.
For the two adapters whose PII enforcement path can be exercised without
optional third-party SDKs, we also drive a live boundary test through the
real call site to verify the regex behaves the same end-to-end.

Only bedrock is driven at its real call site, through
bedrock_adapter._scan_pii. The autogen adapter is checked for sharing the
same regex objects, not exercised end-to-end, so "the two adapters ... we
also drive a live boundary test" overstated what the module covers.

Signed-off-by: Alper <olperander@outlook.com>
Copilot AI review requested due to automatic review settings August 5, 2026 09:31

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (1)

agent-governance-python/agent-os/src/agent_os/integrations/base.py:34

  • This comment references the agt-policies Rego copy using an ellipsis path (".../cli/_stock_rego/patterns.rego"), which makes it hard to locate the file in-repo. Since the exact file exists, it should be named explicitly to keep the note actionable.
    # NOT yet in step with the two Rego copies, which still carry the loose
    # form: policy-engine/policy/lib/patterns.rego and agt-policies
    # .../cli/_stock_rego/patterns.rego. The first declares that it tracks
    # this constant and is reachable as a hard block through

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ran the old and new patterns side by side, and checked every claim in the new comment block including
the ones about files this PR does not touch. All of them hold.

Measured:

input old \b\d{3}[\s.-]?\d{2}[\s.-]?\d{4}\b new
123-45-6789 HIT HIT
123 45 6789 HIT HIT
123.45.6789 HIT HIT
employee_123-45-6789 miss HIT
123456789 HIT miss
Tracking: 123456789 HIT miss
ZIP 12345-6789 HIT miss
ABA 021000021 HIT miss
order 1234567890 shipped miss miss
12-345-6789, 1234-56-7890, build 12-34-5678 tagged miss miss

Two things worth stating plainly from that table. The ZIP+4 case is the one I would lead with if this
needs justifying to anyone: 12345-6789 matched the old pattern because \d{3} takes 123, the
optional separator takes nothing, \d{2} takes 45, and the rest lines up. A US postal code in
ordinary content was a hard deny. And the change is not purely a narrowing: the lookarounds also fix
employee_123-45-6789, which the old \b missed because _ is a word character. So this closes a
false negative in the same edit that closes the false positives.

I verified the note about the two Rego copies too, because a comment asserting that other files are
out of step is worth being right about. Both still carry the loose form on main:

policy-engine/policy/lib/patterns.rego:17
  pii_ssn := `\b\d{3}[\s.\-]?\d{2}[\s.\-]?\d{4}\b`

agent-governance-python/agt-policies/src/agt/cli/_stock_rego/patterns.rego:8
  pii_ssn := `\b\d{3}[\s.\-]?\d{2}[\s.\-]?\d{4}\b`

So the over-block does survive on the OPA path, and the RE2 point is correct: RE2 has no lookarounds,
so the equivalent has to consume a character with (^|[^A-Za-z0-9]), which shifts the span offset
deny_if_pattern reports. Documenting that rather than silently leaving them divergent is the right
call, and calling out that only one of the two declares it tracks this constant is the detail that
makes the note actionable.

The issue chain is accurate: #2635 closed (the broadening that introduced this), #3531 merged (the
gateway-side fix this ports), #3532 open (this adapter-side residual).

One interaction the maintainers should see. #3801 is open against the same two files and it is
complementary rather than conflicting, but it will conflict textually. That PR adds a separate
context-cued pattern so a bare nine-digit run is still caught when it follows ssn,
social security or soc sec. Together the two produce the answer you actually want: bare digits no
longer hard-block, but SSN: 745102386 is still detected. Neither is complete without the other, and
whichever lands second needs a rebase.

One concrete thing to watch when they meet: your _ssn_pattern() helper returns "the SSN regex from
the shared PII_PATTERNS tuple", and #3801 adds a second SSN regex to that tuple. That helper will
need to disambiguate, or the false-positive tests could silently start asserting against the wrong
pattern.

Nothing blocking.

@MohammadHaroonAbuomar

Copy link
Copy Markdown
Collaborator

Closing and reopening to retrigger CI (the previous run was cancelled and is too old to rerun).

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • CI 'Spell-check changed files' fails on this PR's own added lines: Spell-check changed files: unknown words Lookarounds, neighbouring in added lines (fresh CI otherwise green after reopen) Please add the terms to .cspell-repo-terms.txt (or reword) so the gate passes.

Spell-check flagged Lookarounds and neighbouring in this PR's own added
lines. Reworded rather than extending .cspell-repo-terms.txt: the singular
lookaround already passes, and neighboring is the spelling the dictionary
carries.

Signed-off-by: Alper <olperander@outlook.com>

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved after group integration review; spell-check fix is comment-only rewording.

@MohammadHaroonAbuomar
MohammadHaroonAbuomar merged commit da2ce5d into microsoft:main Sep 14, 2026
125 checks passed
dylanyunlon added a commit to dylanyunlon/agent-governance-toolkit that referenced this pull request Sep 17, 2026
…PII patterns

CredentialRedactor.PII_PATTERNS and integrations.base.PII_PATTERNS both
require a separator between digit groups so that bare nine-digit numbers
(tracking numbers, ZIP+4, ABA routing numbers) do not hard-block at the
MCP gateway.  That requirement also lets a genuine SSN through when it
is written without separators next to an explicit cue:

    SSN: 745102386
    ssn=745102386
    social security number 745102386

Add a "US SSN (context-cued)" pattern to both detection sites.  The
pattern fires only when a case-insensitive cue keyword (ssn, social
security, social security number/num/no/#, soc sec) appears immediately
before the nine-digit run, keeping uncued bare digits as non-matches so
the false-positive suppression from microsoft#3531 is preserved.  Both detectors
carry the identical regex to stay in lockstep (microsoft#3591 tracks their
alignment).

Call chain (2 entry points, 2 source files, 2 test files):

  CredentialRedactor.find_pii_matches()
    -> PII_PATTERNS["US SSN (context-cued)"].pattern
  adapter PII scan (autogen/bedrock)
    -> integrations.base.PII_PATTERNS[1]

18 new tests across test_credential_redactor.py (11 positive, 6
negative, 1 matched_text assertion) and test_pii_patterns.py (5
positive, 4 negative).

Closes microsoft#3592

Signed-off-by: dylanyunlon <dogechat@163.com>
AlgoVoi (Christopher Hopley) (chopmob-cloud) added a commit to chopmob-cloud/agent-governance-toolkit that referenced this pull request Sep 26, 2026
Both SSN detectors, the gateway credential_redactor.py "US SSN" pattern
and the adapter integrations/base.py PII_PATTERNS entry, required a
separator between digit groups. A bare nine-digit SSN placed next to an
explicit cue (SSN: 745102386, ssn=745102386, social security number
745102386) matched neither, so cued tool output was neither redacted at
the gateway nor blocked at the adapters.

Add an identical context-cued branch to both detectors: a case-insensitive
cue (ssn or social security [number]) within a short window before an
undelimited nine-digit run. The bare-uncued corpus (tracking, ABA, ZIP+4,
ten-digit) stays non-matching, so pii_leak does not hard-block ordinary
traffic, and the separated forms still match. The two patterns are kept
byte-identical to preserve the parity restored in microsoft#3591; the RE2 Rego
copies are intentionally left for a separate change.

Lockstep regression tests assert the cued-bare forms match on both
detectors and the uncued corpus stays clean; reverting the source makes
them fail.

Fixes microsoft#3592

Signed-off-by: AlgoVoi <chopmob@gmail.com>
Yuvraj Singh (yuvrajsingh2428) pushed a commit to yuvrajsingh2428/agent-governance-toolkit that referenced this pull request Oct 1, 2026
…igit numbers (microsoft#3591)

* fix(agent-os): stop the adapter SSN pattern hard-blocking bare nine-digit numbers

PII_PATTERNS[0] made the separator optional, so it matched any bare nine-digit
run. These patterns drive blocking call sites (autogen_adapter DropMessage,
PolicyViolationError on state updates, bedrock_adapter), so a tracking number,
ABA routing number, or ZIP+4 in ordinary content became a hard deny.

Ports the separator-required pattern from microsoft#3531, which closed the same
gateway-DoS on the MCP gateway copy in credential_redactor.py. Lookarounds
rather than \b keep an SSN glued to '_' detectable.

This narrows the pattern broadened in microsoft#2635: the space and dot forms that
change was about are retained, only the no-separator form is dropped. The
detection-only pattern in policy/lib/patterns.rego stays loose on purpose.

Fixes microsoft#3532

Signed-off-by: Alper <olperander@outlook.com>

* test(agent-os): name the SSN negative test for what it actually covers

Review feedback: _SSN_FALSE_POSITIVES is not limited to bare nine-digit runs.
ZIP+4 is separated, and two cases carry ten and eight digits; those guard the
digit grouping rather than the separator requirement. Rename the test and say
so in the docstring.

Signed-off-by: Alper <olperander@outlook.com>

* docs(agent-os): correct the claim about the Rego SSN copies

The comment said policy-engine/policy/lib/patterns.rego keeps the loose
form because that path is detection-only. It is not: patterns.rego defines
deny_if_pattern, which returns a deny verdict, and agt_default.rego calls
it, so the same over-block is reachable on the OPA path. patterns.rego also
declares that it tracks base.py PII_PATTERNS, which this change makes
untrue, and the wheel-shipped copy under _stock_rego keeps the loose form
as well.

Record the divergence as a known gap instead, including why the Rego copies
cannot take this pattern verbatim: RE2 has no lookarounds, so an equivalent
must anchor with (^|[^A-Za-z0-9]) / ([^A-Za-z0-9]|$), which consumes a
character and shifts the span offset deny_if_pattern reports.

The test module docstring carried the same claim; corrected there too.

Signed-off-by: Alper <olperander@outlook.com>

* docs(agent-os): tighten two inaccurate comments from review

Only policy-engine/policy/lib/patterns.rego declares that it tracks
base.py PII_PATTERNS; the wheel-shipped _stock_rego copy carries the
pattern with no such note. The previous wording claimed both did.

_SSN_FALSE_POSITIVES was described as nine-digit content, but two of the
six cases are a ten-digit run and an eight-digit build tag, which guard
the digit grouping rather than the separator requirement.

Signed-off-by: Alper <olperander@outlook.com>

* docs(agent-os): stop overstating adapter coverage in the test docstring

Only bedrock is driven at its real call site, through
bedrock_adapter._scan_pii. The autogen adapter is checked for sharing the
same regex objects, not exercised end-to-end, so "the two adapters ... we
also drive a live boundary test" overstated what the module covers.

Signed-off-by: Alper <olperander@outlook.com>

* docs(agent-os): use spellings the repo dictionary already accepts

Spell-check flagged Lookarounds and neighbouring in this PR's own added
lines. Reworded rather than extending .cspell-repo-terms.txt: the singular
lookaround already passes, and neighboring is the spelling the dictionary
carries.

Signed-off-by: Alper <olperander@outlook.com>

---------

Signed-off-by: Alper <olperander@outlook.com>
Signed-off-by: yuvrajsingh2428 <offcyuvi2428@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR (< 200 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

adapter-side PII pattern still hard-blocks bare nine-digit numbers (residual of the #3353/#3531 gateway-DoS fix)

5 participants