mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-13 23:42:25 -06:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52d59cf7b7 | |||
| 2dc885ab4d | |||
| 14488f43e0 |
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "turnstone"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
description = "Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."
|
||||
readme = "README.md"
|
||||
license = "BUSL-1.1"
|
||||
|
||||
@@ -6,6 +6,7 @@ from turnstone.core.metacognition import (
|
||||
NUDGE_DENIAL,
|
||||
NUDGE_RESUME,
|
||||
NUDGE_START,
|
||||
NUDGE_TOOL_ERROR,
|
||||
detect_completion,
|
||||
detect_correction,
|
||||
format_nudge,
|
||||
@@ -263,5 +264,27 @@ class TestFormatNudge:
|
||||
def test_start(self):
|
||||
assert format_nudge("start") == NUDGE_START
|
||||
|
||||
def test_tool_error(self):
|
||||
assert format_nudge("tool_error") == NUDGE_TOOL_ERROR
|
||||
|
||||
def test_invalid(self):
|
||||
assert format_nudge("invalid") == ""
|
||||
|
||||
|
||||
class TestToolErrorNudge:
|
||||
def test_fires(self):
|
||||
state: dict[str, float] = {}
|
||||
assert should_nudge("tool_error", state, message_count=5, memory_count=3) is True
|
||||
|
||||
def test_cooldown(self):
|
||||
state: dict[str, float] = {}
|
||||
assert should_nudge("tool_error", state, message_count=5, memory_count=3) is True
|
||||
assert should_nudge("tool_error", state, message_count=6, memory_count=3) is False
|
||||
|
||||
def test_not_on_first_message(self):
|
||||
state: dict[str, float] = {}
|
||||
assert should_nudge("tool_error", state, message_count=1, memory_count=3) is False
|
||||
|
||||
def test_not_with_zero_memories(self):
|
||||
state: dict[str, float] = {}
|
||||
assert should_nudge("tool_error", state, message_count=5, memory_count=0) is False
|
||||
|
||||
@@ -184,6 +184,17 @@ class TestEnvSecretFalsePositives:
|
||||
r = evaluate_output("APP_NAME=myapp\nSECRET_KEY=abc123\nAPI_TOKEN=xyz789\nDEBUG=true")
|
||||
assert "env_file_leak" in r.flags
|
||||
|
||||
def test_single_secret_env_line(self) -> None:
|
||||
"""A single AWS_SECRET_ACCESS_KEY=... line should trigger."""
|
||||
r = evaluate_output("AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
|
||||
assert "env_file_leak" in r.flags
|
||||
assert r.risk_level == "high"
|
||||
|
||||
def test_substring_key_no_false_positive(self) -> None:
|
||||
"""MONKEY=banana should not trigger (KEY is a substring, not a segment)."""
|
||||
r = evaluate_output("MONKEY=banana\nTURKEY=gobble\nDONKEY=hee-haw")
|
||||
assert "env_file_leak" not in r.flags
|
||||
|
||||
|
||||
class TestOutputAssessment:
|
||||
"""Verify OutputAssessment structure."""
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""turnstone - Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."""
|
||||
|
||||
__version__ = "0.8.1"
|
||||
__version__ = "0.8.2"
|
||||
|
||||
@@ -44,12 +44,19 @@ NUDGE_START = (
|
||||
"user's request to find applicable context, preferences, or guidance."
|
||||
)
|
||||
|
||||
NUDGE_TOOL_ERROR = (
|
||||
"A tool just returned an error. Before retrying, check your memories — "
|
||||
"the user may have given feedback about this tool or error pattern in a "
|
||||
"previous session. Use memory(action='search') to find relevant guidance."
|
||||
)
|
||||
|
||||
_NUDGE_MAP: dict[str, str] = {
|
||||
"correction": NUDGE_CORRECTION,
|
||||
"denial": NUDGE_DENIAL,
|
||||
"resume": NUDGE_RESUME,
|
||||
"completion": NUDGE_COMPLETION,
|
||||
"start": NUDGE_START,
|
||||
"tool_error": NUDGE_TOOL_ERROR,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -153,6 +160,9 @@ def should_nudge(
|
||||
# Start nudge only on first message
|
||||
if nudge_type == "start" and message_count != 1:
|
||||
return False
|
||||
# Tool error nudge only if there are memories to search
|
||||
if nudge_type == "tool_error" and memory_count == 0:
|
||||
return False
|
||||
# Resume/start nudge only if there are memories to recall
|
||||
if nudge_type in ("resume", "start") and memory_count == 0:
|
||||
return False
|
||||
|
||||
@@ -54,7 +54,10 @@ _RE_CONNECTION_STRING = re.compile(
|
||||
r"(?:postgresql|mysql|mongodb|redis|amqp)://[^:@\s]+:[^@\s]+@",
|
||||
)
|
||||
_RE_ENV_SECRET_LINE = re.compile(r"[A-Z][A-Z_0-9]+=\S+")
|
||||
_RE_ENV_SECRET_KEY = re.compile(r"SECRET|KEY|TOKEN|PASSWORD|CREDENTIAL", re.IGNORECASE)
|
||||
_RE_ENV_SECRET_KEY = re.compile(
|
||||
r"(?:^|_)(?:SECRET|TOKEN|PASSWORD|CREDENTIAL)(?:_|$)|(?:^|_)KEY(?:_|$)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# (pattern, redact_label) — ordered most-specific first for redaction.
|
||||
_CREDENTIAL_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
||||
@@ -218,9 +221,7 @@ def _check_credentials(
|
||||
risk = "high"
|
||||
|
||||
env_lines = _RE_ENV_SECRET_LINE.findall(text)
|
||||
if len(env_lines) >= 3 and any(
|
||||
_RE_ENV_SECRET_KEY.search(ln.split("=", 1)[0]) for ln in env_lines
|
||||
):
|
||||
if any(_RE_ENV_SECRET_KEY.search(ln.split("=", 1)[0]) for ln in env_lines):
|
||||
_add_flag(flags, "credential_leak")
|
||||
flags.append("env_file_leak")
|
||||
ann.append("Output contains .env-style assignments with secret-bearing keys.")
|
||||
|
||||
@@ -1212,6 +1212,29 @@ class ChatSession:
|
||||
_tname,
|
||||
tool_call_id=tc_id,
|
||||
)
|
||||
# Metacognitive nudge: check memories on tool error
|
||||
if (
|
||||
self._memory_config.nudges
|
||||
and any(
|
||||
isinstance(out, str)
|
||||
and (
|
||||
out.startswith("Error")
|
||||
or " error: " in out[:50]
|
||||
or out.startswith("Command timed out")
|
||||
or out.startswith("Unknown tool:")
|
||||
)
|
||||
for _, out in results
|
||||
)
|
||||
and should_nudge(
|
||||
"tool_error",
|
||||
self._metacog_state,
|
||||
message_count=len(self.messages),
|
||||
memory_count=self._visible_memory_count(),
|
||||
cooldown_secs=self._memory_config.nudge_cooldown,
|
||||
)
|
||||
):
|
||||
self._pending_nudge.append(format_nudge("tool_error"))
|
||||
self._init_system_messages()
|
||||
# Inject user feedback from approval prompt (e.g. "y, use full path")
|
||||
if user_feedback:
|
||||
self.messages.append({"role": "user", "content": user_feedback})
|
||||
|
||||
Reference in New Issue
Block a user