aegis-gateway-pack-classification 2.0.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ # Node/npm local installs
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+ .pnpm-store/
7
+
8
+ # Local environment files
9
+ .env
10
+ .env.*
11
+ !.env.example
12
+
13
+ ## jj
14
+ .jj
15
+
16
+ # Codex local configuration
17
+ .codex/
18
+ AGENTS.md
19
+
20
+ # Claude Flow runtime data
21
+ .claude-flow/data/
22
+ .claude-flow/logs/
23
+ CLAUDE.md
24
+ .claude
25
+ .claude-flow
26
+
27
+ # MCP
28
+ .mcp.json
29
+ ruvector.db
30
+
31
+ # ruflo agents
32
+ .agents
33
+ .swarm
34
+ .agentdb
35
+ agentdb.db
36
+
37
+ # Environment variables
38
+ .env
39
+ .env.local
40
+ .env.*.local
41
+
42
+ # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
43
+ # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
44
+
45
+ ### VisualStudioCode ###
46
+ .vscode/*
47
+ !.vscode/settings.json
48
+ !.vscode/tasks.json
49
+ !.vscode/launch.json
50
+ !.vscode/extensions.json
51
+ !.vscode/*.code-snippets
52
+ .vscode
53
+
54
+ # Local History for Visual Studio Code
55
+ .history/
56
+
57
+ # Built Visual Studio Code Extensions
58
+ *.vsix
59
+
60
+ ### VisualStudioCode Patch ###
61
+ # Ignore all local history of files
62
+ .history
63
+ .ionide
64
+
65
+ ### Misc files
66
+ gemma4
67
+ .postman/
68
+ .openclaude/
69
+ postman/
70
+
71
+ ### OpenClaude
72
+ .openclaude-profile.json
73
+
74
+ # Python
75
+ __pycache__/
76
+ *.py[cod]
77
+ *.egg-info/
78
+ .pytest_cache/
79
+ dist/
80
+ build/
81
+ *.egg
82
+ .venv/
83
+ .uv-cache/
84
+ .tmp/
85
+ .ruff_cache/
86
+
87
+ # Docker
88
+ .docker/
89
+
90
+ ### Build plans
91
+ BUILD_PLAN.md
92
+ CLAUDE.md
93
+ PROJECT_SPEC.md
94
+ AMEND_PLAN.md
95
+ SOURCE_OF_TRUTH.md
96
+ repo-seed/
97
+ .import_linter_cache/
98
+ site/
99
+
100
+ # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: aegis-gateway-pack-classification
3
+ Version: 2.0.0a0
4
+ Summary: Aegis v2 — content classification policy pack (regex labeler).
5
+ Project-URL: Homepage, https://github.com/e-choness/aegis
6
+ Project-URL: Repository, https://github.com/e-choness/aegis
7
+ Project-URL: Issues, https://github.com/e-choness/aegis/issues
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: aegis-gateway-core
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "aegis-gateway-pack-classification"
3
+ version = "2.0.0a0"
4
+ description = "Aegis v2 — content classification policy pack (regex labeler)."
5
+ license = { text = "MIT" }
6
+ requires-python = ">=3.12"
7
+
8
+ dependencies = ["aegis-gateway-core"]
9
+
10
+ [project.urls]
11
+ Homepage = "https://github.com/e-choness/aegis"
12
+ Repository = "https://github.com/e-choness/aegis"
13
+ Issues = "https://github.com/e-choness/aegis/issues"
14
+
15
+ [tool.uv.sources]
16
+ aegis-gateway-core = { workspace = true }
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/aegis_pack_classification"]
24
+
25
+ [project.entry-points."aegis.nodes"]
26
+ classification = "aegis_pack_classification:ClassificationNode"
@@ -0,0 +1,5 @@
1
+ """aegis-pack-classification — regex content labeler."""
2
+
3
+ from aegis_pack_classification.node import ClassificationNode
4
+
5
+ __all__ = ["ClassificationNode"]
@@ -0,0 +1,67 @@
1
+ """ClassificationNode — regex-based content labeler writing labels.classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Sequence
7
+
8
+ from aegis_core.pipeline.state import RunState, RunStateDelta
9
+
10
+ _DEFAULT_PATTERNS: list[tuple[str, str]] = [
11
+ # (label, pattern)
12
+ ("pii", r"\b[\w.+-]+@[\w-]+\.[a-z]{2,}\b"), # email
13
+ ("pii", r"\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b"), # US phone
14
+ ("financial", r"\b\d{4}[\s-]\d{4}[\s-]\d{4}[\s-]\d{4}\b"), # credit card
15
+ ("secret", r"(?i)\b(api[_-]?key|password|secret|token)\s*[=:]\s*\S+"),
16
+ ("medical", r"(?i)\b(diagnosis|prescription|patient|hipaa)\b"),
17
+ ("legal", r"(?i)\b(attorney.client|privileged|confidential)\b"),
18
+ ("public", r".*"), # catch-all fallback
19
+ ]
20
+
21
+
22
+ class ClassificationNode:
23
+ """Pipeline node that classifies the last user message via regex rules.
24
+
25
+ Writes the matched label into ``state.labels["classification"]``.
26
+ If no pattern matches (impossible with the default catch-all, but possible
27
+ with a custom ``patterns`` list), the label is left unchanged.
28
+
29
+ Args:
30
+ patterns: Ordered list of ``(label, regex_pattern)`` pairs. First
31
+ match wins. Defaults to a built-in set covering PII, financial,
32
+ secrets, medical, legal, and public content.
33
+ name: Node name.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ patterns: Sequence[tuple[str, str]] = _DEFAULT_PATTERNS,
39
+ name: str = "classification",
40
+ ) -> None:
41
+ self.name = name
42
+ self._rules: list[tuple[str, re.Pattern[str]]] = [
43
+ (label, re.compile(pattern)) for label, pattern in patterns
44
+ ]
45
+
46
+ async def run(self, state: RunState) -> RunStateDelta:
47
+ """Classify the last user message and return a labels delta."""
48
+ text: str | None = None
49
+ for msg in reversed(state.messages):
50
+ if msg.role == "user":
51
+ text = msg.content
52
+ break
53
+
54
+ if text is None:
55
+ return RunStateDelta()
56
+
57
+ label = self._classify(text)
58
+ if label is None:
59
+ return RunStateDelta()
60
+
61
+ return RunStateDelta(labels={"classification": label})
62
+
63
+ def _classify(self, text: str) -> str | None:
64
+ for label, pattern in self._rules:
65
+ if pattern.search(text):
66
+ return label
67
+ return None
@@ -0,0 +1,125 @@
1
+ """Tests for aegis-pack-classification.
2
+
3
+ Gate: DC uv run pytest packages/aegis-pack-classification -q
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from aegis_pack_classification import ClassificationNode
9
+
10
+ from aegis_core.pipeline.state import RunState
11
+ from aegis_core.providers.models import Message
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Helpers
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ def _state(*contents: str, role: str = "user") -> RunState:
19
+ return RunState(
20
+ run_id="test",
21
+ route="default",
22
+ messages=[Message(role=role, content=c) for c in contents],
23
+ )
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # ClassificationNode — basic labeling
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ class TestClassificationNode:
32
+ async def test_classifies_email_as_pii(self) -> None:
33
+ node = ClassificationNode()
34
+ delta = await node.run(_state("My email is alice@example.com"))
35
+ assert delta.labels is not None
36
+ assert delta.labels["classification"] == "pii"
37
+
38
+ async def test_classifies_phone_as_pii(self) -> None:
39
+ node = ClassificationNode()
40
+ delta = await node.run(_state("Call me at 555-867-5309"))
41
+ assert delta.labels is not None
42
+ assert delta.labels["classification"] == "pii"
43
+
44
+ async def test_classifies_credit_card_as_financial(self) -> None:
45
+ node = ClassificationNode()
46
+ delta = await node.run(_state("Card: 4111 1111 1111 1111"))
47
+ assert delta.labels is not None
48
+ assert delta.labels["classification"] == "financial"
49
+
50
+ async def test_classifies_api_key_as_secret(self) -> None:
51
+ node = ClassificationNode()
52
+ delta = await node.run(_state("api_key=sk-abc123"))
53
+ assert delta.labels is not None
54
+ assert delta.labels["classification"] == "secret"
55
+
56
+ async def test_classifies_password_as_secret(self) -> None:
57
+ node = ClassificationNode()
58
+ delta = await node.run(_state("password: hunter2"))
59
+ assert delta.labels is not None
60
+ assert delta.labels["classification"] == "secret"
61
+
62
+ async def test_classifies_medical(self) -> None:
63
+ node = ClassificationNode()
64
+ delta = await node.run(_state("Patient diagnosis: hypertension"))
65
+ assert delta.labels is not None
66
+ assert delta.labels["classification"] == "medical"
67
+
68
+ async def test_classifies_legal(self) -> None:
69
+ node = ClassificationNode()
70
+ delta = await node.run(_state("This is attorney-client privileged"))
71
+ assert delta.labels is not None
72
+ assert delta.labels["classification"] == "legal"
73
+
74
+ async def test_classifies_plain_text_as_public(self) -> None:
75
+ node = ClassificationNode()
76
+ delta = await node.run(_state("What is the weather today?"))
77
+ assert delta.labels is not None
78
+ assert delta.labels["classification"] == "public"
79
+
80
+ async def test_uses_last_user_message(self) -> None:
81
+ node = ClassificationNode()
82
+ state = RunState(
83
+ run_id="r",
84
+ route="d",
85
+ messages=[
86
+ Message(role="user", content="What is the weather?"),
87
+ Message(role="assistant", content="It is sunny."),
88
+ Message(role="user", content="My email is bob@test.com"),
89
+ ],
90
+ )
91
+ delta = await node.run(state)
92
+ assert delta.labels is not None
93
+ assert delta.labels["classification"] == "pii"
94
+
95
+ async def test_no_user_message_returns_empty_delta(self) -> None:
96
+ node = ClassificationNode()
97
+ state = RunState(
98
+ run_id="r",
99
+ route="d",
100
+ messages=[Message(role="assistant", content="hello")],
101
+ )
102
+ delta = await node.run(state)
103
+ assert delta.labels is None
104
+
105
+ async def test_custom_patterns_first_match_wins(self) -> None:
106
+ node = ClassificationNode(patterns=[
107
+ ("top_secret", r"classified"),
108
+ ("public", r".*"),
109
+ ])
110
+ delta = await node.run(_state("This document is classified"))
111
+ assert delta.labels is not None
112
+ assert delta.labels["classification"] == "top_secret"
113
+
114
+ async def test_no_match_with_empty_patterns(self) -> None:
115
+ node = ClassificationNode(patterns=[])
116
+ delta = await node.run(_state("anything"))
117
+ assert delta.labels is None
118
+
119
+ def test_node_name_default(self) -> None:
120
+ node = ClassificationNode()
121
+ assert node.name == "classification"
122
+
123
+ def test_node_name_custom(self) -> None:
124
+ node = ClassificationNode(name="my_classifier")
125
+ assert node.name == "my_classifier"