clearfront 2.6.0__tar.gz → 2.7.0__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.
- {clearfront-2.6.0 → clearfront-2.7.0}/PKG-INFO +2 -2
- {clearfront-2.6.0 → clearfront-2.7.0}/README.md +1 -1
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/agent.py +54 -12
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/cli.py +12 -0
- clearfront-2.7.0/clearfront/effort.py +65 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/mcp_server.py +1 -1
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/prompts.py +70 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/repl.py +39 -2
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/web/index.html +20 -11
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/web_server.py +31 -18
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront.egg-info/PKG-INFO +2 -2
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront.egg-info/SOURCES.txt +2 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/pyproject.toml +1 -1
- clearfront-2.7.0/tests/test_effort.py +77 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_repl_first_run.py +1 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/LICENSE +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/__init__.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/brightdata.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/correlation.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/corroboration.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/exceptions.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/extractors.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/json_output.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/multi_target.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/pdf_report.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/pivot.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/regexes.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/serp.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/session_history.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/sponsors.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tool_cache.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/__init__.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/data/NOTICE +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/data/wmn-data-unique.json +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/exceptions.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/generate_dorks.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/scrape_url.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_abuseipdb.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_breach.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_censys.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_crt.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_crypto.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_dns.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_domain.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_dorks_live.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_email.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_emailrep.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_exif.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_exposure.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_footprint.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_github.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_gravatar.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_greynoise.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_harvester.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_hudsonrock.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_ip.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_ip2location.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_maigret.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_paste.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_phone.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_shodan.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_username.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_virustotal.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_wayback.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/search_whois.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/tools/whatsmyname.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront/utils.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront.egg-info/dependency_links.txt +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront.egg-info/entry_points.txt +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront.egg-info/requires.txt +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/clearfront.egg-info/top_level.txt +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/setup.cfg +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_authorized_use.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_brightdata.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_cli_openai.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_cli_output.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_corroboration.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_crt.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_dns.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_exif.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_footprint.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_github_code_search.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_graph_export.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_hudsonrock.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_json_export.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_mcp_authorized_use.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_openai_agent.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_pivot_freetier.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_regexes.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_report_export.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_security.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_serp.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_tool_cache.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_tool_parity.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_tools.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_v2140.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_v240.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_wayback_greynoise.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_web_server.py +0 -0
- {clearfront-2.6.0 → clearfront-2.7.0}/tests/test_web_server_openai.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clearfront
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: AI-powered OSINT agent, MCP server, and CLI. Interactive REPL + 30 tools. Anthropic Claude or local Ollama. For authorized security research use only.
|
|
5
5
|
Author: Scott Martin Anderson
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,7 +81,7 @@ Dynamic: license-file
|
|
|
81
81
|
[](https://www.python.org/)
|
|
82
82
|
[](LICENSE)
|
|
83
83
|
[](https://modelcontextprotocol.io/)
|
|
84
|
-
[](CHANGELOG.md)
|
|
85
85
|
|
|
86
86
|
**[clearfront.sh](https://clearfront.sh)** · [Disclaimer](./DISCLAIMER.md)
|
|
87
87
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.python.org/)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://modelcontextprotocol.io/)
|
|
13
|
-
[](CHANGELOG.md)
|
|
14
14
|
|
|
15
15
|
**[clearfront.sh](https://clearfront.sh)** · [Disclaimer](./DISCLAIMER.md)
|
|
16
16
|
|
|
@@ -57,7 +57,8 @@ from clearfront.tools.search_wayback import run_wayback_osint
|
|
|
57
57
|
from clearfront.tools.search_greynoise import run_greynoise_osint
|
|
58
58
|
from clearfront.tools.search_hudsonrock import run_hudsonrock_osint
|
|
59
59
|
from clearfront.pivot import investigate_graph_for_agent
|
|
60
|
-
from clearfront.prompts import
|
|
60
|
+
from clearfront.prompts import system_prompt_for_effort
|
|
61
|
+
from clearfront import effort as _effort
|
|
61
62
|
|
|
62
63
|
logger = logging.getLogger(__name__)
|
|
63
64
|
|
|
@@ -787,17 +788,23 @@ class OISAgent:
|
|
|
787
788
|
self,
|
|
788
789
|
api_key: str | None = None,
|
|
789
790
|
model: str = "claude-sonnet-4-20250514",
|
|
791
|
+
effort: str = _effort.DEFAULT,
|
|
790
792
|
) -> None:
|
|
791
793
|
self.client = anthropic.AsyncAnthropic(
|
|
792
794
|
api_key=api_key or os.environ.get("ANTHROPIC_API_KEY", "")
|
|
793
795
|
)
|
|
794
796
|
self.model = model
|
|
797
|
+
self.effort = _effort.normalize(effort)
|
|
795
798
|
self.history: list[dict[str, Any]] = []
|
|
796
799
|
|
|
797
800
|
def clear_history(self) -> None:
|
|
798
801
|
"""Reset conversation memory."""
|
|
799
802
|
self.history = []
|
|
800
803
|
|
|
804
|
+
def set_effort(self, effort: str) -> None:
|
|
805
|
+
"""Set the sweep effort for subsequent investigations."""
|
|
806
|
+
self.effort = _effort.normalize(effort)
|
|
807
|
+
|
|
801
808
|
async def run(
|
|
802
809
|
self,
|
|
803
810
|
prompt: str,
|
|
@@ -825,8 +832,17 @@ class OISAgent:
|
|
|
825
832
|
tool_calls=[],
|
|
826
833
|
on_tool_call=on_tool_call,
|
|
827
834
|
)
|
|
835
|
+
# Sweep effort shapes the system prompt (enrichment instruction) and caps
|
|
836
|
+
# the number of tool rounds. When the cap is reached we do not abort; we
|
|
837
|
+
# take one more turn with tools disabled so the model writes its report
|
|
838
|
+
# from the evidence gathered so far.
|
|
839
|
+
system_text = system_prompt_for_effort(self.effort)
|
|
840
|
+
max_rounds = _effort.rounds(self.effort)
|
|
841
|
+
tool_rounds = 0
|
|
828
842
|
try:
|
|
829
843
|
while True:
|
|
844
|
+
tool_rounds += 1
|
|
845
|
+
force_final = tool_rounds > max_rounds
|
|
830
846
|
# Prompt caching: a breakpoint on the (single) system block caches the
|
|
831
847
|
# system prompt + all tool definitions together (tools render before
|
|
832
848
|
# system), so that large stable prefix is re-billed at ~0.1x on every
|
|
@@ -840,11 +856,12 @@ class OISAgent:
|
|
|
840
856
|
system=[
|
|
841
857
|
{
|
|
842
858
|
"type": "text",
|
|
843
|
-
"text":
|
|
859
|
+
"text": system_text,
|
|
844
860
|
"cache_control": {"type": "ephemeral"},
|
|
845
861
|
}
|
|
846
862
|
],
|
|
847
863
|
tools=TOOL_DEFINITIONS, # type: ignore[arg-type]
|
|
864
|
+
tool_choice={"type": "none"} if force_final else {"type": "auto"},
|
|
848
865
|
messages=ctx.messages, # type: ignore[arg-type]
|
|
849
866
|
cache_control={"type": "ephemeral"},
|
|
850
867
|
)
|
|
@@ -911,15 +928,21 @@ class OllamaAgent:
|
|
|
911
928
|
self,
|
|
912
929
|
model: str = "llama3.2",
|
|
913
930
|
host: str = "http://localhost:11434",
|
|
931
|
+
effort: str = _effort.DEFAULT,
|
|
914
932
|
) -> None:
|
|
915
933
|
self.model = model
|
|
916
934
|
self.host = host
|
|
935
|
+
self.effort = _effort.normalize(effort)
|
|
917
936
|
self.history: list[dict[str, Any]] = []
|
|
918
937
|
|
|
919
938
|
def clear_history(self) -> None:
|
|
920
939
|
"""Reset conversation memory."""
|
|
921
940
|
self.history = []
|
|
922
941
|
|
|
942
|
+
def set_effort(self, effort: str) -> None:
|
|
943
|
+
"""Set the sweep effort for subsequent investigations."""
|
|
944
|
+
self.effort = _effort.normalize(effort)
|
|
945
|
+
|
|
923
946
|
async def run(
|
|
924
947
|
self,
|
|
925
948
|
prompt: str,
|
|
@@ -962,21 +985,27 @@ class OllamaAgent:
|
|
|
962
985
|
|
|
963
986
|
self.history.append({"role": "user", "content": prompt})
|
|
964
987
|
messages: list[Any] = [
|
|
965
|
-
{"role": "system", "content":
|
|
988
|
+
{"role": "system", "content": system_prompt_for_effort(self.effort)},
|
|
966
989
|
*self.history,
|
|
967
990
|
]
|
|
968
991
|
ctx = _AgentRunContext(messages=messages, tool_calls=[], on_tool_call=on_tool_call)
|
|
969
992
|
|
|
993
|
+
# Sweep effort caps the tool rounds. Ollama has no reliable tool_choice
|
|
994
|
+
# switch, so on the final turn we call without tools, which forces a
|
|
995
|
+
# text answer from the evidence gathered so far.
|
|
996
|
+
max_rounds = _effort.rounds(self.effort)
|
|
997
|
+
tool_rounds = 0
|
|
970
998
|
try:
|
|
971
999
|
client = ollama.AsyncClient(host=self.host)
|
|
972
1000
|
while True:
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1001
|
+
tool_rounds += 1
|
|
1002
|
+
force_final = tool_rounds > max_rounds
|
|
1003
|
+
chat_kwargs: dict[str, Any] = {"model": self.model, "messages": ctx.messages}
|
|
1004
|
+
if not force_final:
|
|
1005
|
+
chat_kwargs["tools"] = _OLLAMA_TOOLS
|
|
1006
|
+
response = await client.chat(**chat_kwargs)
|
|
978
1007
|
msg = response.message
|
|
979
|
-
if not msg.tool_calls:
|
|
1008
|
+
if force_final or not msg.tool_calls:
|
|
980
1009
|
text = msg.content or ""
|
|
981
1010
|
self.history.append({"role": "assistant", "content": text})
|
|
982
1011
|
return AgentResponse(content=text, tool_calls=ctx.tool_calls)
|
|
@@ -1079,17 +1108,23 @@ class OpenAICompatibleAgent:
|
|
|
1079
1108
|
model: str = "gpt-4o-mini",
|
|
1080
1109
|
base_url: str = "http://localhost:8080/v1",
|
|
1081
1110
|
api_key: str | None = None,
|
|
1111
|
+
effort: str = _effort.DEFAULT,
|
|
1082
1112
|
) -> None:
|
|
1083
1113
|
self.model = model
|
|
1084
1114
|
self.base_url = base_url
|
|
1085
1115
|
# Many local servers ignore the key, but the SDK requires a non-empty string.
|
|
1086
1116
|
self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "") or "sk-no-key-required"
|
|
1117
|
+
self.effort = _effort.normalize(effort)
|
|
1087
1118
|
self.history: list[dict[str, Any]] = []
|
|
1088
1119
|
|
|
1089
1120
|
def clear_history(self) -> None:
|
|
1090
1121
|
"""Reset conversation memory."""
|
|
1091
1122
|
self.history = []
|
|
1092
1123
|
|
|
1124
|
+
def set_effort(self, effort: str) -> None:
|
|
1125
|
+
"""Set the sweep effort for subsequent investigations."""
|
|
1126
|
+
self.effort = _effort.normalize(effort)
|
|
1127
|
+
|
|
1093
1128
|
async def run(
|
|
1094
1129
|
self,
|
|
1095
1130
|
prompt: str,
|
|
@@ -1124,19 +1159,26 @@ class OpenAICompatibleAgent:
|
|
|
1124
1159
|
|
|
1125
1160
|
self.history.append({"role": "user", "content": prompt})
|
|
1126
1161
|
messages: list[Any] = [
|
|
1127
|
-
{"role": "system", "content":
|
|
1162
|
+
{"role": "system", "content": system_prompt_for_effort(self.effort)},
|
|
1128
1163
|
*self.history,
|
|
1129
1164
|
]
|
|
1130
1165
|
ctx = _AgentRunContext(messages=messages, tool_calls=[], on_tool_call=on_tool_call)
|
|
1131
1166
|
|
|
1167
|
+
# Sweep effort caps the tool rounds. On the final turn we set
|
|
1168
|
+
# tool_choice="none" so the model writes its report instead of calling
|
|
1169
|
+
# another tool.
|
|
1170
|
+
max_rounds = _effort.rounds(self.effort)
|
|
1171
|
+
tool_rounds = 0
|
|
1132
1172
|
try:
|
|
1133
1173
|
client = openai.AsyncOpenAI(base_url=self.base_url, api_key=self.api_key)
|
|
1134
1174
|
while True:
|
|
1175
|
+
tool_rounds += 1
|
|
1176
|
+
force_final = tool_rounds > max_rounds
|
|
1135
1177
|
response = await client.chat.completions.create(
|
|
1136
1178
|
model=self.model,
|
|
1137
1179
|
messages=ctx.messages,
|
|
1138
1180
|
tools=_OPENAI_TOOLS,
|
|
1139
|
-
tool_choice="auto",
|
|
1181
|
+
tool_choice="none" if force_final else "auto",
|
|
1140
1182
|
max_tokens=_MAX_TOKENS,
|
|
1141
1183
|
)
|
|
1142
1184
|
if not response.choices:
|
|
@@ -1148,7 +1190,7 @@ class OpenAICompatibleAgent:
|
|
|
1148
1190
|
),
|
|
1149
1191
|
)
|
|
1150
1192
|
msg = response.choices[0].message
|
|
1151
|
-
if not msg.tool_calls:
|
|
1193
|
+
if force_final or not msg.tool_calls:
|
|
1152
1194
|
text = msg.content or ""
|
|
1153
1195
|
self.history.append({"role": "assistant", "content": text})
|
|
1154
1196
|
return AgentResponse(content=text, tool_calls=ctx.tool_calls)
|
|
@@ -213,6 +213,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
213
213
|
dest="is_pdf_disabled",
|
|
214
214
|
help="Disable automatic PDF generation alongside Markdown reports.",
|
|
215
215
|
)
|
|
216
|
+
parser.add_argument(
|
|
217
|
+
"--effort",
|
|
218
|
+
type=str,
|
|
219
|
+
default="deeper",
|
|
220
|
+
choices=["faster", "balanced", "deeper"],
|
|
221
|
+
help=(
|
|
222
|
+
"Sweep effort for the interactive analyst (default: deeper). "
|
|
223
|
+
"faster: fewer sources, quick pass. balanced: main sources. "
|
|
224
|
+
"deeper: follows every lead. Change it live with the 'effort' command."
|
|
225
|
+
),
|
|
226
|
+
)
|
|
216
227
|
|
|
217
228
|
subparsers = parser.add_subparsers(dest="command", metavar="command")
|
|
218
229
|
|
|
@@ -1389,6 +1400,7 @@ async def _async_main() -> None:
|
|
|
1389
1400
|
openai_model=getattr(args, "openai_model", "gpt-4o-mini"),
|
|
1390
1401
|
openai_api_key=getattr(args, "openai_api_key", None),
|
|
1391
1402
|
is_pdf_disabled=getattr(args, "is_pdf_disabled", False),
|
|
1403
|
+
effort=getattr(args, "effort", "deeper"),
|
|
1392
1404
|
)
|
|
1393
1405
|
await repl.run()
|
|
1394
1406
|
return
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# clearfront/effort.py
|
|
2
|
+
"""Sweep effort: shared level metadata and the tool-round ceiling per level.
|
|
3
|
+
|
|
4
|
+
Faster, Balanced, and Deeper trade collection breadth (the number of tool
|
|
5
|
+
rounds the analyst runs) and the matching enrichment instruction, never the
|
|
6
|
+
analyst's reasoning or output quality. Deeper is the full default fan-out; its
|
|
7
|
+
ceiling stays env-tunable via ``OIS_MAX_TOOL_ROUNDS``. The lighter levels are
|
|
8
|
+
capped below it and never exceed it.
|
|
9
|
+
|
|
10
|
+
This module holds the pieces that are identical across surfaces: the level list
|
|
11
|
+
(used by the web console chip and the REPL ``effort`` command) and the per-level
|
|
12
|
+
round ceiling (used by both agent loops). The prompt wording itself is
|
|
13
|
+
surface-specific and lives with each surface: the web console keeps its own
|
|
14
|
+
graph-flavoured enrichment text in ``web_server``; the terminal agent's version
|
|
15
|
+
lives in ``prompts`` (see ``system_prompt_for_effort``).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
DEFAULT = "deeper"
|
|
23
|
+
|
|
24
|
+
# User-facing labels and descriptions. The descriptions are kept identical to
|
|
25
|
+
# the web console's sweep-effort menu (clearfront/web/index.html effortLevels) so
|
|
26
|
+
# every surface describes the feature the same way.
|
|
27
|
+
LEVELS: list[dict[str, str]] = [
|
|
28
|
+
{
|
|
29
|
+
"v": "faster",
|
|
30
|
+
"name": "Faster",
|
|
31
|
+
"desc": "Checks fewer sources for a quick sweep. Finishes fast and uses less of your API budget.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"v": "balanced",
|
|
35
|
+
"name": "Balanced",
|
|
36
|
+
"desc": "Covers the main sources. A middle ground on time and cost.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"v": "deeper",
|
|
40
|
+
"name": "Deeper",
|
|
41
|
+
"desc": "Follows every lead for the most complete map, but takes longer and uses more of your API budget.",
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
_VALUES = {level["v"] for level in LEVELS}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize(effort: str | None) -> str:
|
|
49
|
+
"""Return a valid effort value, falling back to the default for anything unknown."""
|
|
50
|
+
return effort if effort in _VALUES else DEFAULT
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def rounds(effort: str) -> int:
|
|
54
|
+
"""Tool-round ceiling for a effort level. Deeper stays env-tunable (default 12);
|
|
55
|
+
the lighter levels are capped below it and never exceed it."""
|
|
56
|
+
ceiling = int(os.environ.get("OIS_MAX_TOOL_ROUNDS", "12"))
|
|
57
|
+
return {"faster": min(4, ceiling), "balanced": min(8, ceiling)}.get(effort, ceiling)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def describe(effort: str) -> str:
|
|
61
|
+
"""One-line description for a effort value (for the REPL 'effort' command)."""
|
|
62
|
+
for level in LEVELS:
|
|
63
|
+
if level["v"] == effort:
|
|
64
|
+
return level["desc"]
|
|
65
|
+
return ""
|
|
@@ -83,6 +83,76 @@ CRITICAL RULES:
|
|
|
83
83
|
SYSTEM_PROMPT = INVESTIGATION_STRATEGY + "\n\n" + ANALYST_CORE
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Sweep effort (terminal agent): swap the enrichment instruction and, for the
|
|
88
|
+
# lighter levels, prepend a forceful collection-mode line. Deeper is the full
|
|
89
|
+
# default fan-out and returns SYSTEM_PROMPT byte-identical. The round ceiling
|
|
90
|
+
# that pairs with each level lives in clearfront/effort.py.
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
# The exact aggressive-enrichment sentence embedded in ANALYST_CORE above. Kept
|
|
94
|
+
# as a constant so the lighter levels can swap it out reliably; a test asserts
|
|
95
|
+
# it still occurs verbatim in SYSTEM_PROMPT so an edit to ANALYST_CORE cannot
|
|
96
|
+
# silently break the swap.
|
|
97
|
+
_ENRICH_DEEPER = (
|
|
98
|
+
"- Enrich aggressively before finalising. Every time a pivot surfaces (an email, domain, "
|
|
99
|
+
"company, real name, phone, or an additional handle), expand it with the applicable tools, "
|
|
100
|
+
"then expand the new entities those reveal, chaining outward until the tool budget is reached. "
|
|
101
|
+
"The more real, connected entities you surface, the stronger the report."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
AGENT_ENRICH = {
|
|
105
|
+
"deeper": _ENRICH_DEEPER,
|
|
106
|
+
"balanced": (
|
|
107
|
+
"- Enrich only the strongest pivots before finalising. When a high-value pivot surfaces "
|
|
108
|
+
"(a confirmed email, domain, real name, or primary handle), expand it once with the "
|
|
109
|
+
"applicable tools. Do not chain outward exhaustively across every lead, even if the user's "
|
|
110
|
+
"own message asks you to map everything or run until the tool budget is reached. Deliver a "
|
|
111
|
+
"solid report on the main footprint."
|
|
112
|
+
),
|
|
113
|
+
"faster": (
|
|
114
|
+
"- Do not enrich outward. This is a single focused pass: run only the highest-signal tools "
|
|
115
|
+
"for the target, and do not scrape URLs or chase company, domain, breach, or secondary "
|
|
116
|
+
"pivots, even if the user's own message asks you to map everything or run until the tool "
|
|
117
|
+
"budget is reached. Deliver a tight, accurate report from what the priority tools return, "
|
|
118
|
+
"and note that a deeper sweep is available for the full map."
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Prepended only for the lighter levels, so the instruction to run light wins
|
|
123
|
+
# over any "map everything" language in the base prompt or the user's message.
|
|
124
|
+
AGENT_MODE_LINE = {
|
|
125
|
+
"balanced": (
|
|
126
|
+
"COLLECTION MODE: BALANCED. This overrides any other instruction, including in the user's "
|
|
127
|
+
"own message, to map every reachable data point or run until the tool budget is reached. "
|
|
128
|
+
"Sweep the main sources and expand only the strongest one or two pivots. Do not chain "
|
|
129
|
+
"outward exhaustively."
|
|
130
|
+
),
|
|
131
|
+
"faster": (
|
|
132
|
+
"COLLECTION MODE: FASTER. This overrides any other instruction, including in the user's own "
|
|
133
|
+
"message, to map every reachable data point, enrich every pivot, or run until the tool "
|
|
134
|
+
"budget is reached. Run a single quick pass of only the highest-signal tools for the target "
|
|
135
|
+
"type. Do not scrape URLs and do not chase company, domain, or secondary pivots."
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def system_prompt_for_effort(effort: str) -> str:
|
|
141
|
+
"""Return the terminal agent's system prompt shaped for a sweep effort.
|
|
142
|
+
|
|
143
|
+
Deeper returns SYSTEM_PROMPT unchanged. Balanced and Faster swap the
|
|
144
|
+
enrichment sentence for the lighter instruction and prepend the matching
|
|
145
|
+
collection-mode line.
|
|
146
|
+
"""
|
|
147
|
+
if effort not in AGENT_ENRICH:
|
|
148
|
+
effort = "deeper"
|
|
149
|
+
core = SYSTEM_PROMPT
|
|
150
|
+
if effort != "deeper":
|
|
151
|
+
core = core.replace(_ENRICH_DEEPER, AGENT_ENRICH[effort])
|
|
152
|
+
preamble = AGENT_MODE_LINE.get(effort)
|
|
153
|
+
return f"{preamble}\n\n{core}" if preamble else core
|
|
154
|
+
|
|
155
|
+
|
|
86
156
|
# ---------------------------------------------------------------------------
|
|
87
157
|
# Compact variant for small local models (Ollama)
|
|
88
158
|
# ---------------------------------------------------------------------------
|
|
@@ -31,6 +31,7 @@ from rich.panel import Panel
|
|
|
31
31
|
|
|
32
32
|
from clearfront import __version__
|
|
33
33
|
from clearfront.agent import OllamaAgent, OpenAICompatibleAgent, OISAgent
|
|
34
|
+
from clearfront import effort as _effort
|
|
34
35
|
|
|
35
36
|
logger = logging.getLogger(__name__)
|
|
36
37
|
|
|
@@ -134,6 +135,7 @@ def _print_help() -> None:
|
|
|
134
135
|
"[bold]Commands:[/]",
|
|
135
136
|
"",
|
|
136
137
|
" [#00ff88]<target>[/] Investigate any target (email, username, domain, IP, name)",
|
|
138
|
+
" [#00ff88]effort[/] Set sweep effort (faster, balanced, deeper)",
|
|
137
139
|
" [#00ff88]clear[/] Clear conversation memory",
|
|
138
140
|
" [#00ff88]save[/] Save last report to reports/",
|
|
139
141
|
" [#00ff88]tools[/] List available OSINT tools",
|
|
@@ -216,6 +218,7 @@ def _print_config(
|
|
|
216
218
|
ollama_host: str,
|
|
217
219
|
is_pdf_disabled: bool,
|
|
218
220
|
openai_base_url: str = "",
|
|
221
|
+
effort: str = _effort.DEFAULT,
|
|
219
222
|
) -> None:
|
|
220
223
|
masked = ("*" * 20 + api_key[-6:]) if api_key and len(api_key) > 6 else "not set"
|
|
221
224
|
rows = [
|
|
@@ -229,6 +232,7 @@ def _print_config(
|
|
|
229
232
|
else:
|
|
230
233
|
rows.append(f"[bold]Ollama:[/] {ollama_host}")
|
|
231
234
|
rows += [
|
|
235
|
+
f"[bold]Effort:[/] {effort}",
|
|
232
236
|
"[bold]Reports:[/] ./reports/",
|
|
233
237
|
f"[bold]PDF:[/] {'disabled' if is_pdf_disabled else 'enabled'}",
|
|
234
238
|
]
|
|
@@ -276,6 +280,7 @@ class OISRepl:
|
|
|
276
280
|
openai_model: str = "gpt-4o-mini",
|
|
277
281
|
openai_api_key: str | None = None,
|
|
278
282
|
is_pdf_disabled: bool = False,
|
|
283
|
+
effort: str = _effort.DEFAULT,
|
|
279
284
|
) -> None:
|
|
280
285
|
self._api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
|
|
281
286
|
self._provider = provider
|
|
@@ -285,12 +290,14 @@ class OISRepl:
|
|
|
285
290
|
self._openai_model = openai_model
|
|
286
291
|
self._openai_api_key = openai_api_key
|
|
287
292
|
self._is_pdf_disabled = is_pdf_disabled
|
|
293
|
+
self._effort = _effort.normalize(effort)
|
|
288
294
|
|
|
289
295
|
self._agent: OISAgent | OllamaAgent | OpenAICompatibleAgent
|
|
290
296
|
if provider == "ollama":
|
|
291
297
|
self._agent = OllamaAgent(
|
|
292
298
|
model=ollama_model,
|
|
293
299
|
host=ollama_host,
|
|
300
|
+
effort=self._effort,
|
|
294
301
|
)
|
|
295
302
|
self._display_model = ollama_model
|
|
296
303
|
elif provider == "openai":
|
|
@@ -298,10 +305,11 @@ class OISRepl:
|
|
|
298
305
|
model=openai_model,
|
|
299
306
|
base_url=openai_base_url,
|
|
300
307
|
api_key=openai_api_key,
|
|
308
|
+
effort=self._effort,
|
|
301
309
|
)
|
|
302
310
|
self._display_model = openai_model
|
|
303
311
|
else:
|
|
304
|
-
self._agent = OISAgent(api_key=self._api_key)
|
|
312
|
+
self._agent = OISAgent(api_key=self._api_key, effort=self._effort)
|
|
305
313
|
self._display_model = "claude-sonnet-4-20250514"
|
|
306
314
|
|
|
307
315
|
self._last_response: str = ""
|
|
@@ -449,7 +457,7 @@ class OISRepl:
|
|
|
449
457
|
return False
|
|
450
458
|
os.environ["ANTHROPIC_API_KEY"] = key
|
|
451
459
|
self._api_key = key
|
|
452
|
-
self._agent = OISAgent(api_key=key)
|
|
460
|
+
self._agent = OISAgent(api_key=key, effort=self._effort)
|
|
453
461
|
saved = self._persist_key_to_env(key)
|
|
454
462
|
if saved:
|
|
455
463
|
console.print(f"\n [dim]Key saved to {saved} and active for this session.[/]\n")
|
|
@@ -512,9 +520,38 @@ class OISRepl:
|
|
|
512
520
|
self._ollama_host,
|
|
513
521
|
self._is_pdf_disabled,
|
|
514
522
|
self._openai_base_url,
|
|
523
|
+
self._effort,
|
|
515
524
|
)
|
|
516
525
|
continue
|
|
517
526
|
|
|
527
|
+
# 'effort' shows the current level; 'effort <level>' sets it. Only
|
|
528
|
+
# treat 'effort <arg>' as a command when <arg> is a real level, so
|
|
529
|
+
# a genuine query that happens to start with 'effort' still runs.
|
|
530
|
+
effort_parts = user_input.split(maxsplit=1)
|
|
531
|
+
if effort_parts[0].lower() == "effort" and (
|
|
532
|
+
len(effort_parts) == 1
|
|
533
|
+
or effort_parts[1].strip().lower() in {lvl["v"] for lvl in _effort.LEVELS}
|
|
534
|
+
):
|
|
535
|
+
if len(effort_parts) == 2:
|
|
536
|
+
chosen = effort_parts[1].strip().lower()
|
|
537
|
+
self._effort = chosen
|
|
538
|
+
self._agent.set_effort(chosen)
|
|
539
|
+
console.print(
|
|
540
|
+
f" [dim]Sweep effort set to[/] [#00ff88]{chosen}[/][dim]. "
|
|
541
|
+
f"{_effort.describe(chosen)}[/]\n"
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
console.print(f"\n [bold]Sweep effort:[/] [#00ff88]{self._effort}[/]")
|
|
545
|
+
for lvl in _effort.LEVELS:
|
|
546
|
+
marker = "[#00ff88]›[/]" if lvl["v"] == self._effort else " "
|
|
547
|
+
console.print(
|
|
548
|
+
f" {marker} [#00ff88]{lvl['v']:<9}[/][dim]{lvl['desc']}[/]"
|
|
549
|
+
)
|
|
550
|
+
console.print(
|
|
551
|
+
" [dim]Set with: effort faster | balanced | deeper[/]\n"
|
|
552
|
+
)
|
|
553
|
+
continue
|
|
554
|
+
|
|
518
555
|
if user_input.lower() == "save":
|
|
519
556
|
if self._last_response:
|
|
520
557
|
path = _save_report(self._last_response)
|
|
@@ -514,13 +514,13 @@
|
|
|
514
514
|
|
|
515
515
|
<!-- right: backend status readout + controls -->
|
|
516
516
|
<div class="flex items-center gap-1.5 flex-none">
|
|
517
|
-
<!--
|
|
517
|
+
<!-- effort selector: no-noun chip, opens a level menu whose footer explains the cost -->
|
|
518
518
|
<div class="relative hidden sm:block" @keydown.escape.window="depthOpen = false">
|
|
519
519
|
<button @click="depthOpen = !depthOpen"
|
|
520
520
|
:aria-expanded="depthOpen"
|
|
521
521
|
aria-haspopup="true"
|
|
522
|
-
aria-label="
|
|
523
|
-
title="
|
|
522
|
+
aria-label="Effort"
|
|
523
|
+
title="Effort: how far each investigation reaches. Trades time and API budget, never analyst quality."
|
|
524
524
|
class="flex items-center gap-1.5 font-mono text-xs px-2.5 py-1 rounded border border-app text-primary bg-transparent hover:border-strong transition-colors cursor-pointer whitespace-nowrap">
|
|
525
525
|
<span x-text="depthCurrent().name"></span>
|
|
526
526
|
<svg width="9" height="9" viewBox="0 0 10 10" fill="none" stroke="currentColor"
|
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
<div x-show="depthOpen" x-cloak
|
|
532
532
|
x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
|
533
533
|
@click.outside="depthOpen = false" @mouseleave="depthHover = null"
|
|
534
|
-
role="radiogroup" aria-label="
|
|
534
|
+
role="radiogroup" aria-label="Effort"
|
|
535
535
|
class="absolute right-0 mt-2 w-60 p-1.5 rounded-lg border border-app bg-surface z-50"
|
|
536
536
|
style="box-shadow:0 8px 24px rgba(0,0,0,0.28);">
|
|
537
537
|
<template x-for="l in depthLevels" :key="l.v">
|
|
@@ -920,7 +920,7 @@ OPEN-SOURCE INTELLIGENCE AUTHORIZED USE · v
|
|
|
920
920
|
<script>
|
|
921
921
|
document.addEventListener('alpine:init', () => {
|
|
922
922
|
Alpine.data('clearfront', () => ({
|
|
923
|
-
version: '2.
|
|
923
|
+
version: '2.7.0',
|
|
924
924
|
booting: false,
|
|
925
925
|
bootLines: [],
|
|
926
926
|
bootAnim: false,
|
|
@@ -955,8 +955,9 @@ document.addEventListener('alpine:init', () => {
|
|
|
955
955
|
setupData: {},
|
|
956
956
|
isLight: false,
|
|
957
957
|
|
|
958
|
-
//
|
|
958
|
+
// Effort: header control for how far each investigation fans out. Trades
|
|
959
959
|
// collection breadth (tool rounds), never analyst quality. Persisted in settings.
|
|
960
|
+
// Kept as "depth" in the Alpine state + /api/chat payload (internal wire name).
|
|
960
961
|
depthOpen: false,
|
|
961
962
|
depthHover: null,
|
|
962
963
|
depthLevels: [
|
|
@@ -987,13 +988,13 @@ document.addEventListener('alpine:init', () => {
|
|
|
987
988
|
svgIcon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.666 18.666 0 0 1-2.485 5.33"/>',
|
|
988
989
|
title: 'Check my identity',
|
|
989
990
|
desc: 'Map your digital footprint',
|
|
990
|
-
prompt: 'Check my own identity exposure. This is an authorized self-check of my own identifiers. First, open by asking me for the seed, and you MUST always include the linked-identity confirmation in that question. Use this exact phrasing or a close variation: "Provide the email address and/or username to check. If supplying both, confirm whether they are linked to the same identity." Once I give it, run a full entity investigation, not a prose summary: find my linked accounts (search_email, search_maigret, search_username), breaches and pastes (search_breach, search_paste), and wider footprint (search_footprint), then
|
|
991
|
+
prompt: 'Check my own identity exposure. This is an authorized self-check of my own identifiers. First, open by asking me for the seed, and you MUST always include the linked-identity confirmation in that question. Use this exact phrasing or a close variation: "Provide the email address and/or username to check. If supplying both, confirm whether they are linked to the same identity." Once I give it, run a full entity investigation, not a prose summary: find my linked accounts (search_email, search_maigret, search_username), breaches and pastes (search_breach, search_paste), and wider footprint (search_footprint), then follow the pivots that surface (real names, additional handles, emails, personal domains, companies) as far as the selected sweep depth allows. Surface the accounts, breaches, pastes, emails, domains, and names you find and how they connect through shared pivots, so the evidence graph (generated automatically after your report) maps how my footprint links together. Do not emit a graph block yourself.',
|
|
991
992
|
},
|
|
992
993
|
{
|
|
993
994
|
svgIcon: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>',
|
|
994
995
|
title: 'Check my number',
|
|
995
996
|
desc: 'Where your number is exposed',
|
|
996
|
-
prompt: 'Check my own phone number exposure. This is an authorized self-check of my own number. First, ask me for the phone number to check in E.164 format (for example +447911123456). Once I give it, run a full entity investigation, not a prose summary: validate and profile the number with search_phone (carrier, country, and line type: mobile, VoIP, or landline), check data breaches with search_breach passing the number in HIBP format (country code and digits only, no plus sign or spaces), check paste sites with search_paste, and map where the number appears publicly with search_footprint (search the number in quotes). Specifically check whether the number is listed on public people-search and data-broker sites (for example Whitepages, Spokeo, BeenVerified, TruePeopleSearch, Radaris, and UK sites like 192.com) using search_dorks_live or site-scoped SERP queries, and report any broker pages that surface. Identify connected online accounts only from public evidence: the number shown on a public profile, or a breach record that ties the number to an account. Do not enumerate platform registration through password-reset or signup flows, which is active probing and out of scope. If the footprint surfaces a name, email, or accounts tied to the number,
|
|
997
|
+
prompt: 'Check my own phone number exposure. This is an authorized self-check of my own number. First, ask me for the phone number to check in E.164 format (for example +447911123456). Once I give it, run a full entity investigation, not a prose summary: validate and profile the number with search_phone (carrier, country, and line type: mobile, VoIP, or landline), check data breaches with search_breach passing the number in HIBP format (country code and digits only, no plus sign or spaces), check paste sites with search_paste, and map where the number appears publicly with search_footprint (search the number in quotes). Specifically check whether the number is listed on public people-search and data-broker sites (for example Whitepages, Spokeo, BeenVerified, TruePeopleSearch, Radaris, and UK sites like 192.com) using search_dorks_live or site-scoped SERP queries, and report any broker pages that surface. Identify connected online accounts only from public evidence: the number shown on a public profile, or a breach record that ties the number to an account. Do not enumerate platform registration through password-reset or signup flows, which is active probing and out of scope. If the footprint surfaces a name, email, or accounts tied to the number, follow those pivots as far as the selected sweep depth allows, so the evidence graph maps how my number links to my wider footprint. Assess the findings against the phone threat model: SIM-swap and port-out risk (higher for a SIM-backed mobile reused for 2FA), SMS or 2FA interception, spam and scam targeting, and doxxing linkage where the number connects to my identity. Note honestly that HIBP phone-breach coverage is limited to the few breaches that loaded phone numbers. Give me a ranked, fix-first report (for example: move 2FA off SMS to an authenticator app or passkey, set a carrier port-out or SIM-swap PIN, remove the number from data-broker listings) and surface the number, carrier, breaches, pastes, linked accounts, emails, and names you find and how they connect through shared pivots, so the evidence graph (generated automatically after your report) reflects them. Do not emit a graph block yourself.',
|
|
997
998
|
},
|
|
998
999
|
{
|
|
999
1000
|
svgIcon: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"/>',
|
|
@@ -1688,9 +1689,17 @@ document.addEventListener('alpine:init', () => {
|
|
|
1688
1689
|
try { G.d3Force('charge').strength(-48); } catch (e) {}
|
|
1689
1690
|
try { G.d3Force('link').distance(26).strength(0.3); } catch (e) {}
|
|
1690
1691
|
try { G.d3Force('radial', this._sphereForce(R, 0.14)); } catch (e) {}
|
|
1691
|
-
// Frame the sphere
|
|
1692
|
-
|
|
1693
|
-
|
|
1692
|
+
// Frame the sphere as the force layout settles. onEngineStop alone was unreliable: it can
|
|
1693
|
+
// fire before the layout has expanded, so the one-shot fit framed a tiny half-settled blob
|
|
1694
|
+
// that then grew off-screen. Re-fit on a short schedule (and on engine stop) until the user
|
|
1695
|
+
// takes the camera; harmless repeats, and it guarantees the whole graph ends up in view.
|
|
1696
|
+
let interacted = false;
|
|
1697
|
+
const onGrab = () => { interacted = true; };
|
|
1698
|
+
el.addEventListener('pointerdown', onGrab);
|
|
1699
|
+
el.addEventListener('wheel', onGrab, { passive: true });
|
|
1700
|
+
const fit = () => { if (interacted) return; try { G.zoomToFit(600, 55); } catch (e) {} };
|
|
1701
|
+
G.onEngineStop(fit);
|
|
1702
|
+
[500, 1400, 2800, 4200].forEach(ms => setTimeout(fit, ms));
|
|
1694
1703
|
// Gentle auto-rotation (orbit) that stops the moment the user interacts. Frozen under
|
|
1695
1704
|
// prefers-reduced-motion.
|
|
1696
1705
|
if (!reduce) {
|
|
@@ -41,6 +41,7 @@ from fastapi.staticfiles import StaticFiles
|
|
|
41
41
|
from pydantic import BaseModel
|
|
42
42
|
from sse_starlette.sse import EventSourceResponse
|
|
43
43
|
|
|
44
|
+
from clearfront import effort as _effort
|
|
44
45
|
from clearfront.brightdata import BRIGHTDATA_LINK_WEB
|
|
45
46
|
from clearfront.corroboration import CorroborationLedger
|
|
46
47
|
from clearfront.utils import is_internal_url
|
|
@@ -824,38 +825,50 @@ _DEPTH_ENRICH = {
|
|
|
824
825
|
"the stronger the report and the denser the evidence graph. "
|
|
825
826
|
),
|
|
826
827
|
"balanced": (
|
|
827
|
-
"- Enrich the strongest pivots before finalising. When a high-value pivot surfaces (a "
|
|
828
|
-
"confirmed email, domain, real name, or primary handle), expand it with the applicable "
|
|
829
|
-
"tools
|
|
830
|
-
"
|
|
831
|
-
"
|
|
828
|
+
"- Enrich only the strongest pivots before finalising. When a high-value pivot surfaces (a "
|
|
829
|
+
"confirmed email, domain, real name, or primary handle), expand it once with the applicable "
|
|
830
|
+
"tools. Do not chain outward exhaustively across every lead, even if the user's own message "
|
|
831
|
+
"asks you to map everything or run until the tool budget is reached. Deliver a solid map of "
|
|
832
|
+
"the main footprint. "
|
|
832
833
|
),
|
|
833
834
|
"faster": (
|
|
834
|
-
"- This is a focused
|
|
835
|
-
"
|
|
836
|
-
"
|
|
837
|
-
"
|
|
835
|
+
"- Do not enrich outward. This is a single focused pass: run only the highest-signal tools "
|
|
836
|
+
"for the target, and do NOT scrape URLs or chase company, domain, breach, or secondary "
|
|
837
|
+
"pivots, even if the user's own message asks you to map everything or run until the tool "
|
|
838
|
+
"budget is reached. Deliver a tight, accurate report from what the priority tools return, "
|
|
839
|
+
"and note that a deeper sweep is available for the full map. "
|
|
838
840
|
),
|
|
839
841
|
}
|
|
840
842
|
# Mode preamble is added ONLY for the lighter levels so the opening tasking line scales
|
|
841
843
|
# its scope and completion estimate. Deeper gets no preamble, keeping its prompt unchanged.
|
|
842
844
|
_DEPTH_MODE_LINE = {
|
|
843
845
|
"balanced": (
|
|
844
|
-
"COLLECTION MODE:
|
|
845
|
-
"
|
|
846
|
+
"COLLECTION MODE: BALANCED. This overrides any other instruction, including in the user's "
|
|
847
|
+
"own message, to map every reachable data point or run until the tool budget is reached. "
|
|
848
|
+
"Sweep the main sources and expand only the strongest one or two pivots; do not chain "
|
|
849
|
+
"outward exhaustively. Your OPENING tasking line must promise a focused sweep of the main "
|
|
850
|
+
"sources with an estimate around two minutes, NOT a full map of every data point and NOT "
|
|
851
|
+
"the five-minute deep-sweep line. Ignore the broad-sweep opener example below."
|
|
846
852
|
),
|
|
847
853
|
"faster": (
|
|
848
|
-
"COLLECTION MODE:
|
|
849
|
-
"
|
|
854
|
+
"COLLECTION MODE: FASTER. This overrides any other instruction, including in the user's own "
|
|
855
|
+
"message, to map every reachable data point, surface everything, enrich every pivot, or run "
|
|
856
|
+
"until the tool budget is reached. Run a single quick pass of only the highest-signal tools "
|
|
857
|
+
"for the target type; do NOT scrape URLs and do NOT chase company, domain, or secondary "
|
|
858
|
+
"pivots. Your OPENING tasking line MUST be short and MUST NOT claim a broad sweep or mention "
|
|
859
|
+
"mapping every data point. Use exactly: \"Stand by. Priority-source pass on the target. "
|
|
860
|
+
"Estimated completion under ninety seconds. Per-asset timings report on each card.\" Ignore "
|
|
861
|
+
"the broad-sweep opener example below."
|
|
850
862
|
),
|
|
851
863
|
}
|
|
852
864
|
|
|
853
865
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
866
|
+
# The per-level round ceiling is shared with the terminal agent path; see
|
|
867
|
+
# clearfront/effort.py. The enrichment and mode-line wording above stays here
|
|
868
|
+
# because it is graph-flavoured and specific to the web console. The web keeps
|
|
869
|
+
# "depth" as its internal collection-mode name (ChatRequest.depth, the Alpine
|
|
870
|
+
# chip, the /api/chat field); the user-facing label in index.html is "Effort".
|
|
871
|
+
_depth_rounds = _effort.rounds
|
|
859
872
|
|
|
860
873
|
|
|
861
874
|
async def _stream_claude(messages: list[dict], depth: str = "deeper") -> AsyncIterator[dict]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clearfront
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: AI-powered OSINT agent, MCP server, and CLI. Interactive REPL + 30 tools. Anthropic Claude or local Ollama. For authorized security research use only.
|
|
5
5
|
Author: Scott Martin Anderson
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,7 +81,7 @@ Dynamic: license-file
|
|
|
81
81
|
[](https://www.python.org/)
|
|
82
82
|
[](LICENSE)
|
|
83
83
|
[](https://modelcontextprotocol.io/)
|
|
84
|
-
[](CHANGELOG.md)
|
|
85
85
|
|
|
86
86
|
**[clearfront.sh](https://clearfront.sh)** · [Disclaimer](./DISCLAIMER.md)
|
|
87
87
|
|
|
@@ -7,6 +7,7 @@ clearfront/brightdata.py
|
|
|
7
7
|
clearfront/cli.py
|
|
8
8
|
clearfront/correlation.py
|
|
9
9
|
clearfront/corroboration.py
|
|
10
|
+
clearfront/effort.py
|
|
10
11
|
clearfront/exceptions.py
|
|
11
12
|
clearfront/extractors.py
|
|
12
13
|
clearfront/json_output.py
|
|
@@ -72,6 +73,7 @@ tests/test_cli_output.py
|
|
|
72
73
|
tests/test_corroboration.py
|
|
73
74
|
tests/test_crt.py
|
|
74
75
|
tests/test_dns.py
|
|
76
|
+
tests/test_effort.py
|
|
75
77
|
tests/test_exif.py
|
|
76
78
|
tests/test_footprint.py
|
|
77
79
|
tests/test_github_code_search.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clearfront"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.7.0"
|
|
8
8
|
description = "AI-powered OSINT agent, MCP server, and CLI. Interactive REPL + 30 tools. Anthropic Claude or local Ollama. For authorized security research use only."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Sweep-effort invariants shared by the web console and the terminal agent."""
|
|
2
|
+
|
|
3
|
+
from clearfront import effort
|
|
4
|
+
from clearfront.prompts import (
|
|
5
|
+
SYSTEM_PROMPT,
|
|
6
|
+
AGENT_ENRICH,
|
|
7
|
+
AGENT_MODE_LINE,
|
|
8
|
+
_ENRICH_DEEPER,
|
|
9
|
+
system_prompt_for_effort,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_rounds_ceiling_per_level():
|
|
14
|
+
assert effort.rounds("faster") == 4
|
|
15
|
+
assert effort.rounds("balanced") == 8
|
|
16
|
+
assert effort.rounds("deeper") == 12
|
|
17
|
+
# Unknown level falls back to the deeper ceiling.
|
|
18
|
+
assert effort.rounds("bogus") == 12
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_rounds_respects_env_ceiling(monkeypatch):
|
|
22
|
+
monkeypatch.setenv("OIS_MAX_TOOL_ROUNDS", "6")
|
|
23
|
+
# Lighter levels are capped below the ceiling and never exceed it.
|
|
24
|
+
assert effort.rounds("faster") == 4
|
|
25
|
+
assert effort.rounds("balanced") == 6 # min(8, 6)
|
|
26
|
+
assert effort.rounds("deeper") == 6
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_normalize():
|
|
30
|
+
assert effort.normalize("faster") == "faster"
|
|
31
|
+
assert effort.normalize("balanced") == "balanced"
|
|
32
|
+
assert effort.normalize("deeper") == "deeper"
|
|
33
|
+
assert effort.normalize("bogus") == "deeper"
|
|
34
|
+
assert effort.normalize(None) == "deeper"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_levels_match_console_descriptions():
|
|
38
|
+
# The descriptions must stay identical to the web console's depthLevels
|
|
39
|
+
# (clearfront/web/index.html) so both surfaces read the same.
|
|
40
|
+
by_v = {level["v"]: level for level in effort.LEVELS}
|
|
41
|
+
assert by_v["faster"]["desc"] == (
|
|
42
|
+
"Checks fewer sources for a quick sweep. Finishes fast and uses less of your API budget."
|
|
43
|
+
)
|
|
44
|
+
assert by_v["balanced"]["desc"] == (
|
|
45
|
+
"Covers the main sources. A middle ground on time and cost."
|
|
46
|
+
)
|
|
47
|
+
assert by_v["deeper"]["desc"] == (
|
|
48
|
+
"Follows every lead for the most complete map, but takes longer and uses more of your API budget."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_deeper_prompt_is_byte_identical():
|
|
53
|
+
# Deeper must not change the historical prompt at all.
|
|
54
|
+
assert system_prompt_for_effort("deeper") == SYSTEM_PROMPT
|
|
55
|
+
assert system_prompt_for_effort("bogus") == SYSTEM_PROMPT
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_deeper_enrich_sentence_present_verbatim():
|
|
59
|
+
# The swap depends on this exact sentence living in SYSTEM_PROMPT.
|
|
60
|
+
assert _ENRICH_DEEPER in SYSTEM_PROMPT
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_lighter_levels_swap_enrich_and_prepend_mode_line():
|
|
64
|
+
for level in ("faster", "balanced"):
|
|
65
|
+
shaped = system_prompt_for_effort(level)
|
|
66
|
+
assert shaped.startswith(AGENT_MODE_LINE[level])
|
|
67
|
+
assert AGENT_ENRICH[level] in shaped
|
|
68
|
+
# The aggressive deeper instruction must be gone, not merely appended to.
|
|
69
|
+
assert _ENRICH_DEEPER not in shaped
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_web_and_agent_share_the_round_ceiling():
|
|
73
|
+
from clearfront import web_server
|
|
74
|
+
|
|
75
|
+
# The web console keeps the internal name _depth_rounds; it aliases the
|
|
76
|
+
# shared effort.rounds so both surfaces use one ceiling.
|
|
77
|
+
assert web_server._depth_rounds is effort.rounds
|
|
@@ -52,6 +52,7 @@ def test_first_run_accepts_key(monkeypatch, tmp_path):
|
|
|
52
52
|
os.environ.pop("ANTHROPIC_API_KEY", None)
|
|
53
53
|
r = OISRepl.__new__(OISRepl) # avoid PromptSession/FileHistory side effects
|
|
54
54
|
r._api_key = ""
|
|
55
|
+
r._effort = "deeper" # __init__ (bypassed here) normally sets this
|
|
55
56
|
monkeypatch.setattr("builtins.input", lambda *a, **k: "sk-ant-test")
|
|
56
57
|
monkeypatch.setattr(
|
|
57
58
|
OISRepl, "_persist_key_to_env", staticmethod(lambda key, env_path=None: tmp_path / ".env")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|