Files
CASAN/AINative_OKR_CASAN5/packages/casan-harness/scripts/bash/unicode-normalize.py
T
thanhnvandClaude Opus 4.8 664bd1f00c feat(plan-01): Phase 1 — relocate harness code to packages/casan-harness (symlink facade)
Physically move the pure-code subtrees out of .specify into the package, leaving
compat symlinks at the old .specify/<dir> paths so every existing reference (internal
CASAN_HARNESS_ROOT + external CI/docker/mjs) keeps resolving. Runtime state stays put.

Moved (git mv): scripts/ tests/ security/ templates/ config/ governance/ memory/
  .specify/<dir>  ->  packages/casan-harness/<dir>   (+ .specify/<dir> symlink)
Stays in .specify (state/governance/domain, handled later): logs/ agentops/ level5/
  init-options.json traceability-map.json

Python `.resolve()` self-location followed the compat symlink into packages and lost
the app root; generate-casan-demo-context.py, generate-agentops-dashboard.py and
dashboard-server.py now walk UP for the `.specify` state marker instead of a fixed
parent depth (fixes "missing trace files" in run-casan4).

Full gate: PASS=64 FAIL=0 SKIP=3 (CASAN_CI_STEP_TIMEOUT_SEC=1200 — track-a ~450s runs
close to the 600s default and can tip over under load; this is timing variance, not a
regression — it passed cleanly with headroom). Runtime log/audit artifacts kept unstaged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 00:06:00 +09:00

63 lines
2.6 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""CASAN H4 — Unicode confusable / obfuscation normalizer (Track A, V3).
Reads text on stdin, writes a normalized variant on stdout used ONLY for
injection/jailbreak phrase matching (never for PII/secret redaction, so we
never widen what counts as a secret).
Deterministic transforms, in order:
1. NFKC compatibility normalization — folds fullwidth (ignore) and
other compatibility forms to their ASCII equivalents.
2. Strip zero-width / BOM / soft-hyphen formatting characters that split a
word so a blocklist never sees it (i·g·n·o·r·e).
3. Fold a fixed table of common Cyrillic/Greek homoglyphs to their Latin
lookalikes (іgnоrе -> ignore). NFKC does NOT do this — confusables are a
separate Unicode concern — so the table is explicit and auditable.
The table is intentionally small and covers the lookalikes actually used in
prompt-injection homoglyph attacks; extend it as new vectors appear.
"""
import sys
import unicodedata
# Zero-width, BOM, and invisible formatting code points.
ZERO_WIDTH = {
0x200B, # zero-width space
0x200C, # zero-width non-joiner
0x200D, # zero-width joiner
0x2060, # word joiner
0xFEFF, # BOM / zero-width no-break space
0x00AD, # soft hyphen
0x180E, # Mongolian vowel separator
0x2061, 0x2062, 0x2063, 0x2064, # invisible math operators
}
# Common Cyrillic / Greek homoglyphs -> Latin lookalike. Lowercase and
# uppercase both listed because matching is case-insensitive downstream but the
# fold must run before case handling to be safe.
CONFUSABLES = {
# Cyrillic lowercase
"а": "a", "е": "e", "о": "o", "р": "p", "с": "c", "у": "y", "х": "x",
"ѕ": "s", "і": "i", "ј": "j", "ԁ": "d", "һ": "h", "ӏ": "l", "п": "n",
"г": "r", "т": "t", "к": "k", "м": "m", "в": "b",
# Cyrillic uppercase
"А": "A", "В": "B", "Е": "E", "К": "K", "М": "M", "Н": "H", "О": "O",
"Р": "P", "С": "C", "Т": "T", "У": "Y", "Х": "X", "Ѕ": "S", "І": "I",
"Ј": "J",
# Greek
"α": "a", "ε": "e", "ο": "o", "ρ": "p", "τ": "t", "ν": "v", "κ": "k",
"Α": "A", "Β": "B", "Ε": "E", "Ζ": "Z", "Η": "H", "Ι": "I", "Κ": "K",
"Μ": "M", "Ν": "N", "Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X",
}
def normalize(text: str) -> str:
text = unicodedata.normalize("NFKC", text)
text = "".join(ch for ch in text if ord(ch) not in ZERO_WIDTH)
text = "".join(CONFUSABLES.get(ch, ch) for ch in text)
return text
if __name__ == "__main__":
sys.stdout.write(normalize(sys.stdin.read()))