Documentation
¶
Overview ¶
Package redact provides regex-based code anonymization. ASTAnonymizer uses regex-based identifier detection to locate identifiers in code blocks and replace them with deterministic anonymous names while preserving structure.
AST anonymization only activates at RedactionLevelMaximum. At Standard and Minimal levels, the v1 code block masking (maskCodeBlocks) still applies.
Package redact defines redaction levels for privacy control. Each level applies progressively more aggressive redaction. Category defaults activate secrets and paths at Minimal, PII and project identity at Standard, and individual rules may require Maximum. Maximum also enables AST anonymization and entropy detection.
Package redact provides PII detection and redaction. PIIDetector identifies email addresses, names, phone numbers, and other personally identifiable information in transcripts.
Package redact provides post-redaction residue detection. ResidueDetector scans already-redacted text for patterns that may have slipped through the primary redaction rules. It is advisory only — it never modifies content, only produces warnings.
Package redact provides privacy-preserving transcript redaction. SecretDetector identifies API keys, tokens, passwords, and other secrets in session transcripts using regex patterns.
Index ¶
- Constants
- Variables
- func RedactJSONDocBytes(r JSONRedactor, data []byte) []byte
- func RedactJSONLBytes(r JSONRedactor, data []byte, opts ...RedactJSONLBytesOption) ([]byte, error)
- func Version() string
- type ASTAnonymizer
- type Category
- type CategoryString
- type CodeBlock
- type DefaultRedactor
- func (r *DefaultRedactor) Detect(input string) []Match
- func (r *DefaultRedactor) Level() string
- func (r *DefaultRedactor) Redact(input string, matches []Match) string
- func (r *DefaultRedactor) RedactJSON(value any) any
- func (r *DefaultRedactor) RedactMetadata(meta *schema.UnifiedMetadata) *schema.UnifiedMetadata
- func (r *DefaultRedactor) RedactText(input string) (output string)
- func (r *DefaultRedactor) Report() RedactionReport
- func (r *DefaultRedactor) RuleSetVersion() string
- type DefaultResidueDetector
- type FallbackAnonymizer
- type FalsePositiveCase
- type FilePathFilter
- type JSONRedactor
- type Match
- type NoopFilePathFilter
- type Option
- type PIIDetector
- type RedactJSONLBytesOption
- type RedactionLevel
- type RedactionReport
- type Redactor
- type RegexAnonymizer
- type ResidueDetector
- type ResidueRule
- type ResidueWarning
- type Rule
- type SecretDetector
- type SupportedLang
- type TitleContext
- type TitlePipeline
- func (p *TitlePipeline) Generate(firstTurn string, context TitleContext) (TitleResult, error)
- func (p *TitlePipeline) GenerateFromTurns(turns []string, context TitleContext) (TitleResult, int, []error)
- func (p *TitlePipeline) Sanitize(title string, context TitleContext) (TitleResult, error)
- func (p *TitlePipeline) SimpleTitle(firstTurn string, harness schema.Harness) (string, error)
- type TitleResult
- type TreeSitterAnonymizer
- type UserPattern
- type XDGPaths
Constants ¶
const ( WrapperSystemReminder = "system-reminder" WrapperUserQuery = "user_query" WrapperCommandName = "command-name" WrapperCommandMessage = "command-message" WrapperCommandArgs = "command-args" WrapperLocalCommandCaveat = "local-command-caveat" WrapperLocalCommandStdout = "local-command-stdout" WrapperLocalCommandStderr = "local-command-stderr" WrapperTaskNotification = "task-notification" WrapperTeammateMessage = "teammate-message" WrapperEnvironmentContext = "environment_context" WrapperSystemContext = "system-context" WrapperAgentMessage = "agent-message" WrapperRecommendedPlugins = "recommended_plugins" WrapperUserAction = "user_action" )
Harness-owned markup wrapper names. A harness injects these blocks into a recorded user turn: they carry command scaffolding, tool output, or system context, never user prose. They are exported so every consumer of a recorded transcript names the same markup as this package instead of repeating string literals.
const ( // DefaultScannerInitBuf is the initial buffer size for the JSONL scanner. // Matches the previous internal/defaults.ScannerInitBuf value. DefaultScannerInitBuf = 64 * 1024 // 64 KiB // DefaultScannerMaxLine is the maximum line size for the JSONL scanner. // Matches the previous internal/defaults.ScannerMaxLine value. DefaultScannerMaxLine = 10 * 1024 * 1024 // 10 MiB )
const AgentMailPrefix = "Another Claude session sent a message:"
AgentMailPrefix opens the turn a harness records when another agent session sends this one a message. The line precedes an agent-message block; the whole turn carries no user prose.
const InterruptedRequestPrefix = "[Request interrupted by user"
InterruptedRequestPrefix opens the turn a harness records when the user interrupts a response. Both recorded variants, "[Request interrupted by user]" and "[Request interrupted by user for tool use]", share it, and any text that follows is harness scaffolding, never user prose.
const MaximumAvailable = true
MaximumAvailable reports whether the Maximum redaction level (code-aware, tree-sitter AST anonymization) is compiled into this binary.
It is a build-tag-set constant, NOT a runtime probe: tree-sitter is a CGo dependency, so Maximum is reachable only in cgo builds. CLI help text, error paths, and tests branch on this typed constant rather than a stringly check.
In cgo builds it is true and newMaximumAnonymizer() returns the real tree-sitter-backed pipeline. The !cgo counterpart lives in maximum_nocgo.go.
const PreviewMaxLength = 40
PreviewMaxLength is the maximum number of bytes in a ResidueWarning.Preview.
const RuleSetVersion = "3.1.1"
RuleSetVersion is the semantic version of the compiled rule set.
Versioning conventions:
- Major bump: rule removal or replacement pattern narrowing (potential data-leak regression)
- Minor bump: new rules added (more patterns detected)
- Patch bump: regex refinement that does not remove or narrow existing coverage
Version history:
1.0.0 — baseline: 40 rules (31 secrets + 5 PII + 4 paths) migrated from internal/redact
1.1.0 — +10 CategoryProject rules (git remotes, import paths, Docker refs, branch names, CI vars)
1.2.0 — FilterFn on Rule struct; awsSecretKeyFilter reduces false positives on aws_secret_key;
creditCardFilter rejects UUID-fragment shaped matches; AWS keyword list tightened
(removed bare "secret"/"password"/"aws") to reduce FPs in coding transcripts.
2.0.0 — Aligned with Yelp/detect-secrets canonical regexes (MAJOR — narrows coverage):
openai_key requires T3BlbkFJ marker (was wrong "sk-(live|test|proj)-" Stripe-shaped format);
github_pat requires exact {36} body (was {20,}); stripe_key requires exact {24} (was {20,})
and adds rk_ restricted-key prefix; sendgrid_key requires exact {22}.{43} (was {22,}.{22,});
slack_token requires numeric segments (was any alphanumeric); discord_bot_token third
segment exact {27} (was {27,}); +twilio_auth_token (SK{32}); +private_key SSH2 ENCRYPTED
+ PuTTY-User-Key-File-2 sentinels. AWS access key prefix list intentionally remains
broader than detect-secrets (covers AGPA/AIDA/AROA/AIPA/ANPA/ANVA in addition to
AKIA/A3T/ASIA) — see rules.go comment for rationale.
2.1.0 — Added rules to close detect-secrets coverage gaps: +ABIA, +ACCA prefixes on
aws_access_key (SSO user/role credentials we previously missed);
+slack_webhook_url (incoming-webhook URL form); +basic_auth_uri (RFC 3986
URI-form user:pass@host). JWT regex extended to accept unsigned (2-segment)
JWTs. Imported 220+ verbatim test cases from detect-secrets/tests/plugins/
into the YAML fixture corpus (317 total cases, 89% TP match rate vs
detect-secrets; 11% are documented intentional broader-coverage deviations).
2.1.1 — Bugfix release: private_key_block adds dedicated PGP PRIVATE KEY BLOCK arm
(was generating wrong "BEGIN PGP PRIVATE KEY" header without the BLOCK suffix);
jwt_token tightens 2-segment lower bound to {10,} to reject doc-example shapes
like "eyJa.eyJb" while keeping all real unsigned JWT formats; FP corpus runner
now fail-closes on unknown rule IDs; added FP boundary cases for new v2.1.0
rules (slack_webhook_url, basic_auth_uri, twilio_auth_token, jwt_token).
2.1.2 — gitlab_agent
now bounds to detect-secrets' 50–1024 range via composed {50,1000}{0,24}
(was open-ended {50,}; RE2 caps single repeats at 1000), rejecting 1025+
char tokens; basic_auth_uri excludes template braces { } so
"https://{user}:{pass}@host" no longer matches; removed dead looksLikeFilePath
helper; rule-count comments replaced by TestRules_CountByCategory; misc test
comment/fixture corrections (Discord {27} mechanism, fp_/tp_ naming, fixture
organization). No new rules; two minor coverage narrowings (glagent upper
bound, basic_auth_uri templates).
2.2.0 — Added access_code rule to redact short-lived login/verification/device
codes that appear in transcript text (e.g. "Access code: ABCD-1234").
2.2.2 — Bugfix: restore correct redaction when two home paths appear on one line
without Perl lookahead (unsupported by Go regexp). Uses single-segment
rules plus spaced-username companion rules and FilterFn prefix rejection.
3.0.0 — git_remote_https / git_remote_ssh / git_branch_output moved to Maximum-only
via per-rule minimum levels; Standard no longer redacts git URLs or branch names.
3.1.0 — a redacted path keeps its last folder and replaces everything above it
with one <PATH> placeholder, in the slash, single-dash and double-dash
forms; the prefixes come from every value the session records, the
project and host slugs and Windows paths included, not from the
working directory alone; a slug is read by the shape of its field;
the fallback slug rules fire on a machine-name-prefixed slug; titles use the same form and stored
two-placeholder values converge on it.
3.1.1 — a location that is only the account name, recorded by a session run
straight from the home folder, collapses to the placeholder in the
path form and in both slug forms instead of returning the account.
const SkillBodyPrefix = "Base directory for this skill:"
SkillBodyPrefix opens a skill body that a harness injects as a complete user turn. A turn whose trimmed text starts with this prefix carries no user prose.
Variables ¶
var ResidueRules = []ResidueRule{ {ID: "residue_anthropic_key", Pattern: regexp.MustCompile(`sk-ant-[a-zA-Z0-9_-]{10,}`)}, {ID: "residue_openai_key", Pattern: regexp.MustCompile(`sk-(?:live|test|proj)-[a-zA-Z0-9_-]{10,}`)}, {ID: "residue_github_pat", Pattern: regexp.MustCompile(`gh[pours]_[a-zA-Z0-9]{10,}`)}, {ID: "residue_aws_access_key", Pattern: regexp.MustCompile(`(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{12,}`)}, {ID: "residue_stripe_key", Pattern: regexp.MustCompile(`sk_(?:live|test)_[a-zA-Z0-9]{10,}`)}, {ID: "residue_twilio_key", Pattern: regexp.MustCompile(`AC[a-f0-9]{20,}`)}, {ID: "residue_sendgrid_key", Pattern: regexp.MustCompile(`SG\.[a-zA-Z0-9_-]{10,}`)}, {ID: "residue_slack_token", Pattern: regexp.MustCompile(`xox[baprs]-[a-zA-Z0-9-]{8,}`)}, {ID: "residue_jwt_token", Pattern: regexp.MustCompile(`eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]+`)}, {ID: "residue_private_key", Pattern: regexp.MustCompile(`-----BEGIN\s+\S*\s*PRIVATE\s+KEY`)}, {ID: "residue_generic_api_key", Pattern: regexp.MustCompile(`(?i)(?:api[_-]?key|apikey)\s*[=:]\s*\S{10,}`)}, {ID: "residue_bearer_token", Pattern: regexp.MustCompile(`(?i)Bearer\s+\S{8,}`)}, {ID: "residue_basic_auth", Pattern: regexp.MustCompile(`(?i)(?:Authorization:\s*Basic|Basic\s+auth)\s+\S{8,}`)}, {ID: "residue_email", Pattern: regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)}, {ID: "residue_phone", Pattern: regexp.MustCompile(`(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}`)}, {ID: "residue_ssn", Pattern: regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`)}, {ID: "residue_credit_card", Pattern: regexp.MustCompile(`\b(?:\d{4}[-\s]?){3}\d{4}\b`)}, {ID: "residue_ip_address", Pattern: regexp.MustCompile(`\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`)}, {ID: "residue_unix_path", Pattern: regexp.MustCompile(`/(?:Users|home)/[a-zA-Z0-9._-]+`)}, {ID: "residue_windows_path", Pattern: regexp.MustCompile(`(?i)C:\\Users\\[a-zA-Z0-9._-]+`)}, {ID: "residue_base64_long", Pattern: regexp.MustCompile(`[A-Za-z0-9+/]{40,}={0,2}`)}, {ID: "residue_hex_long", Pattern: regexp.MustCompile(`\b[0-9a-f]{32,}\b`)}, {ID: "residue_broad_api_key", Pattern: regexp.MustCompile(`(?:sk|xox[baprs])-[A-Za-z0-9\-]{10,}`)}, {ID: "residue_artifactory_api_token", Pattern: regexp.MustCompile(`AKC[a-zA-Z0-9]{8,}`)}, {ID: "residue_artifactory_password", Pattern: regexp.MustCompile(`AP[0-9A-F][a-zA-Z0-9]{6,}`)}, {ID: "residue_azure_storage_key", Pattern: regexp.MustCompile(`AccountKey=[a-zA-Z0-9+/=]{40,}`)}, {ID: "residue_discord_bot_token", Pattern: regexp.MustCompile(`[MNO][a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{20,}`)}, {ID: "residue_gitlab_pat", Pattern: regexp.MustCompile(`(?:glpat|gldt|glft|glsoat|glrt)-[A-Za-z0-9_-]{15,}`)}, {ID: "residue_gitlab_runner_registration", Pattern: regexp.MustCompile(`GR1348941[A-Za-z0-9_-]{15,}`)}, {ID: "residue_gitlab_cicd", Pattern: regexp.MustCompile(`glcbt-[A-Za-z0-9_-]{15,}`)}, {ID: "residue_gitlab_incoming_mail", Pattern: regexp.MustCompile(`glimt-[A-Za-z0-9_-]{20,}`)}, {ID: "residue_gitlab_trigger", Pattern: regexp.MustCompile(`glptt-[A-Za-z0-9_-]{30,}`)}, {ID: "residue_gitlab_agent", Pattern: regexp.MustCompile(`glagent-[A-Za-z0-9_-]{40,}`)}, {ID: "residue_gitlab_oauth_secret", Pattern: regexp.MustCompile(`gloas-[A-Za-z0-9_-]{50,}`)}, {ID: "residue_mailchimp_api_key", Pattern: regexp.MustCompile(`[0-9a-z]{28,}-us[0-9]{1,2}`)}, {ID: "residue_npm_auth_token", Pattern: regexp.MustCompile(`/:_authToken=\s*\S{10,}`)}, {ID: "residue_pypi_token", Pattern: regexp.MustCompile(`pypi-AgEI[A-Za-z0-9_-]{40,}`)}, {ID: "residue_pypi_test_token", Pattern: regexp.MustCompile(`pypi-AgEN[A-Za-z0-9_-]{40,}`)}, {ID: "residue_square_oauth_secret", Pattern: regexp.MustCompile(`sq0csp-[0-9A-Za-z_-]{30,}`)}, {ID: "residue_telegram_bot_token", Pattern: regexp.MustCompile(`\d{7,}:[0-9A-Za-z_-]{30,}`)}, { ID: "residue_git_remote_https", Pattern: regexp.MustCompile(`https?://(?:github\.com|gitlab\.com|bitbucket\.org)/\S*`), }, { ID: "residue_git_remote_ssh", Pattern: regexp.MustCompile(`git@(?:github\.com|gitlab\.com|bitbucket\.org):\S*`), }, { ID: "residue_go_import_path", Pattern: regexp.MustCompile(`github\.com/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+`), }, { ID: "residue_go_module_path", Pattern: regexp.MustCompile(`module\s+github\.com/\S+`), }, { ID: "residue_npm_repo_url", Pattern: regexp.MustCompile(`"(?:repository|homepage|bugs)":\s*"https?://github\.com`), }, { ID: "residue_docker_image_ref", Pattern: regexp.MustCompile(`(?:FROM|image:)\s+[a-zA-Z0-9_./:-]+`), }, { ID: "residue_ghcr_image_ref", Pattern: regexp.MustCompile(`ghcr\.io/\S+`), }, { ID: "residue_git_branch_output", Pattern: regexp.MustCompile(`(?:\*\s+|origin/)[a-zA-Z0-9_./-]+`), }, { ID: "residue_gitlab_ci_project", Pattern: regexp.MustCompile(`CI_PROJECT_[A-Z_]+\s*[=:]\s*\S*`), }, { ID: "residue_github_actions_repo", Pattern: regexp.MustCompile(`GITHUB_REPOSITORY\s*[=:]?\s*\S*`), }, }
ResidueRules is the complete compiled residue rule table. All patterns are compiled at package init via regexp.MustCompile.
These are intentionally broader versions of the main 20 Rules (13 secrets + 5 PII + 2 paths), plus 3 additional patterns for base64, hex, and bearer tokens. They are designed to catch near-misses — patterns that are close to but not exactly matching the primary rules.
Trade-off note: residue_aws_secret_key was removed. The primary aws_secret_key rule matches exactly 40-char base64 (30-char keys with padding). A residue rule at [A-Za-z0-9+/]{30,} would be broader than residue_base64_long ([A-Za-z0-9+/]{40,}), covering 30–39 char strings that base64_long misses. However, this range produces significant noise on random base64-encoded data. We accept the 30–39 char detection gap in exchange for reduced false-positive rate. Document if a future high-confidence rule for that range becomes available.
var Rules = []Rule{ { ID: "anthropic_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`sk-ant-[a-zA-Z0-9]{4,}-[a-zA-Z0-9_-]{20,}`), Replacement: "<ANTHROPIC_KEY>", }, { ID: "openai_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`sk-[A-Za-z0-9_-]*[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}`), Replacement: "<OPENAI_KEY>", }, { ID: "github_pat", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36}`), Replacement: "<GITHUB_PAT>", }, { ID: "aws_access_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:A3T[A-Z0-9]|ABIA|ACCA|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}`), Replacement: "<AWS_ACCESS_KEY>", }, { ID: "aws_secret_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`([A-Za-z0-9+/]{40}|[A-Za-z0-9+/]{39}=|[A-Za-z0-9+/]{38}==)([^A-Za-z0-9+/=]|$)`), Replacement: "<AWS_SECRET_KEY>${2}", }, { ID: "stripe_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:r|s)k_(?:live|test)_[0-9a-zA-Z]{24}`), Replacement: "<STRIPE_KEY>", }, { ID: "twilio_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`AC[a-z0-9]{32}`), Replacement: "<TWILIO_KEY>", }, { ID: "twilio_auth_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`SK[a-z0-9]{32}`), Replacement: "<TWILIO_AUTH_TOKEN>", }, { ID: "sendgrid_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}`), Replacement: "<SENDGRID_KEY>", }, { ID: "slack_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?i)xox[abopsr]-(?:\d+-)+[a-z0-9]+`), Replacement: "<SLACK_TOKEN>", }, { ID: "slack_webhook_url", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?i)https://hooks\.slack\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+`), Replacement: "<SLACK_WEBHOOK_URL>", }, { ID: "jwt_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}(?:\.[a-zA-Z0-9_-]+)?`), Replacement: "<JWT_TOKEN>", }, { ID: "private_key_block", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:-----BEGIN (?:RSA |EC |DSA |OPENSSH |SSH2 ENCRYPTED )?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----|PuTTY-User-Key-File-2)`), Replacement: "<PRIVATE_KEY>", }, { ID: "generic_api_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?i)(?:api[_-]?key|apikey)\s*[=:]\s*["']?([a-zA-Z0-9_\-]{20,})["']?`), Replacement: "<API_KEY>", }, { ID: "bearer_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?i)Bearer\s+([a-zA-Z0-9_\-\.+/=]{20,})`), Replacement: "<BEARER_TOKEN>", }, { ID: "basic_auth", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?i)(?:Authorization:\s*Basic\s+|Basic\s+auth(?:orization)?\s*[=:]\s*)([a-zA-Z0-9+/=]{10,})`), Replacement: "<BASIC_AUTH>", }, { ID: "basic_auth_uri", Category: CategorySecrets, Pattern: regexp.MustCompile(`://[^:/?#\[\]@!$&'()*+,;={}\s]+:[^:/?#\[\]@!$&'()*+,;={}\s]+@`), Replacement: "://<BASIC_AUTH_URI>@", }, { ID: "access_code", Category: CategorySecrets, Pattern: regexp.MustCompile( `(?i)\b((?:access|verification|device|one[- ]?time|otp)\s*code(?:\s*(?:is|:|=)\s*|\s+))([A-Za-z0-9][A-Za-z0-9-]{5,31})\b`, ), Replacement: "${1}<ACCESS_CODE>", }, { ID: "artifactory_api_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:^|[\s=:"])AKC[a-zA-Z0-9]{10,}(?:[\s"]|$)`), Replacement: "<ARTIFACTORY_API_TOKEN>", }, { ID: "artifactory_password", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:^|[\s=:"])AP[0-9A-F][a-zA-Z0-9]{8,}(?:[\s"]|$)`), Replacement: "<ARTIFACTORY_PASSWORD>", }, { ID: "azure_storage_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`AccountKey=[a-zA-Z0-9+/=]{88}`), Replacement: "<AZURE_STORAGE_KEY>", }, { ID: "discord_bot_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`[MNO][a-zA-Z0-9_-]{23,25}\.[a-zA-Z0-9_-]{6}\.[a-zA-Z0-9_-]{27}`), Replacement: "<DISCORD_BOT_TOKEN>", }, { ID: "gitlab_pat", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:glpat|gldt|glft|glsoat|glrt)-[A-Za-z0-9_\-]{20,50}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_TOKEN>", }, { ID: "gitlab_runner_registration", Category: CategorySecrets, Pattern: regexp.MustCompile(`GR1348941[A-Za-z0-9_\-]{20,50}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_RUNNER_TOKEN>", }, { ID: "gitlab_cicd", Category: CategorySecrets, Pattern: regexp.MustCompile(`glcbt-(?:[0-9a-fA-F]{2}_)?[A-Za-z0-9_\-]{20,50}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_CICD_TOKEN>", }, { ID: "gitlab_incoming_mail", Category: CategorySecrets, Pattern: regexp.MustCompile(`glimt-[A-Za-z0-9_\-]{25}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_MAIL_TOKEN>", }, { ID: "gitlab_trigger", Category: CategorySecrets, Pattern: regexp.MustCompile(`glptt-[A-Za-z0-9_\-]{40}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_TRIGGER_TOKEN>", }, { ID: "gitlab_agent", Category: CategorySecrets, Pattern: regexp.MustCompile(`glagent-[A-Za-z0-9_\-]{50,1000}[A-Za-z0-9_\-]{0,24}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_AGENT_TOKEN>", }, { ID: "gitlab_oauth_secret", Category: CategorySecrets, Pattern: regexp.MustCompile(`gloas-[A-Za-z0-9_\-]{64}(?:[^A-Za-z0-9_]|$)`), Replacement: "<GITLAB_OAUTH_SECRET>", }, { ID: "mailchimp_api_key", Category: CategorySecrets, Pattern: regexp.MustCompile(`[0-9a-z]{32}-us[0-9]{1,2}(?:[^0-9]|$)`), Replacement: "<MAILCHIMP_API_KEY>", }, { ID: "npm_auth_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`//.+/:_authToken=\s*(?:npm_.+|[A-Fa-f0-9-]{36})`), Replacement: "<NPM_AUTH_TOKEN>", }, { ID: "pypi_api_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`pypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{70,}`), Replacement: "<PYPI_TOKEN>", }, { ID: "pypi_test_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`pypi-AgENdGVzdC5weXBpLm9yZw[A-Za-z0-9_-]{70,}`), Replacement: "<PYPI_TEST_TOKEN>", }, { ID: "square_oauth_secret", Category: CategorySecrets, Pattern: regexp.MustCompile(`sq0csp-[0-9A-Za-z\\_\-]{43}`), Replacement: "<SQUARE_OAUTH_SECRET>", }, { ID: "telegram_bot_token", Category: CategorySecrets, Pattern: regexp.MustCompile(`(?:^|[^0-9A-Za-z])\d{8,10}:[0-9A-Za-z_-]{35}(?:[^A-Za-z0-9_-]|$)`), Replacement: "<TELEGRAM_BOT_TOKEN>", }, { ID: "email", Category: CategoryPII, Pattern: regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`), Replacement: "<EMAIL>", FilterFn: emailFilter, }, { ID: "phone_us", Category: CategoryPII, Pattern: regexp.MustCompile(`(?:\+1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}`), Replacement: "<PHONE>", }, { ID: "ssn", Category: CategoryPII, Pattern: regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`), Replacement: "<SSN>", }, { ID: "credit_card", Category: CategoryPII, Pattern: regexp.MustCompile(`\b(?:\d{4}[-\s]?){3}\d{4}\b`), Replacement: "<CREDIT_CARD>", }, { ID: "ip_address", Category: CategoryPII, Pattern: regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b`), Replacement: "<IP_ADDRESS>", }, { ID: "unix_home_path", Category: CategoryPaths, Pattern: regexp.MustCompile(`(/(?:Users|home)/)[^/\s]+`), Replacement: "${1}<USER>", }, { ID: "unix_home_path_spaced", Category: CategoryPaths, Pattern: regexp.MustCompile(`(/(?:Users|home)/)([^/\s]+ [^/\s]+)/`), Replacement: "${1}<USER>/", }, { ID: "windows_home_path", Category: CategoryPaths, Pattern: regexp.MustCompile(`((?i)C:\\Users\\)[^\\\s]+`), Replacement: `${1}<USER>`, }, { ID: "windows_home_path_spaced", Category: CategoryPaths, Pattern: regexp.MustCompile(`((?i)C:\\Users\\)([^\\\s]+ [^\\\s]+)\\`), Replacement: `${1}<USER>\`, }, { ID: "claude_project_slug", Category: CategoryPaths, Pattern: regexp.MustCompile(`(^|\W)(-(?:home|Users)-)([a-zA-Z0-9_][a-zA-Z0-9_.-]*?)(-)`), Replacement: "${1}${2}<USER>${4}", }, { ID: "peasant_host_slug", Category: CategoryPaths, Pattern: regexp.MustCompile(`(--(?:home|Users)--)([a-zA-Z0-9_][a-zA-Z0-9_.-]*?)(--)`), Replacement: "${1}<USER>${3}", }, }
Rules is the complete compiled rule table. All patterns are compiled at package init — no runtime compilation cost.
Rules is exported as a mutable var to allow test injection (e.g., nil-Pattern rules for fail-closed testing). Production code treats it as read-only after init. An accessor returning a copy was considered but adds API surface for a single test.
Functions ¶
func RedactJSONDocBytes ¶
func RedactJSONDocBytes(r JSONRedactor, data []byte) []byte
RedactJSONDocBytes processes an entire JSON document, applying RedactJSON to the decoded value and marshalling back. Returns the original data on any error.
func RedactJSONLBytes ¶
func RedactJSONLBytes(r JSONRedactor, data []byte, opts ...RedactJSONLBytesOption) ([]byte, error)
RedactJSONLBytes processes a JSONL byte slice line by line, applying RedactJSON to each decoded line and marshalling back. Unparseable lines pass through unchanged.
A DefaultRedactor's constructor scanner configuration is the base. Per-call options take precedence. Other JSONRedactor implementations use the exported defaults as their base. If the scanner encounters a line exceeding the buffer (bufio.ErrTooLong), the function returns the original unmodified data as a fail-safe to prevent writing a truncated or partially-redacted transcript file.
Types ¶
type ASTAnonymizer ¶
type ASTAnonymizer interface {
// AnonymizeCode rewrites the code string for the given language.
// If lang is unsupported, returns code unchanged and nil error.
// Replacement is deterministic: same identifier → same anonymous name.
AnonymizeCode(lang SupportedLang, code string) (string, error)
}
ASTAnonymizer rewrites code blocks to replace identifiers while preserving structure. Replacement is deterministic: same identifier → same anonymous name within a single anonymizer instance (session-scoped).
Cross-instance determinism is guaranteed only when both instances process identical code: the mapping is derived from the set of identifiers found, not from a global registry.
func NewRegexAnonymizer ¶
func NewRegexAnonymizer() ASTAnonymizer
NewRegexAnonymizer returns a new ASTAnonymizer backed by regex-based identifier detection. The anonymizer maintains a session-scoped identifier map for determinism: same identifier → same anonymous name within the instance.
type Category ¶
type Category string
Category classifies what kind of sensitive data a Rule targets.
const ( // CategorySecrets targets API keys, tokens, credentials, and other secret values. CategorySecrets Category = "secrets" // CategoryPII targets personally identifiable information (email, phone, SSN, etc.). CategoryPII Category = "pii" // CategoryPaths targets filesystem paths that may expose usernames. CategoryPaths Category = "paths" // CategoryProject targets project identity information: git remotes, import // paths, Docker image refs, branch names, and CI environment variables. // Individual rules may require a stricter level than the category default. CategoryProject Category = "project" )
func AllCategories ¶
func AllCategories() []Category
AllCategories returns the canonical engine-category enumeration. The caller receives a copy so it cannot mutate validation or mapping behavior.
func (Category) String ¶
func (c Category) String() CategoryString
String returns the category's canonical rendered form. Invalid categories return the zero value, never a valid fallback. Call Validate at trust boundaries before rendering values that did not originate in this package.
type CategoryString ¶
type CategoryString string
CategoryString is the canonical rendered form of a redaction category. It is shared by API responses, generated fixtures, and any other consumer that needs stable category labels.
const ( CategoryStringCredential CategoryString = "CREDENTIAL" CategoryStringPII CategoryString = "PII" CategoryStringPath CategoryString = "PATH" CategoryStringInternal CategoryString = "INTERNAL" )
func (CategoryString) String ¶
func (s CategoryString) String() string
type CodeBlock ¶
type CodeBlock struct {
Lang SupportedLang
Content string
Start int // byte offset in source text where the full fence starts
End int // byte offset in source text where the full fence ends
}
CodeBlock represents a fenced code block extracted from markdown text.
func ExtractCodeBlocks ¶
ExtractCodeBlocks finds all fenced code blocks (```lang ... ```) in text. Returns extracted blocks with their language hints and byte offsets.
type DefaultRedactor ¶
type DefaultRedactor struct {
// contains filtered or unexported fields
}
DefaultRedactor is the production implementation of Redactor. It is safe for concurrent use via an internal mutex.
func (*DefaultRedactor) Detect ¶
func (r *DefaultRedactor) Detect(input string) []Match
Detect scans input for all sensitive patterns active at the receiver's redaction level. It returns a slice of Match values sorted by Offset (ascending).
Detect does NOT modify the input string. The returned MatchedText fields share the backing array of input — callers that need to hold matches past the input's lifetime must copy MatchedText themselves.
If no patterns match, Detect returns nil (not an empty slice).
func (*DefaultRedactor) Level ¶
func (r *DefaultRedactor) Level() string
Level returns the redaction level as a string.
func (*DefaultRedactor) Redact ¶
func (r *DefaultRedactor) Redact(input string, matches []Match) string
Redact replaces the byte spans identified by matches in input with their rule replacements, then returns the resulting string. It does NOT re-scan — only the provided match spans are replaced.
If matches is nil or empty, Redact returns input unchanged. Overlapping matches are skipped: if a span is already covered by a previously applied match, the overlapping match is silently ignored.
The output string is built in a single pass from left to right.
func (*DefaultRedactor) RedactJSON ¶
func (r *DefaultRedactor) RedactJSON(value any) any
RedactJSON recursively descends through a JSON-decoded value and redacts strings. Supported types: string, []any, map[string]any, json.Number. Other types pass through unchanged. json.Number is returned as-is: integers > 2^53 decoded via UseNumber() retain full precision.
func (*DefaultRedactor) RedactMetadata ¶
func (r *DefaultRedactor) RedactMetadata(meta *schema.UnifiedMetadata) *schema.UnifiedMetadata
RedactMetadata returns a deep copy of meta with sensitive fields redacted. The original meta pointer is never mutated.
Redaction is applied in two stages:
- redactPrivatePaths — context-aware slug/path replacement using extracted identifiers
- redactAllFields — standard RedactText on all sensitive fields (secrets, PII, remaining paths)
Fields redacted: Source.FilePath, CWD, HostSlug, Git.Remote/Branch/Worktree/Tracking, Project.FilePath/Name, Diagnostics.Warnings[i].Location/Message.
func (*DefaultRedactor) RedactText ¶
func (r *DefaultRedactor) RedactText(input string) (output string)
RedactText applies the redaction pipeline to plain text:
- AST anonymization at Maximum; lower levels preserve code for rule scanning
- Level-gated regex rules: built-ins use category defaults plus optional per-rule minimums; user-defined rules inherit their category default
- Entropy detection (Maximum only)
- Residue scan (advisory — appends to Report().Warnings, never modifies content)
Thread-safe. Fail-closed: panics are recovered and maximum-level rules are applied.
func (*DefaultRedactor) Report ¶
func (r *DefaultRedactor) Report() RedactionReport
Report returns a copy of the current RedactionReport. The Matches field contains the matches from the last Detect() call (last-call-only).
func (*DefaultRedactor) RuleSetVersion ¶
func (r *DefaultRedactor) RuleSetVersion() string
RuleSetVersion returns the semantic version of the compiled rule set. Delegates to the package-level Version() function.
type DefaultResidueDetector ¶
type DefaultResidueDetector struct{}
DefaultResidueDetector is the production implementation of ResidueDetector.
func (*DefaultResidueDetector) Scan ¶
func (d *DefaultResidueDetector) Scan(redactedText string) []ResidueWarning
Scan examines redactedText for patterns that may indicate missed sensitive data. Returns zero or more warnings; all matches are returned — callers that need deduplicated results should filter by RuleID/Location. The input string is never modified.
type FallbackAnonymizer ¶
type FallbackAnonymizer struct {
// contains filtered or unexported fields
}
FallbackAnonymizer tries the primary ASTAnonymizer first. If it returns an error, it falls back to the secondary. If the secondary also fails, returns the secondary's error.
This wraps TreeSitterAnonymizer (primary) with RegexAnonymizer (fallback) so that Maximum-level redaction degrades gracefully when tree-sitter fails (e.g., for malformed code snippets).
func NewFallbackAnonymizer ¶
func NewFallbackAnonymizer(primary, fallback ASTAnonymizer) *FallbackAnonymizer
NewFallbackAnonymizer returns an ASTAnonymizer that tries primary first, falling back to fallback on error. Panics if either argument is nil.
func (*FallbackAnonymizer) AnonymizeCode ¶
func (f *FallbackAnonymizer) AnonymizeCode(lang SupportedLang, code string) (string, error)
AnonymizeCode tries the primary anonymizer. If it returns an error, the fallback is used instead. Returns nil error if either succeeds.
type FalsePositiveCase ¶
type FalsePositiveCase struct {
// Name is a unique slug used as the t.Run subtest name.
Name string `yaml:"name"`
// Input is the text passed to Detect.
Input string `yaml:"input"`
// Rule is the rule ID to look for in the returned matches (e.g. "aws_secret_key").
Rule string `yaml:"rule"`
// Expect is either "match" or "no_match".
// "match" — the rule must fire on the input (true positive).
// "no_match" — the rule must NOT fire on the input (false positive guard).
Expect string `yaml:"expect"`
// Level is the redaction level at which to run Detect. Empty means Standard.
Level RedactionLevel `yaml:"level"`
// Reason documents why the case behaves as expected.
Reason string `yaml:"reason"`
// Source records where the pattern was observed:
// "real_transcript" — from an actual agent session
// "detect_secrets" — from the detect-secrets reference implementation
// "synthetic" — hand-crafted for coverage
Source string `yaml:"source"`
}
FalsePositiveCase is a single fixture entry in testdata/false_positives.yaml. Each case specifies an input string, the rule under test, the expected outcome ("match" or "no_match"), a human-readable reason, and the provenance of the case.
func LoadFalsePositiveFixtures ¶
func LoadFalsePositiveFixtures() ([]FalsePositiveCase, error)
LoadFalsePositiveFixtures parses the embedded YAML fixture file and returns all test cases. Returns an error if the YAML is malformed.
type FilePathFilter ¶
FilePathFilter reports whether entropy detection should be suppressed for a given source file path. This interface exists as an architectural anchor for future integration: once the transcript schema exposes per-turn source paths, wire this into detectEntropyInWords to skip entropy scanning on lock files (e.g. package-lock.json, go.sum, yarn.lock) and generated files (e.g. swagger.json). See URD R8 (unified-schema-kak3).
type JSONRedactor ¶
JSONRedactor is the minimal interface for transcript redaction. Both redact.Redactor and ingest.TextRedactor satisfy this.
type Match ¶
type Match struct {
// Rule is the rule ID that produced this match (e.g., "github_token", "email").
Rule string
// Offset is the byte offset of the match start in the input string.
Offset int
// Length is the byte length of the match.
Length int
// Category is the rule category that fired (e.g., CategorySecrets, CategoryPII).
Category Category
// MatchedText is input[Offset:Offset+Length] — zero-copy substring slice.
MatchedText string
}
Match represents a single detected sensitive pattern in the input string.
MatchedText is a zero-copy substring slice of the original input string (input[Offset:Offset+Length]). It shares the backing array of the input — do not hold a Match longer than the input string is alive.
Match is in-memory only — it is never serialised to disk or the network.
type NoopFilePathFilter ¶
type NoopFilePathFilter struct{}
NoopFilePathFilter is the current production implementation. It always returns false, meaning entropy detection runs unconditionally. Replace with a path-aware implementation when per-turn source paths are available.
func (NoopFilePathFilter) SuppressEntropy ¶
func (NoopFilePathFilter) SuppressEntropy(_ string) bool
type Option ¶
type Option func(*redactorConfig)
Option configures the redactor. Options are applied in order and only affect the specific parameter they override; all other parameters use their defaults.
func WithScannerBufSize ¶
WithScannerBufSize overrides the default scanner buffer sizes. Default: DefaultScannerInitBuf (64 KiB) and DefaultScannerMaxLine (10 MiB).
type PIIDetector ¶
type PIIDetector struct{}
PIIDetector finds and redacts personally identifiable information.
type RedactJSONLBytesOption ¶
type RedactJSONLBytesOption func(*scannerConfig)
RedactJSONLBytesOption configures the standalone RedactJSONLBytes function.
func WithRedactScannerBufSize ¶
func WithRedactScannerBufSize(initBuf, maxLine int) RedactJSONLBytesOption
WithRedactScannerBufSize overrides the default buffer sizes for RedactJSONLBytes. Default: DefaultScannerInitBuf (64 KiB) and DefaultScannerMaxLine (10 MiB).
type RedactionLevel ¶
type RedactionLevel string
RedactionLevel controls the aggressiveness of transcript redaction.
const ( // Minimal redacts detected secrets and identifying paths. Minimal RedactionLevel = "minimal" // Standard adds PII and project-identity rules that use the category default. Standard RedactionLevel = "standard" // Maximum adds stricter per-rule redaction, AST anonymization, and entropy detection. Maximum RedactionLevel = "maximum" )
func AllRedactionLevels ¶
func AllRedactionLevels() []RedactionLevel
AllRedactionLevels returns every engine level, ordered from weakest to strongest. The returned slice is an independent copy and may be modified by the caller.
func Max ¶
func Max(a, b RedactionLevel) RedactionLevel
Max returns whichever of a or b has the higher Ord (i.e. stricter redaction). If both are equal, a is returned.
func (RedactionLevel) IsValid ¶
func (l RedactionLevel) IsValid() bool
IsValid returns true if the level is one of the known variants.
func (RedactionLevel) Ord ¶
func (l RedactionLevel) Ord() int
Ord returns the ordinal for this level: Minimal=0, Standard=1, Maximum=2. Unknown levels return -1.
func (RedactionLevel) String ¶
func (l RedactionLevel) String() string
type RedactionReport ¶
type RedactionReport struct {
TotalRedactions int
Counts map[string]int // rule ID → redaction count
Categories []string // categories that fired at least once
Warnings []string // residue detection warnings (v2)
// Matches contains all matches from the last Detect() call.
// This is last-call-only (not accumulated across multiple Detect calls).
// In-memory only — never serialised to disk or network.
Matches []Match
}
RedactionReport summarises what has been redacted across all calls on a single Redactor.
type Redactor ¶
type Redactor interface {
// Detect scans input for sensitive patterns at the configured level.
// Returns matches sorted by offset. Does NOT modify input.
// Returns nil if no patterns match.
Detect(input string) []Match
// Redact replaces match spans in input with redaction placeholders.
// Does NOT re-scan — operates only on the provided matches.
// Returns input unchanged if matches is nil or empty.
Redact(input string, matches []Match) string
// RedactText is the single-step convenience API: equivalent to r.Redact(input, r.Detect(input)).
// Applies regex rules and entropy detection to plain text.
RedactText(input string) string
// RedactJSON recursively applies RedactText to all string values in a JSON-decoded value.
RedactJSON(value any) any
// RedactMetadata applies RedactText to sensitive fields in a DEEP COPY of the metadata.
// The original pointer is never mutated.
RedactMetadata(meta *schema.UnifiedMetadata) *schema.UnifiedMetadata
// Report returns an aggregate of all redactions performed so far.
// The Matches field contains all matches from the last Detect() call only (not accumulated).
Report() RedactionReport
// Level returns the redaction level as a string (e.g. "minimal", "standard", "maximum").
Level() string
// RuleSetVersion returns the semantic version of the compiled rule set (e.g. "1.1.0").
// Populated into RedactionInfo.RuleSetVersion on every redaction write.
RuleSetVersion() string
}
Redactor applies privacy-preserving redaction to text, JSON values, and session metadata. All methods are safe for concurrent use.
func NewRedactor ¶
func NewRedactor(level RedactionLevel, userPatterns []UserPattern, xdg XDGPaths, opts ...Option) (Redactor, error)
NewRedactor returns a Redactor configured for the given level, user-defined patterns, and XDG path rules.
userPatterns are compiled at construction time. An invalid regex in any pattern causes NewRedactor to return an error immediately (fail-closed: no partial redactor is returned). User pattern categories must be one of the known variants (CategorySecrets, CategoryPII, CategoryPaths, CategoryProject). User patterns inherit the category's default minimum: secrets and paths at Minimal, PII and project identity at Standard.
xdg generates additional path redaction rules for non-standard XDG locations where the username may appear outside /home/ or /Users/ prefixes.
At Maximum level, the ASTAnonymizer is a FallbackAnonymizer that uses TreeSitterAnonymizer as the primary and RegexAnonymizer as the fallback (see newMaximumAnonymizer in maximum_cgo.go). Because tree-sitter is a cgo dependency, Maximum is available ONLY in cgo builds: in a !cgo binary (redact.MaximumAvailable == false) NewRedactor(Maximum) returns an actionable error rather than silently degrading. At Standard/Minimal levels, the ASTAnonymizer is not used at Standard/Minimal; fenced code remains intact and the level-gated rules scan it like any other text.
type RegexAnonymizer ¶
type RegexAnonymizer struct {
// contains filtered or unexported fields
}
RegexAnonymizer replaces identifiers in code blocks with deterministic anonymous names while preserving language syntax structure.
Implementation note: Uses regex-based identifier detection rather than tree-sitter CGo bindings. This avoids CGo build complexity and is sufficient for the anonymization use case where we need to replace user-chosen names, not perform semantic analysis. The approach matches the spec's documented fallback strategy.
Determinism: same identifier → same anonymous name within an instance. Cross-instance determinism is achieved by sorting identifiers alphabetically before assigning names, so the mapping is input-dependent, not order-dependent. Cross-instance determinism only holds when both instances process identical code.
func (*RegexAnonymizer) AnonymizeCode ¶
func (a *RegexAnonymizer) AnonymizeCode(lang SupportedLang, code string) (string, error)
AnonymizeCode replaces user-defined identifiers in code with deterministic anonymous names while preserving language keywords, built-in types, and syntax.
Unknown languages are returned unchanged with nil error. Parse errors are not possible with regex-based approach; invalid code is processed best-effort.
type ResidueDetector ¶
type ResidueDetector interface {
// Scan examines redactedText for patterns that may indicate missed
// sensitive data. Returns zero or more warnings; all matches are
// returned. The input string is never modified.
// Callers that need deduplicated results should filter by RuleID/Location.
Scan(redactedText string) []ResidueWarning
}
ResidueDetector scans already-redacted text for potential missed patterns. Advisory only — never modifies content.
func NewResidueDetector ¶
func NewResidueDetector() ResidueDetector
NewResidueDetector returns a ResidueDetector that scans for missed patterns.
type ResidueRule ¶
ResidueRule is a compiled pattern for advisory residue scanning. Residue rules are intentionally broader/noisier than the primary Rules table — a higher false-positive rate is acceptable because warnings are advisory and never block the pipeline.
type ResidueWarning ¶
type ResidueWarning struct {
RuleID string // identifies which residue rule triggered
Location int // byte offset in redactedText where the match starts
Preview string // up to PreviewMaxLength bytes of surrounding context (itself redacted for safety)
}
ResidueWarning describes a potential missed sensitive pattern found during post-redaction residue scanning.
type Rule ¶
type Rule struct {
ID string
Category Category
MinimumLevel RedactionLevel // empty uses the category's default minimum
Pattern *regexp.Regexp // compiled at package init via regexp.MustCompile
Replacement string
// FilterFn is an optional post-match filter applied after the regex fires.
// If non-nil, FilterFn(matched, input, offset) must return true for the
// match to be kept. A false return suppresses the match entirely.
// Parameters:
// matched — the full matched text (same as input[offset:offset+len(matched)])
// input — the original full input string being scanned
// offset — byte offset of matched within input
// FilterFn must be safe for concurrent use (stateless).
FilterFn func(matched string, input string, offset int) bool
}
Rule is a compiled regex redaction rule with a category and replacement string.
type SecretDetector ¶
type SecretDetector struct{}
SecretDetector finds and redacts secret values in text.
type SupportedLang ¶
type SupportedLang string
SupportedLang represents a language for which AST anonymization is available.
const ( LangGo SupportedLang = "go" LangPython SupportedLang = "python" LangTypeScript SupportedLang = "typescript" LangJavaScript SupportedLang = "javascript" LangBash SupportedLang = "bash" LangUnknown SupportedLang = "" )
func ParseLangHint ¶
func ParseLangHint(hint string) SupportedLang
ParseLangHint maps a markdown fence language hint (e.g., "go", "py", "ts") to a SupportedLang. Unrecognized hints return LangUnknown.
func (SupportedLang) IsSupported ¶
func (l SupportedLang) IsSupported() bool
IsSupported returns true if the language has regex-based identifier detection available.
func (SupportedLang) String ¶
func (l SupportedLang) String() string
type TitleContext ¶
TitleContext identifies the harness and exact project root for title cleanup and context-aware path normalization.
type TitlePipeline ¶
type TitlePipeline struct {
// contains filtered or unexported fields
}
TitlePipeline is an immutable, concurrency-safe baseline-Standard title pipeline. Runtime redactor configuration cannot extend its fixed policy.
func NewTitlePipeline ¶
func NewTitlePipeline() (*TitlePipeline, error)
NewTitlePipeline constructs the fixed baseline-Standard title policy.
func (*TitlePipeline) Generate ¶
func (p *TitlePipeline) Generate(firstTurn string, context TitleContext) (TitleResult, error)
Generate cleans markup owned by the supplied harness, sanitizes the result, and always applies generated-title whitespace trimming and length capping.
Two results tell the caller to move on to the next user turn:
- An empty Text means the turn held no user prose once harness-owned markup was removed. The caller must not store an empty title; it tries the next user turn.
- An error means the turn is unusable because its recognized markup is unbalanced, crossed, or nested, so cleaning cannot be proven complete. The caller must not expose the raw turn text; it tries the next user turn, or falls back to a generic title.
GenerateFromTurns applies both rules for the caller.
func (*TitlePipeline) GenerateFromTurns ¶
func (p *TitlePipeline) GenerateFromTurns(turns []string, context TitleContext) (TitleResult, int, []error)
GenerateFromTurns returns the first usable generated title in turn order.
It defines the turn-selection contract once, so every caller skips the same turns: a turn that cleans to empty text carries no user prose, and a turn that fails to clean is unusable. Both are skipped, and each per-turn error is collected in skipped so a caller can log or count it. The errors never contain raw turn text.
It returns the first usable result with the index of the turn that produced it, or an empty result and index -1 when no turn is usable. A nil receiver returns index -1 and the shared nil-receiver diagnostic in skipped.
func (*TitlePipeline) Sanitize ¶
func (p *TitlePipeline) Sanitize(title string, context TitleContext) (TitleResult, error)
Sanitize never interprets harness markup. It preserves exact input bytes when no sensitive rewrite occurs and the title is within the length limit; a sensitive or over-limit title returns its canonical rewritten or capped text.
func (*TitlePipeline) SimpleTitle ¶
SimpleTitle cleans harness-owned markup and caps length WITHOUT redaction. It does NOT remove secrets, PII, paths, or user identifiers. Its output is for local, trusted display only (e.g. a first-time-setup selection list) and MUST NOT be stored or published as a transcript title. Use Generate for any published title.
It reports the same two "try the next user turn" outcomes as Generate: an empty string means the turn held no user prose once harness-owned markup was removed, and an error means the turn is unusable and its raw text must not be exposed.
type TitleResult ¶
type TitleResult struct {
Text string
Categories []CategoryString
}
TitleResult contains canonical title text and the ordered categories whose sensitive content was replaced during this call.
func (TitleResult) HasSensitiveContent ¶
func (r TitleResult) HasSensitiveContent() bool
HasSensitiveContent reports whether this call replaced sensitive content.
type TreeSitterAnonymizer ¶
type TreeSitterAnonymizer struct {
// contains filtered or unexported fields
}
TreeSitterAnonymizer replaces identifiers in code blocks with deterministic anonymous names using tree-sitter AST parsing for accurate identifier detection.
Determinism: same identifier → same anonymous name within an instance. Cross-instance determinism holds when both instances process identical code (names are assigned in alphabetical order of the identifier set, not occurrence order).
Thread safety: safe for concurrent use. Parsers are created per-call. The nameMap is protected by a mutex.
func NewTreeSitterAnonymizer ¶
func NewTreeSitterAnonymizer() *TreeSitterAnonymizer
NewTreeSitterAnonymizer returns a new TreeSitterAnonymizer. The anonymizer maintains a session-scoped identifier map for determinism.
func (*TreeSitterAnonymizer) AnonymizeCode ¶
func (a *TreeSitterAnonymizer) AnonymizeCode(lang SupportedLang, code string) (string, error)
AnonymizeCode rewrites the code string for the given language using tree-sitter AST parsing to find and replace identifiers.
If lang is unsupported or LangUnknown, returns code unchanged with nil error. Returns an error if tree-sitter parsing or query execution fails. On error, the caller should fall back to RegexAnonymizer.
type UserPattern ¶
type UserPattern struct {
ID string
Category Category
Pattern string // raw regex string — compiled by NewRedactor
Replacement string
}
UserPattern is a user-defined regex redaction rule loaded from config.yaml. Compiled and applied by NewRedactor. User patterns inherit their semantic category's default minimum; the configuration surface does not currently expose stricter per-pattern activation.
type XDGPaths ¶
type XDGPaths struct {
DataHome string // resolved $XDG_DATA_HOME (default: ~/.local/share)
ConfigHome string // resolved $XDG_CONFIG_HOME (default: ~/.config)
StateHome string // resolved $XDG_STATE_HOME (default: ~/.local/state)
}
XDGPaths holds resolved XDG base directory paths for custom path redaction. Zero values mean "not configured" — no extra rules generated. These are injected at construction time to keep the redactor testable with no OS state.