Skip to content

Commit 046eb79

Browse files
committed
adding feature to sdk
1 parent 087d4fb commit 046eb79

4 files changed

Lines changed: 75 additions & 1 deletion

File tree

‎node9/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ def configure(*, agent_name: str = "", policy: str = "") -> None:
1616
Thread-safe — safe to call from concurrent async frameworks (LangGraph, FastMCP).
1717
1818
policy values: "audit" | "require_approval" | "block_on_rules" | "" (SaaS default)
19+
20+
Configuration precedence (highest wins):
21+
1. configure() called at runtime — highest priority
22+
2. Node9Agent.agent_name / .policy class attributes
23+
3. NODE9_AGENT_NAME / NODE9_AGENT_POLICY env vars — baseline at import time
24+
25+
Calling configure() after the first tool call is allowed but not recommended —
26+
in-flight calls will have already used the previous identity.
1927
"""
2028
_config.set_identity(agent_name=agent_name, policy=policy)
2129

‎node9/_agent.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,11 @@ def internal(fn: Callable) -> Callable:
134134
Marks a Node9Agent method as infrastructure (git plumbing, workspace setup).
135135
136136
- Never calls evaluate() — no SaaS call, no blocking, no DLP scan
137-
- Logs to stdout: [node9 internal] method_name(args)
137+
- Logs to stderr: [node9 internal] method_name(args)
138138
- Use only for non-agent-decision code (git, workspace setup, file plumbing).
139139
Do NOT use to bypass governance on agent-controlled actions.
140+
- NOT reachable via dispatch() — the LLM cannot invoke @internal methods
141+
through the tool-call interface. dispatch() only routes to @tool methods.
140142
141143
WARNING: @internal skips all governance. By convention, @internal methods
142144
should have names starting with '_' to make the bypass visible at call sites.

‎tests/test_agent.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,21 @@ def test_internal_logs_to_stderr(self, tmp_path, capsys):
215215
assert "_git_push" in captured.err
216216
assert "internal" not in captured.out # must NOT appear on stdout
217217

218+
def test_internal_not_reachable_via_dispatch(self, tmp_path):
219+
"""LLM cannot invoke @internal methods through dispatch() — they are invisible to the tool router."""
220+
agent = SimpleAgent(workspace=str(tmp_path))
221+
result = agent.dispatch("_git_push", {"branch": "main"})
222+
assert "Unknown tool" in result, (
223+
"@internal method '_git_push' should not be reachable via dispatch()"
224+
)
225+
226+
def test_internal_not_reachable_via_dispatch_by_function_name(self, tmp_path):
227+
"""dispatch() ignores @internal even if called by the underlying function name."""
228+
agent = SimpleAgent(workspace=str(tmp_path))
229+
# SimpleAgent._git_push is @internal — must not be callable via dispatch
230+
result = agent.dispatch("_git_push", {})
231+
assert "Unknown tool" in result
232+
218233
def test_internal_on_public_method_warns(self):
219234
"""@internal on a public method (no leading _) emits RuntimeWarning."""
220235
import warnings

‎tests/test_dlp.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,55 @@ def test_content_within_100k_is_scanned(self):
9898
assert result is not None
9999

100100

101+
class TestRealPatternMatching:
102+
"""Verify the actual DLP regexes match the credential formats they target.
103+
104+
Strings are constructed from fragments joined at runtime so no complete
105+
credential pattern appears as a literal in this source file — which would
106+
trigger node9's own DLP hook when writing or committing this file.
107+
"""
108+
109+
def test_aws_key_in_content_is_detected(self):
110+
# AWS Access Key ID: AKIA + 16 uppercase alphanumeric chars
111+
# Split to avoid a complete match in source text
112+
prefix = "AKI" + "A" # "AKIA" in source is never complete
113+
suffix = "X" * 8 + "0" * 8 # 16-char body, clearly synthetic
114+
fake_key = prefix + suffix
115+
result = dlp_scan("config.txt", f"aws_key={fake_key}")
116+
assert result is not None
117+
assert "AWS" in result
118+
119+
def test_github_token_in_content_is_detected(self):
120+
# GitHub token: ghp_ + 36 alphanumeric
121+
fake_token = "gh" + "p_" + "A" * 36
122+
result = dlp_scan("env.txt", f"GH_TOKEN={fake_token}")
123+
assert result is not None
124+
assert "GitHub" in result
125+
126+
def test_pem_key_in_content_is_detected(self):
127+
# PEM private key header — split across fragments so it never appears whole
128+
pem_begin = "-----BEGIN " + "RSA "
129+
pem_end = "PRIVATE KEY-----"
130+
result = dlp_scan("key.txt", pem_begin + pem_end)
131+
assert result is not None
132+
assert "Private Key" in result
133+
134+
def test_clean_content_with_normal_path_passes(self):
135+
result = dlp_scan("output.txt", "def hello():\n return 42\n")
136+
assert result is None
137+
138+
def test_sensitive_path_detected_regardless_of_content(self):
139+
result = dlp_scan("/home/user/.ssh/" + "id_rsa", "normal content")
140+
assert result is not None
141+
142+
def test_both_path_and_content_triggers(self):
143+
# Both path and content are bad — path check runs first, result is non-None
144+
prefix = "AKI" + "A"
145+
fake_key = prefix + "X" * 8 + "0" * 8
146+
result = dlp_scan("/home/user/.ssh/" + "id_rsa", fake_key)
147+
assert result is not None
148+
149+
101150
class TestSensitivePathDetection:
102151
"""Test sensitive file path blocking — no credential content needed."""
103152

0 commit comments

Comments
 (0)