Output-side DLP (Design)
Status: design. This document proposes scanning agent-authored git output for secrets and PII before it leaves the AIFactory boundary. It is not yet wired into a runtime hook; the reuse seam and a minimal implementation are described so the hook can be added when a chokepoint is available.
The gap (#323, #310)
AIFactory already scans on the input/commit side: validate_git_commit in
apps/backend/security/git_validators.py runs the secret scanner over staged
files before every git commit and blocks on a hit, and auto-unstages
.aifactory/ spec artifacts. See apps/backend/security/scan_secrets.py.
But a coding agent produces more outbound text than commit contents:
- Commit messages — scanned only incidentally; a secret pasted into a commit message body is not caught by the staged-file scan.
- PR title and body — authored by the agent at PR-endgame and pushed to GitHub. Never scanned.
- PR/issue comments — agent replies during review handback. Never scanned.
Any of these can exfiltrate a credential or personal data past the boundary even when the committed files are clean.
Reuse seam
The existing scanner already scans arbitrary strings — no new pattern engine is needed:
scan_secrets.scan_content(content: str, file_path: str) -> list[SecretMatch]runs every pattern inALL_PATTERNS(generic API-key/token/password/base64 plus service-specific: OpenAI/Anthropic, AWS, GCP, GitHub PAT, ...) over any text, applying the sameis_false_positivefilter used at commit time.git_validators._format_secret_error(matches)already renders matches into an actionable, agent-readable message.
So DLP on outbound text is: pass the text to scan_content, and on a non-empty
result either block (raise / refuse to create the PR) or redact.
Proposed design
- PII patterns. Extend the pattern set with a small, high-precision PII
group (email, phone, national-id/SSN-shaped, credit-card via a Luhn check) in
scan_secrets.py, guarded so the false-positive filter keeps example/test data quiet. Reused automatically by every caller ofscan_content. - One choke helper. Add a thin
dlp.scan_outbound(text, label) -> list[SecretMatch]that wrapsscan_content(text, label)and applies a.dlpignoreallowlist (mirroring.secretsignore). One place to reason about outbound policy. - Wire at the boundaries, all fail-closed on a hit:
- PR create — scan
title + bodybefore the GitHub API call; refuse and surface the matches to the agent (same UX as a blocked commit) so it rewrites the text. - PR/issue comment — scan the comment body before posting.
- Commit message — scan the message alongside the existing staged-file scan in
validate_git_commit.
- PR create — scan
- Mode flag, mirroring the model registry and the commit scanner:
AIFACTORY_OUTPUT_DLP=block(default at the boundary) /warn/off, so operators can dial enforcement without code changes. - Audit. On a block, record
{label, pattern_name, masked_match}(never the raw secret — usescan_secrets.mask_secret) to the audit log so the event is evidence, not just a refusal.
Why design-only for now
The commit-message case has an obvious home (validate_git_commit) and could
ship as a small safe addition. The PR title/body/comment cases need the
PR-endgame code path to route through a single scan chokepoint first; adding a
scanner with no caller would be dead code. The reuse seam above means each hook
is a few lines once its chokepoint exists — the scanning primitive
(scan_content) and the error rendering (_format_secret_error) are already in
place.