whitebox follow up: better wiki
This commit is contained in:
@@ -116,8 +116,8 @@ WHITE-BOX TESTING (code provided):
|
||||
- Static coverage target per repository: run one `semgrep` pass, one secrets pass (`gitleaks` and/or `trufflehog`), one `trivy fs` pass, and one AST-structural pass (`sg` and/or Tree-sitter); if any are skipped, record why in the shared wiki
|
||||
- Keep AST artifacts bounded and high-signal: scope to relevant paths/hypotheses, avoid whole-repo generic function dumps
|
||||
- AST target selection rule: build `sg-targets.txt` from `semgrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`), then run `xargs ... sg run` against that file list. Only use path-heuristic fallback if semgrep scope is unavailable, and log fallback reason in the wiki.
|
||||
- Shared memory: Use notes as shared working memory; discover wiki notes with `list_notes`, then read the selected one via `get_note(note_id=...)` before analysis
|
||||
- Before `agent_finish`/`finish_scan`, update the shared repo wiki with scanner summaries, key routes/sinks, and dynamic follow-up plan
|
||||
- Shared memory: Use notes as shared working memory; discover wiki notes with `list_notes`, read `wiki:overview` first when available, then read `wiki:security` via `get_note(note_id=...)` before analysis
|
||||
- Before `agent_finish`/`finish_scan`, update `wiki:security` with scanner summaries, key routes/sinks, and dynamic follow-up plan
|
||||
- Dynamic: Run the application and test live to validate exploitability
|
||||
- NEVER rely solely on static code analysis when dynamic validation is possible
|
||||
- Begin with fast source triage and dynamic run preparation in parallel; use static findings to prioritize live testing.
|
||||
|
||||
@@ -44,14 +44,16 @@ Coverage target per repository:
|
||||
|
||||
## Wiki Note Requirement (Source Map)
|
||||
|
||||
When source is present, maintain one wiki note per repository and keep it current.
|
||||
When source is present, maintain two stable wiki notes per repository and keep them current:
|
||||
- `wiki:overview` for architecture/source-map context
|
||||
- `wiki:security` for scanner and validation deltas
|
||||
|
||||
Operational rules:
|
||||
- At task start, call `list_notes` with `category=wiki`, then read the selected wiki with `get_note(note_id=...)`.
|
||||
- If no repo wiki exists, create one with `create_note` and `category=wiki`.
|
||||
- Update the same wiki via `update_note`; avoid creating duplicate wiki notes for the same repo.
|
||||
- Child agents should read wiki notes first via `get_note`, then extend with new evidence from their scope.
|
||||
- Before calling `agent_finish`, each source-focused child agent should append a short delta update to the shared repo wiki (scanner outputs, route/sink map deltas, dynamic follow-ups).
|
||||
- At task start, call `list_notes` with `category=wiki`; read `wiki:overview` first, then `wiki:security` via `get_note(note_id=...)`.
|
||||
- If wiki notes are missing, create them with `create_note`, `category=wiki`, and tags including `wiki:overview` or `wiki:security`.
|
||||
- Update existing notes via `update_note`; avoid creating duplicates.
|
||||
- Child agents should read both notes first, then extend with new evidence from their scope.
|
||||
- Before calling `agent_finish`, each source-focused child agent should append a short delta update to `wiki:security` (scanner outputs, route/sink map deltas, dynamic follow-ups).
|
||||
|
||||
Recommended sections:
|
||||
- Architecture overview
|
||||
|
||||
@@ -19,12 +19,12 @@ Before scanning, check shared wiki memory:
|
||||
|
||||
```text
|
||||
1) list_notes(category="wiki")
|
||||
2) get_note(note_id=...) for the selected repo wiki before analysis
|
||||
3) Reuse matching repo wiki note if present
|
||||
4) create_note(category="wiki") only if missing
|
||||
2) get_note(note_id=...) for `wiki:overview` first, then `wiki:security`
|
||||
3) Reuse matching repo wiki notes if present
|
||||
4) create_note(category="wiki") only if missing (with tags `wiki:overview` / `wiki:security`)
|
||||
```
|
||||
|
||||
After every major source-analysis batch, update the same repo wiki note with `update_note` so other agents can reuse your latest map.
|
||||
After every major source-analysis batch, update `wiki:security` with `update_note` so other agents can reuse your latest map.
|
||||
|
||||
## Baseline Coverage Bundle (Recommended)
|
||||
|
||||
@@ -74,7 +74,7 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
|
||||
--format json --output "$ART/trivy-fs.json" . || true
|
||||
```
|
||||
|
||||
If one tool is skipped or fails, record that in the shared wiki note along with the reason.
|
||||
If one tool is skipped or fails, record that in `wiki:security` along with the reason.
|
||||
|
||||
## Semgrep First Pass
|
||||
|
||||
@@ -143,7 +143,7 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
|
||||
|
||||
## Wiki Update Template
|
||||
|
||||
Keep one wiki note per repository and update these sections:
|
||||
Keep `wiki:overview` and `wiki:security` per repository. Update these sections in `wiki:security`:
|
||||
|
||||
```text
|
||||
## Architecture
|
||||
@@ -164,4 +164,4 @@ Before `agent_finish`, make one final `update_note` call to capture:
|
||||
- Do not treat scanner output as final truth.
|
||||
- Do not spend full cycles on low-signal pattern matches.
|
||||
- Do not report source-only findings without validation evidence.
|
||||
- Do not create multiple wiki notes for the same repository when one already exists.
|
||||
- Do not create duplicate `wiki:overview` or `wiki:security` notes for the same repository.
|
||||
|
||||
@@ -15,7 +15,7 @@ Thorough understanding before exploitation. Test every parameter, every endpoint
|
||||
|
||||
**Whitebox (source available)**
|
||||
- Map every file, module, and code path in the repository
|
||||
- Load and maintain shared `wiki` notes from the start (`list_notes(category="wiki")` then `get_note(note_id=...)`), then continuously update one repo note
|
||||
- Load and maintain shared `wiki` notes from the start (`list_notes(category="wiki")`, then `get_note(note_id=...)` for `wiki:overview` and `wiki:security`), then continuously update `wiki:security`
|
||||
- Start with broad source-aware triage (`semgrep`, `ast-grep`, `gitleaks`, `trufflehog`, `trivy fs`) and use outputs to drive deep review
|
||||
- Execute at least one structural AST pass (`sg` and/or Tree-sitter) per repository and store artifacts for reuse
|
||||
- Keep AST artifacts bounded and query-driven (target relevant paths/sinks first; avoid whole-repo generic function dumps)
|
||||
@@ -31,7 +31,7 @@ Thorough understanding before exploitation. Test every parameter, every endpoint
|
||||
- Review file handling: upload, download, processing
|
||||
- Understand the deployment model and infrastructure assumptions
|
||||
- Check all dependency versions and repository risks against CVE/misconfiguration data
|
||||
- Before final completion, update the shared repo wiki with scanner summary + dynamic follow-ups
|
||||
- Before final completion, update `wiki:security` with scanner summary + dynamic follow-ups
|
||||
|
||||
**Blackbox (no source)**
|
||||
- Exhaustive subdomain enumeration with multiple sources and tools
|
||||
|
||||
@@ -15,7 +15,7 @@ Optimize for fast feedback on critical security issues. Skip exhaustive enumerat
|
||||
|
||||
**Whitebox (source available)**
|
||||
- Focus on recent changes: git diffs, new commits, modified files—these are most likely to contain fresh bugs
|
||||
- Read existing `wiki` notes first (`list_notes(category="wiki")` then `get_note(note_id=...)`) to avoid remapping from scratch
|
||||
- Read existing `wiki` notes first (`list_notes(category="wiki")`, then `get_note(note_id=...)` for `wiki:overview` and `wiki:security`) to avoid remapping from scratch
|
||||
- Run a fast static triage on changed files first (`semgrep`, then targeted `sg` queries)
|
||||
- Run at least one lightweight AST pass (`sg` or Tree-sitter) so structural mapping is not skipped
|
||||
- Keep AST commands tightly scoped to changed or high-risk paths; avoid broad repository-wide pattern dumps
|
||||
@@ -23,7 +23,7 @@ Optimize for fast feedback on critical security issues. Skip exhaustive enumerat
|
||||
- Identify security-sensitive patterns in changed code: auth checks, input handling, database queries, file operations
|
||||
- Trace user input through modified code paths
|
||||
- Check if security controls were modified or bypassed
|
||||
- Before completion, update the shared repo wiki with what changed and what needs dynamic follow-up
|
||||
- Before completion, update `wiki:security` with what changed and what needs dynamic follow-up
|
||||
|
||||
**Blackbox (no source)**
|
||||
- Map authentication and critical user flows
|
||||
|
||||
@@ -15,7 +15,7 @@ Systematic testing across the full attack surface. Understand the application be
|
||||
|
||||
**Whitebox (source available)**
|
||||
- Map codebase structure: modules, entry points, routing
|
||||
- Start by loading existing `wiki` notes (`list_notes(category="wiki")` then `get_note(note_id=...)`) and update one shared repo note as mapping evolves
|
||||
- Start by loading existing `wiki` notes (`list_notes(category="wiki")`, then `get_note(note_id=...)` for `wiki:overview` and `wiki:security`) and update `wiki:security` as mapping evolves
|
||||
- Run `semgrep` first-pass triage to prioritize risky flows before deep manual review
|
||||
- Run at least one AST-structural mapping pass (`sg` and/or Tree-sitter), then use outputs for route, sink, and trust-boundary mapping
|
||||
- Keep AST output bounded to relevant paths and hypotheses; avoid whole-repo generic function dumps
|
||||
@@ -25,7 +25,7 @@ Systematic testing across the full attack surface. Understand the application be
|
||||
- Analyze database interactions and ORM usage
|
||||
- Check dependencies and repo risks with `trivy fs`, `gitleaks`, and `trufflehog`
|
||||
- Understand the data model and sensitive data locations
|
||||
- Before completion, update the shared repo wiki with source findings summary and dynamic validation next steps
|
||||
- Before completion, update `wiki:security` with source findings summary and dynamic validation next steps
|
||||
|
||||
**Blackbox (no source)**
|
||||
- Crawl application thoroughly, interact with every feature
|
||||
|
||||
@@ -44,7 +44,32 @@ def _extract_repo_tags(agent_state: Any | None) -> set[str]:
|
||||
return repo_tags
|
||||
|
||||
|
||||
def _load_primary_wiki_note(agent_state: Any | None = None) -> dict[str, Any] | None:
|
||||
def _extract_wiki_kind(note: dict[str, Any]) -> str:
|
||||
note_kind = str(note.get("wiki_kind") or "").strip().lower()
|
||||
if note_kind in {"overview", "security", "general"}:
|
||||
return note_kind
|
||||
|
||||
note_tags = note.get("tags") or []
|
||||
if isinstance(note_tags, list):
|
||||
normalized_tags = {str(tag).strip().lower() for tag in note_tags if str(tag).strip()}
|
||||
if "wiki:overview" in normalized_tags:
|
||||
return "overview"
|
||||
if "wiki:security" in normalized_tags:
|
||||
return "security"
|
||||
|
||||
title = str(note.get("title") or "").lower()
|
||||
if "overview" in title or "architecture" in title:
|
||||
return "overview"
|
||||
if "security" in title or "vuln" in title or "finding" in title:
|
||||
return "security"
|
||||
return "general"
|
||||
|
||||
|
||||
def _load_primary_wiki_note(
|
||||
agent_state: Any | None = None,
|
||||
preferred_kind: str | None = None,
|
||||
allow_kind_fallback: bool = True,
|
||||
) -> dict[str, Any] | None:
|
||||
try:
|
||||
from strix.tools.notes.notes_actions import get_note, list_notes
|
||||
|
||||
@@ -56,19 +81,32 @@ def _load_primary_wiki_note(agent_state: Any | None = None) -> dict[str, Any] |
|
||||
if not notes:
|
||||
return None
|
||||
|
||||
candidate_notes = notes
|
||||
selected_note_id = None
|
||||
repo_tags = _extract_repo_tags(agent_state)
|
||||
if repo_tags:
|
||||
tagged_notes = []
|
||||
for note in notes:
|
||||
note_tags = note.get("tags") or []
|
||||
if not isinstance(note_tags, list):
|
||||
continue
|
||||
normalized_note_tags = {str(tag).strip().lower() for tag in note_tags if str(tag).strip()}
|
||||
if normalized_note_tags.intersection(repo_tags):
|
||||
tagged_notes.append(note)
|
||||
if tagged_notes:
|
||||
candidate_notes = tagged_notes
|
||||
|
||||
normalized_kind = (preferred_kind or "").strip().lower()
|
||||
if normalized_kind in {"overview", "security", "general"}:
|
||||
for note in candidate_notes:
|
||||
if _extract_wiki_kind(note) == normalized_kind:
|
||||
selected_note_id = note.get("note_id")
|
||||
break
|
||||
|
||||
note_id = selected_note_id or notes[0].get("note_id")
|
||||
if not selected_note_id and (not normalized_kind or allow_kind_fallback):
|
||||
selected_note_id = candidate_notes[0].get("note_id")
|
||||
|
||||
note_id = selected_note_id
|
||||
if not isinstance(note_id, str) or not note_id:
|
||||
return None
|
||||
|
||||
@@ -90,26 +128,44 @@ def _inject_wiki_context_for_whitebox(agent_state: Any) -> None:
|
||||
if not _is_whitebox_agent(agent_state.agent_id):
|
||||
return
|
||||
|
||||
wiki_note = _load_primary_wiki_note(agent_state)
|
||||
if not wiki_note:
|
||||
return
|
||||
overview_note = _load_primary_wiki_note(
|
||||
agent_state,
|
||||
preferred_kind="overview",
|
||||
allow_kind_fallback=False,
|
||||
)
|
||||
security_note = _load_primary_wiki_note(
|
||||
agent_state,
|
||||
preferred_kind="security",
|
||||
allow_kind_fallback=True,
|
||||
)
|
||||
|
||||
title = str(wiki_note.get("title") or "repo wiki")
|
||||
content = str(wiki_note.get("content") or "").strip()
|
||||
if not content:
|
||||
return
|
||||
notes_to_embed: list[tuple[str, dict[str, Any]]] = []
|
||||
if isinstance(overview_note, dict):
|
||||
notes_to_embed.append(("overview", overview_note))
|
||||
|
||||
if isinstance(security_note, dict):
|
||||
overview_note_id = str(overview_note.get("note_id")) if isinstance(overview_note, dict) else ""
|
||||
security_note_id = str(security_note.get("note_id"))
|
||||
if not overview_note_id or overview_note_id != security_note_id:
|
||||
notes_to_embed.append(("security", security_note))
|
||||
|
||||
max_chars = 4000
|
||||
truncated_content = content[:max_chars]
|
||||
suffix = "\n\n[truncated for context size]" if len(content) > max_chars else ""
|
||||
agent_state.add_message(
|
||||
"user",
|
||||
(
|
||||
f"<shared_repo_wiki title=\"{title}\">\n"
|
||||
f"{truncated_content}{suffix}\n"
|
||||
"</shared_repo_wiki>"
|
||||
),
|
||||
)
|
||||
for wiki_kind, note in notes_to_embed:
|
||||
title = str(note.get("title") or "repo wiki")
|
||||
content = str(note.get("content") or "").strip()
|
||||
if not content:
|
||||
continue
|
||||
|
||||
truncated_content = content[:max_chars]
|
||||
suffix = "\n\n[truncated for context size]" if len(content) > max_chars else ""
|
||||
agent_state.add_message(
|
||||
"user",
|
||||
(
|
||||
f"<shared_repo_wiki type=\"{wiki_kind}\" title=\"{title}\">\n"
|
||||
f"{truncated_content}{suffix}\n"
|
||||
"</shared_repo_wiki>"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _append_wiki_update_on_finish(
|
||||
@@ -125,7 +181,11 @@ def _append_wiki_update_on_finish(
|
||||
try:
|
||||
from strix.tools.notes.notes_actions import append_note_content
|
||||
|
||||
note = _load_primary_wiki_note(agent_state)
|
||||
note = _load_primary_wiki_note(
|
||||
agent_state,
|
||||
preferred_kind="security",
|
||||
allow_kind_fallback=True,
|
||||
)
|
||||
if not note:
|
||||
return
|
||||
|
||||
@@ -179,10 +239,10 @@ def _run_agent_in_thread(
|
||||
wiki_memory_instruction = ""
|
||||
if getattr(getattr(agent, "llm_config", None), "is_whitebox", False):
|
||||
wiki_memory_instruction = (
|
||||
'\n - White-box memory (recommended): call list_notes(category="wiki") and then '
|
||||
"get_note(note_id=...) before substantive work (including terminal scans)"
|
||||
"\n - Reuse one repo wiki note where possible and avoid duplicates"
|
||||
"\n - Before agent_finish, call list_notes(category=\"wiki\") + get_note(note_id=...) again, then append a short scope delta via update_note (new routes/sinks, scanner results, dynamic follow-ups)"
|
||||
'\n - White-box memory (recommended): call list_notes(category="wiki"), read '
|
||||
"wiki:overview first when available, then wiki:security via get_note(note_id=...) before substantive work (including terminal scans)"
|
||||
"\n - Prefer two stable wiki notes per repo: one tagged wiki:overview and one tagged wiki:security; avoid duplicates"
|
||||
"\n - Before agent_finish, call list_notes(category=\"wiki\") + get_note(note_id=...) again, then append a short scope delta via update_note to wiki:security (new routes/sinks, scanner results, dynamic follow-ups)"
|
||||
"\n - If terminal output contains `command not found` or shell parse errors, correct and rerun before using the result"
|
||||
"\n - Use ASCII-only shell commands; if a command includes unexpected non-ASCII characters, rerun with a clean ASCII command"
|
||||
"\n - Keep AST artifacts bounded: target relevant paths and avoid whole-repo generic function dumps"
|
||||
@@ -382,10 +442,10 @@ def create_agent(
|
||||
"keep artifacts bounded and skip forced AST steps for purely dynamic validation tasks.\n"
|
||||
"- Keep AST output bounded: scope to relevant paths/files, avoid whole-repo "
|
||||
"generic function patterns, and cap artifact size.\n"
|
||||
'- Use shared wiki memory by calling list_notes(category="wiki") then '
|
||||
"get_note(note_id=...).\n"
|
||||
'- Use shared wiki memory by calling list_notes(category="wiki"), reading wiki:overview first '
|
||||
"then wiki:security via get_note(note_id=...).\n"
|
||||
'- Before agent_finish, call list_notes(category="wiki") + get_note(note_id=...) '
|
||||
"again, reuse one repo wiki, and call update_note.\n"
|
||||
"again, and append updates to wiki:security.\n"
|
||||
"- If terminal output contains `command not found` or shell parse errors, "
|
||||
"correct and rerun before using the result."
|
||||
)
|
||||
|
||||
@@ -15,6 +15,28 @@ _loaded_notes_run_dir: str | None = None
|
||||
_DEFAULT_CONTENT_PREVIEW_CHARS = 280
|
||||
|
||||
|
||||
def _note_tag_set(note: dict[str, Any]) -> set[str]:
|
||||
tags = note.get("tags", [])
|
||||
if not isinstance(tags, list):
|
||||
return set()
|
||||
return {str(tag).strip().lower() for tag in tags if str(tag).strip()}
|
||||
|
||||
|
||||
def _infer_wiki_kind(note: dict[str, Any]) -> str:
|
||||
tag_set = _note_tag_set(note)
|
||||
if "wiki:overview" in tag_set:
|
||||
return "overview"
|
||||
if "wiki:security" in tag_set:
|
||||
return "security"
|
||||
|
||||
title = str(note.get("title", "")).lower()
|
||||
if "overview" in title or "architecture" in title:
|
||||
return "overview"
|
||||
if "security" in title or "vuln" in title or "finding" in title:
|
||||
return "security"
|
||||
return "general"
|
||||
|
||||
|
||||
def _get_run_dir() -> Path | None:
|
||||
try:
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
@@ -107,6 +129,7 @@ def _ensure_notes_loaded() -> None:
|
||||
for note_id, note in _notes_storage.items():
|
||||
if note.get("category") == "wiki":
|
||||
_persist_wiki_note(note_id, note)
|
||||
_persist_wiki_index()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@@ -133,6 +156,13 @@ def _get_wiki_directory() -> Path | None:
|
||||
return wiki_dir
|
||||
|
||||
|
||||
def _get_wiki_index_path() -> Path | None:
|
||||
wiki_dir = _get_wiki_directory()
|
||||
if not wiki_dir:
|
||||
return None
|
||||
return wiki_dir / "index.json"
|
||||
|
||||
|
||||
def _get_wiki_note_path(note_id: str, note: dict[str, Any]) -> Path | None:
|
||||
wiki_dir = _get_wiki_directory()
|
||||
if not wiki_dir:
|
||||
@@ -167,6 +197,34 @@ def _persist_wiki_note(note_id: str, note: dict[str, Any]) -> None:
|
||||
wiki_path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _persist_wiki_index() -> None:
|
||||
index_path = _get_wiki_index_path()
|
||||
if not index_path:
|
||||
return
|
||||
|
||||
notes: list[dict[str, Any]] = []
|
||||
for note_id, note in _notes_storage.items():
|
||||
if note.get("category") != "wiki":
|
||||
continue
|
||||
wiki_path = _get_wiki_note_path(note_id, note)
|
||||
notes.append(
|
||||
{
|
||||
"note_id": note_id,
|
||||
"title": str(note.get("title", "")),
|
||||
"wiki_kind": _infer_wiki_kind(note),
|
||||
"tags": note.get("tags", []),
|
||||
"created_at": note.get("created_at", ""),
|
||||
"updated_at": note.get("updated_at", ""),
|
||||
"wiki_filename": note.get("wiki_filename", ""),
|
||||
"wiki_path": wiki_path.name if wiki_path else "",
|
||||
}
|
||||
)
|
||||
|
||||
notes.sort(key=lambda item: item.get("updated_at", ""), reverse=True)
|
||||
payload = {"generated_at": datetime.now(UTC).isoformat(), "notes": notes}
|
||||
index_path.write_text(f"{json.dumps(payload, ensure_ascii=True, indent=2)}\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _remove_wiki_note(note_id: str, note: dict[str, Any]) -> None:
|
||||
wiki_path = _get_wiki_note_path(note_id, note)
|
||||
if not wiki_path:
|
||||
@@ -226,6 +284,9 @@ def _to_note_listing_entry(
|
||||
if isinstance(wiki_filename, str) and wiki_filename:
|
||||
entry["wiki_filename"] = wiki_filename
|
||||
|
||||
if note.get("category") == "wiki":
|
||||
entry["wiki_kind"] = _infer_wiki_kind(note)
|
||||
|
||||
content = str(note.get("content", ""))
|
||||
if include_content:
|
||||
entry["content"] = content
|
||||
@@ -290,6 +351,7 @@ def create_note( # noqa: PLR0911
|
||||
_append_note_event("create", note_id, note)
|
||||
if category == "wiki":
|
||||
_persist_wiki_note(note_id, note)
|
||||
_persist_wiki_index()
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
return {"success": False, "error": f"Failed to create note: {e}", "note_id": None}
|
||||
@@ -356,6 +418,8 @@ def get_note(note_id: str) -> dict[str, Any]:
|
||||
|
||||
note_with_id = note.copy()
|
||||
note_with_id["note_id"] = note_id
|
||||
if note.get("category") == "wiki":
|
||||
note_with_id["wiki_kind"] = _infer_wiki_kind(note)
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
return {
|
||||
@@ -420,6 +484,7 @@ def update_note(
|
||||
_append_note_event("update", note_id, note)
|
||||
if note.get("category") == "wiki":
|
||||
_persist_wiki_note(note_id, note)
|
||||
_persist_wiki_index()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -443,10 +508,13 @@ def delete_note(note_id: str) -> dict[str, Any]:
|
||||
|
||||
note = _notes_storage[note_id]
|
||||
note_title = note["title"]
|
||||
if note.get("category") == "wiki":
|
||||
is_wiki = note.get("category") == "wiki"
|
||||
if is_wiki:
|
||||
_remove_wiki_note(note_id, note)
|
||||
del _notes_storage[note_id]
|
||||
_append_note_event("delete", note_id)
|
||||
if is_wiki:
|
||||
_persist_wiki_index()
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
return {"success": False, "error": f"Failed to delete note: {e}"}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<details>Use this tool for documenting discoveries, observations, methodology notes, and questions.
|
||||
This is your personal and shared run memory for recording information you want to remember or reference later.
|
||||
Use category "wiki" for repository source maps shared across agents in the same run.
|
||||
For Codewiki patterns, prefer wiki tags `wiki:overview` and `wiki:security` for stable note roles.
|
||||
For tracking actionable tasks, use the todo tool instead.</details>
|
||||
<parameters>
|
||||
<parameter name="title" type="string" required="true">
|
||||
@@ -109,7 +110,7 @@ The /api/internal/* endpoints are high priority as they appear to lack authentic
|
||||
</parameter>
|
||||
</parameters>
|
||||
<returns type="Dict[str, Any]">
|
||||
<description>Response containing: - notes: List of matching notes (metadata + optional content/content_preview) - total_count: Total number of notes found</description>
|
||||
<description>Response containing: - notes: List of matching notes (metadata + optional content/content_preview; wiki entries include wiki_kind) - total_count: Total number of notes found</description>
|
||||
</returns>
|
||||
<examples>
|
||||
# List all findings
|
||||
|
||||
@@ -296,3 +296,120 @@ def test_load_primary_wiki_note_prefers_repo_tag_match(monkeypatch) -> None:
|
||||
assert note is not None
|
||||
assert note["note_id"] == "wiki-target"
|
||||
assert selected_note_ids == ["wiki-target"]
|
||||
|
||||
|
||||
def test_load_primary_wiki_note_prefers_requested_wiki_kind(monkeypatch) -> None:
|
||||
selected_note_ids: list[str] = []
|
||||
|
||||
def fake_list_notes(category=None):
|
||||
assert category == "wiki"
|
||||
return {
|
||||
"success": True,
|
||||
"notes": [
|
||||
{"note_id": "wiki-security", "tags": ["repo:appsmith", "wiki:security"]},
|
||||
{"note_id": "wiki-overview", "tags": ["repo:appsmith", "wiki:overview"]},
|
||||
],
|
||||
"total_count": 2,
|
||||
}
|
||||
|
||||
def fake_get_note(note_id: str):
|
||||
selected_note_ids.append(note_id)
|
||||
return {
|
||||
"success": True,
|
||||
"note": {
|
||||
"note_id": note_id,
|
||||
"title": "Repo Wiki",
|
||||
"content": "content",
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
|
||||
|
||||
agent_state = SimpleNamespace(task="analyze /workspace/appsmith")
|
||||
overview_note = agents_graph_actions._load_primary_wiki_note(
|
||||
agent_state,
|
||||
preferred_kind="overview",
|
||||
allow_kind_fallback=False,
|
||||
)
|
||||
security_note = agents_graph_actions._load_primary_wiki_note(
|
||||
agent_state,
|
||||
preferred_kind="security",
|
||||
allow_kind_fallback=True,
|
||||
)
|
||||
|
||||
assert overview_note is not None
|
||||
assert security_note is not None
|
||||
assert overview_note["note_id"] == "wiki-overview"
|
||||
assert security_note["note_id"] == "wiki-security"
|
||||
assert selected_note_ids == ["wiki-overview", "wiki-security"]
|
||||
|
||||
|
||||
def test_agent_finish_prefers_security_wiki_for_append(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
|
||||
|
||||
agents_graph_actions._agent_graph["nodes"].clear()
|
||||
agents_graph_actions._agent_graph["edges"].clear()
|
||||
agents_graph_actions._agent_messages.clear()
|
||||
agents_graph_actions._running_agents.clear()
|
||||
agents_graph_actions._agent_instances.clear()
|
||||
agents_graph_actions._agent_states.clear()
|
||||
|
||||
parent_id = "parent-sec"
|
||||
child_id = "child-sec"
|
||||
agents_graph_actions._agent_graph["nodes"][parent_id] = {
|
||||
"name": "Parent",
|
||||
"task": "parent task",
|
||||
"status": "running",
|
||||
"parent_id": None,
|
||||
}
|
||||
agents_graph_actions._agent_graph["nodes"][child_id] = {
|
||||
"name": "Child",
|
||||
"task": "child task",
|
||||
"status": "running",
|
||||
"parent_id": parent_id,
|
||||
}
|
||||
agents_graph_actions._agent_instances[child_id] = SimpleNamespace(
|
||||
llm_config=LLMConfig(is_whitebox=True)
|
||||
)
|
||||
|
||||
captured: dict[str, str] = {}
|
||||
|
||||
def fake_list_notes(category=None):
|
||||
assert category == "wiki"
|
||||
return {
|
||||
"success": True,
|
||||
"notes": [
|
||||
{"note_id": "wiki-overview", "tags": ["repo:appsmith", "wiki:overview"]},
|
||||
{"note_id": "wiki-security", "tags": ["repo:appsmith", "wiki:security"]},
|
||||
],
|
||||
"total_count": 2,
|
||||
}
|
||||
|
||||
def fake_get_note(note_id: str):
|
||||
return {
|
||||
"success": True,
|
||||
"note": {"note_id": note_id, "title": "Repo Wiki", "content": "Existing wiki content"},
|
||||
}
|
||||
|
||||
def fake_append_note_content(note_id: str, delta: str):
|
||||
captured["note_id"] = note_id
|
||||
captured["delta"] = delta
|
||||
return {"success": True, "note_id": note_id}
|
||||
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.append_note_content", fake_append_note_content)
|
||||
|
||||
state = SimpleNamespace(agent_id=child_id, parent_id=parent_id, task="analyze /workspace/appsmith")
|
||||
result = agents_graph_actions.agent_finish(
|
||||
agent_state=state,
|
||||
result_summary="Static triage completed",
|
||||
findings=["Found candidate sink"],
|
||||
success=True,
|
||||
final_recommendations=["Validate with dynamic PoC"],
|
||||
)
|
||||
|
||||
assert result["agent_completed"] is True
|
||||
assert captured["note_id"] == "wiki-security"
|
||||
assert "Static triage completed" in captured["delta"]
|
||||
|
||||
@@ -124,7 +124,7 @@ def test_get_note_returns_full_note(tmp_path: Path, monkeypatch) -> None:
|
||||
title="Repo wiki",
|
||||
content="entrypoints and sinks",
|
||||
category="wiki",
|
||||
tags=["repo:appsmith"],
|
||||
tags=["repo:appsmith", "wiki:security"],
|
||||
)
|
||||
assert created["success"] is True
|
||||
note_id = created["note_id"]
|
||||
@@ -134,6 +134,7 @@ def test_get_note_returns_full_note(tmp_path: Path, monkeypatch) -> None:
|
||||
assert result["success"] is True
|
||||
assert result["note"]["note_id"] == note_id
|
||||
assert result["note"]["content"] == "entrypoints and sinks"
|
||||
assert result["note"]["wiki_kind"] == "security"
|
||||
finally:
|
||||
_reset_notes_state()
|
||||
set_global_tracer(previous_tracer) # type: ignore[arg-type]
|
||||
@@ -212,3 +213,55 @@ def test_list_and_get_note_handle_wiki_repersist_oserror_gracefully(
|
||||
finally:
|
||||
_reset_notes_state()
|
||||
set_global_tracer(previous_tracer) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_wiki_index_tracks_overview_and_security_notes(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_reset_notes_state()
|
||||
|
||||
previous_tracer = get_global_tracer()
|
||||
tracer = Tracer("wiki-index-run")
|
||||
set_global_tracer(tracer)
|
||||
|
||||
try:
|
||||
overview = notes_actions.create_note(
|
||||
title="Repo overview wiki",
|
||||
content="architecture and entrypoints",
|
||||
category="wiki",
|
||||
tags=["repo:demo", "wiki:overview"],
|
||||
)
|
||||
assert overview["success"] is True
|
||||
overview_id = overview["note_id"]
|
||||
assert isinstance(overview_id, str)
|
||||
|
||||
security = notes_actions.create_note(
|
||||
title="Repo security wiki",
|
||||
content="scanner summary and follow-ups",
|
||||
category="wiki",
|
||||
tags=["repo:demo", "wiki:security"],
|
||||
)
|
||||
assert security["success"] is True
|
||||
security_id = security["note_id"]
|
||||
assert isinstance(security_id, str)
|
||||
|
||||
wiki_index = tmp_path / "strix_runs" / "wiki-index-run" / "wiki" / "index.json"
|
||||
assert wiki_index.exists() is True
|
||||
index_data = wiki_index.read_text(encoding="utf-8")
|
||||
assert '"wiki_kind": "overview"' in index_data
|
||||
assert '"wiki_kind": "security"' in index_data
|
||||
|
||||
listed = notes_actions.list_notes(category="wiki")
|
||||
assert listed["success"] is True
|
||||
note_kinds = {note["note_id"]: note.get("wiki_kind") for note in listed["notes"]}
|
||||
assert note_kinds[overview_id] == "overview"
|
||||
assert note_kinds[security_id] == "security"
|
||||
|
||||
deleted = notes_actions.delete_note(note_id=overview_id)
|
||||
assert deleted["success"] is True
|
||||
|
||||
index_after_delete = wiki_index.read_text(encoding="utf-8")
|
||||
assert overview_id not in index_after_delete
|
||||
assert security_id in index_after_delete
|
||||
finally:
|
||||
_reset_notes_state()
|
||||
set_global_tracer(previous_tracer) # type: ignore[arg-type]
|
||||
|
||||
Reference in New Issue
Block a user