From 23c7aa6dc24e7ffb727835afd2b0bc70d2775760 Mon Sep 17 00:00:00 2001 From: "fineas-bot[bot]" <258147136+fineas-bot[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:06:05 -0400 Subject: [PATCH 1/2] Add self-scoped Centaur investigator Adds a self-scoped Centaur investigator for current sandbox/thread debugging, gates broad investigator and raw vlogs access behind operator mode, and updates sandbox prompt guidance away from raw log tools by default. Safety notes: - centaur-investigator self defaults to the current CENTAUR_THREAD_KEY/session scope. - Cross-thread investigate/search requires CENTAUR_INVESTIGATOR_OPERATOR_MODE=1. - Raw vlogs query/fields/field-values/streams requires operator mode. - Returned debug context avoids raw Slack text, message parts, session event payloads, and raw log rows. --- services/sandbox/SYSTEM_PROMPT.md | 23 +- tools/infra/centaur_investigator/cli.py | 40 +++- tools/infra/centaur_investigator/client.py | 226 +++++++++++++++++- .../infra/centaur_investigator/pyproject.toml | 2 +- .../centaur_investigator/tests/test_client.py | 79 ++++++ tools/infra/vlogs/cli.py | 23 ++ 6 files changed, 362 insertions(+), 31 deletions(-) diff --git a/services/sandbox/SYSTEM_PROMPT.md b/services/sandbox/SYSTEM_PROMPT.md index 1b689bc75..e6215a77f 100644 --- a/services/sandbox/SYSTEM_PROMPT.md +++ b/services/sandbox/SYSTEM_PROMPT.md @@ -102,7 +102,7 @@ |websearch search "query" → web research |slack search "query" → Slack search |linear search "query" → Linear issue search -|vlogs query "level:error" → recent service errors +|centaur-investigator self → current sandbox/thread debug state |Tool commands are normal CLIs backed by mounted repo packages. Use direct tool CLIs for tools. |For tool smoke tests, use ` health` as the canonical check. Do not invent ad hoc "test this tool" probes or raw upstream calls unless `health` fails and you are triaging the failure. |For broad tool smoke tests, use the `tool-health-smoke` skill or run its health runner when it is available. @@ -112,23 +112,10 @@ |Do not serialize independent searches across Slack, CRM, notes, web, or observability unless one result is needed to construct the next query. |Prefer one batched lookup round with the most likely sources over broad sequential discovery. If a tool contract is already shown in this prompt, a live skill, or recent ` --help` output, use that contract directly. | -|[Observability — logs + execution data] -|You have full access to Centaur's internal observability via tool CLIs such as `vlogs`. -|If a user says a workflow, alert, or channel post never populated, or asks you to check the code for issues, investigate runtime evidence before proposing redesigns or simplifications: read the relevant code paths, check workflow status, and inspect `vlogs thread_trace` or `vlogs thread_logs` plus any other relevant observability tools first. -|If a user reports an internal tool integration or auth failure, inspect runtime evidence before suggesting secret or permission rewiring: check live tool behavior and `vlogs` evidence to confirm whether secrets resolved and what request failed, then compare the tool's code path with a known-good integration before recommending secret or permission changes. -| -|Logs (VictoriaLogs via `vlogs`): -| vlogs errors → errors across all services (last 1h) -| vlogs errors --service api --start 6h → API errors in last 6h -| vlogs thread_logs --thread-key C0AJ07U8Z1N:1234 → all logs for a specific thread -| vlogs thread_trace --thread-key C0AJ07U8Z1N:1234 → end-to-end timeline across API, sandbox, tools, subagents, and delivery -| vlogs slow_requests --threshold-ms 3000 → requests slower than 3s -| vlogs tool_calls --tool-name websearch --start 24h → tool call history -| vlogs execution_timeline --execution-id exe_123 → full execution trace -| vlogs service_health → error/request counts per service -| vlogs sandbox_activity → sandbox container lifecycle -| vlogs tool_analytics --start 7d → tool usage stats -| vlogs query 'level:error AND event:tool_call_completed' --limit 20 → raw LogsQL +|[Observability — current sandbox only] +|Use `centaur-investigator self` for Centaur runtime debugging. It is scoped to this sandbox's current thread/session by default. +|If a user reports a workflow, alert, channel post, tool integration, or auth failure, inspect repo code and then use `centaur-investigator self --json` for runtime evidence before proposing secret, permission, or design changes. +|Do not run raw `vlogs`, `vmetrics`, Grafana, broad session searches, or arbitrary Postgres queries for other threads/sandboxes unless the sandbox has explicit operator-mode access for that investigation. | [Ethereum Mainnet RPC] |When you need an Ethereum mainnet RPC endpoint and the user has not specified another provider, use the Reth-hosted mainnet endpoints: diff --git a/tools/infra/centaur_investigator/cli.py b/tools/infra/centaur_investigator/cli.py index dc299909b..b614a61ca 100644 --- a/tools/infra/centaur_investigator/cli.py +++ b/tools/infra/centaur_investigator/cli.py @@ -17,18 +17,26 @@ app = typer.Typer( name="centaur-investigator", - help="Investigate Centaur threads and sessions from readonly Postgres.", + help="Investigate the current Centaur sandbox/thread by default.", ) @app.command("health") def health(): - """Assert centaur-investigator connectivity and auth with a safe read-only check.""" + """Assert centaur-investigator connectivity without scanning other threads.""" from .client import _client client = _client() try: - details = client.search_sessions(limit=1) + details = client.self_state(limit=1, include_observability=False) + if details.get("status") == "error" and "requires CENTAUR_THREAD_KEY" in str( + details.get("error") + ): + details = { + "status": "ok", + "scope": "unclaimed", + "note": "health ran outside a claimed session; self-debug activates with CENTAUR_THREAD_KEY", + } if isinstance(details, dict) and details.get("status") == "error": raise RuntimeError( str(details.get("error") or "centaur-investigator health check failed") @@ -91,6 +99,32 @@ def _print_investigation(result: dict[str, Any]) -> None: console.print(table) +@app.command("self") +def self_debug( + limit: int = typer.Option(25, "--limit", "-n", help="Max rows per source."), + observability: bool = typer.Option( + True, + "--observability/--no-observability", + help="Query self-scoped aggregate vlogs/vmetrics metadata.", + ), + window_hours: int = typer.Option(24, "--window-hours", help="Observability lookback."), + logs_limit: int = typer.Option(100, "--logs-limit", help="Max log metadata rows."), + json_output: bool = typer.Option(False, "--json", help="Output raw JSON."), +) -> None: + """Inspect only this sandbox's current thread/session state.""" + result = CentaurInvestigatorClient().self_state( + limit=limit, + include_observability=observability, + window_hours=window_hours, + logs_limit=logs_limit, + ) + _require_ok(result) + if json_output: + _print_json(result) + return + _print_investigation(result) + + @app.command("investigate") def investigate( query: str = typer.Argument(..., help="Natural-language query, Slack link, or thread_key."), diff --git a/tools/infra/centaur_investigator/client.py b/tools/infra/centaur_investigator/client.py index 0982f80f2..201d76c0a 100644 --- a/tools/infra/centaur_investigator/client.py +++ b/tools/infra/centaur_investigator/client.py @@ -18,6 +18,8 @@ CENTAUR_POSTGRES_DSN_ENV = "CENTAUR_POSTGRES_DSN" CENTAUR_INVESTIGATOR_DATABASE_ENV = "CENTAUR_INVESTIGATOR_POSTGRES_DATABASE" +CENTAUR_CURRENT_THREAD_ENV = "CENTAUR_THREAD_KEY" +CENTAUR_INVESTIGATOR_OPERATOR_ENV = "CENTAUR_INVESTIGATOR_OPERATOR_MODE" DEFAULT_POSTGRES_DATABASE = "ai_v2" DEFAULT_LIMIT = 25 MAX_LIMIT = 200 @@ -91,6 +93,19 @@ def _connection_role(connection: dict[str, Any]) -> str | None: return str(row.get("active_role") or row.get("current_user") or "") or None +def _truthy_env(name: str) -> bool: + return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} + + +def _operator_mode_enabled() -> bool: + return _truthy_env(CENTAUR_INVESTIGATOR_OPERATOR_ENV) + + +def _current_thread_key() -> str | None: + value = os.getenv(CENTAUR_CURRENT_THREAD_ENV, "").strip() # noqa: TID251 + return value or None + + def _normalize_ts(value: str | None) -> str | None: if not value: return None @@ -285,6 +300,45 @@ def parse_slack_reference(reference: str) -> dict[str, Any]: } +def _thread_key_equivalents(thread_key: str | None) -> list[str]: + if not thread_key: + return [] + text = thread_key.strip() + if not text: + return [] + values = [text] + parsed = parse_slack_reference(text) + if parsed.get("status") == "ok": + values.extend(str(value) for value in parsed.get("thread_key_candidates") or [] if value) + return _dedupe(values) + + +def _thread_keys_intersect(left: list[str], right: list[str]) -> bool: + left_values: set[str] = set() + for value in left: + left_values.update(_thread_key_equivalents(value)) + right_values: set[str] = set() + for value in right: + right_values.update(_thread_key_equivalents(value)) + return bool(left_values & right_values) + + +def _parsed_for_thread_key(thread_key: str) -> dict[str, Any]: + parsed = parse_slack_reference(thread_key) + if parsed.get("status") == "ok": + return parsed + return { + "status": "ok", + "kind": "thread_key", + "thread_key": thread_key.strip(), + "thread_key_candidates": [thread_key.strip()], + "thread_key_like": None, + "channel_key_like": None, + "channel_id": None, + "thread_ts": None, + } + + def _safe_load_module(module_name: str, path: Path) -> Any | None: if not path.exists(): return None @@ -302,6 +356,63 @@ class CentaurInvestigatorClient: def __init__(self, database_url: str | None = None) -> None: self._database_url = (database_url or _scoped_database_url()).strip() + @staticmethod + def _scope_error(target: str) -> dict[str, Any]: + current = _current_thread_key() + if current: + error = ( + "centaur-investigator is self-scoped by default and cannot inspect " + f"{target!r} from current thread {current!r}. " + f"Set {CENTAUR_INVESTIGATOR_OPERATOR_ENV}=1 only in an operator sandbox." + ) + else: + error = ( + "centaur-investigator is self-scoped by default and requires " + f"{CENTAUR_CURRENT_THREAD_ENV}. Set {CENTAUR_INVESTIGATOR_OPERATOR_ENV}=1 " + "only in an operator sandbox for cross-thread investigation." + ) + return {"status": "error", "error": error} + + @staticmethod + def _authorize_thread_candidates( + candidates: list[str], + *, + target: str, + ) -> dict[str, Any]: + if _operator_mode_enabled(): + return { + "status": "ok", + "scope": "operator", + "current_thread_key": _current_thread_key(), + } + current = _current_thread_key() + if not current: + return CentaurInvestigatorClient._scope_error(target) + if not _thread_keys_intersect([current], candidates): + return CentaurInvestigatorClient._scope_error(target) + return {"status": "ok", "scope": "self", "current_thread_key": current} + + @staticmethod + def _apply_self_scope(parsed: dict[str, Any], authz: dict[str, Any]) -> dict[str, Any]: + if authz.get("scope") != "self": + return parsed + current = str(authz.get("current_thread_key") or "").strip() + candidates = _dedupe( + _thread_key_equivalents(current) + + [ + str(value) + for value in parsed.get("thread_key_candidates") or [] + if value and _thread_keys_intersect([current], [str(value)]) + ] + ) + scoped = dict(parsed) + scoped["thread_key"] = current or scoped.get("thread_key") + scoped["thread_key_candidates"] = candidates or [current] + scoped["thread_key_like"] = None + scoped["channel_key_like"] = None + scoped["scope"] = "self" + return scoped + def _require_database_url(self) -> str: if not self._database_url: raise RuntimeError(f"{CENTAUR_POSTGRES_DSN_ENV} is required") @@ -359,24 +470,26 @@ async def _session_state_async( if not thread_key.strip() or not _KEY_SOURCE_RE.match(thread_key): return {"status": "error", "error": "thread_key must be namespaced"} + parsed = _parsed_for_thread_key(thread_key.strip()) + authz = self._authorize_thread_candidates( + [str(value) for value in parsed.get("thread_key_candidates") or [thread_key.strip()]], + target=thread_key.strip(), + ) + if authz.get("status") != "ok": + return authz + parsed = self._apply_self_scope(parsed, authz) + conn = await self._connect() try: result = await self._collect_state( conn, - parsed={ - "kind": "thread_key", - "thread_key": thread_key.strip(), - "thread_key_candidates": [thread_key.strip()], - "thread_key_like": None, - "channel_key_like": None, - "channel_id": None, - "thread_ts": None, - }, + parsed=parsed, limit=limit, ) finally: await conn.close() + result["scope"] = authz.get("scope") if include_observability: result["observability"] = self._observability( thread_keys=result.get("thread_keys") or [thread_key.strip()], @@ -408,6 +521,84 @@ def session_state( except Exception as exc: return {"status": "error", "error": str(exc)} + def self_state( + self, + limit: int = DEFAULT_LIMIT, + include_observability: bool = True, + window_hours: int = DEFAULT_WINDOW_HOURS, + logs_limit: int = 100, + ) -> dict[str, Any]: + """Inspect only the current sandbox thread.""" + thread_key = _current_thread_key() + if not thread_key: + return self._scope_error("current thread") + result = self.session_state( + thread_key, + limit=limit, + include_observability=include_observability, + window_hours=window_hours, + logs_limit=logs_limit, + ) + if result.get("status") == "error" and not _operator_mode_enabled(): + return self._local_self_state( + thread_key, + postgres_error=str(result.get("error") or "Postgres state unavailable"), + include_observability=include_observability, + window_hours=_clamp(window_hours, minimum=1, maximum=MAX_WINDOW_HOURS), + logs_limit=_clamp(logs_limit, minimum=1, maximum=MAX_LOG_LIMIT), + ) + return result + + def _local_self_state( + self, + thread_key: str, + *, + postgres_error: str, + include_observability: bool, + window_hours: int, + logs_limit: int, + ) -> dict[str, Any]: + parsed = self._apply_self_scope( + _parsed_for_thread_key(thread_key), + {"status": "ok", "scope": "self", "current_thread_key": thread_key}, + ) + thread_keys = parsed.get("thread_key_candidates") or [thread_key] + result: dict[str, Any] = { + "status": "ok", + "scope": "self", + "parsed": parsed, + "thread_keys": thread_keys, + "execution_ids": [], + "analysis": { + "summary": ( + "Current sandbox thread scope resolved. Postgres session state is unavailable." + ), + "findings": ["Resolved current sandbox thread key from CENTAUR_THREAD_KEY."], + "warnings": [postgres_error], + "primary_source": "local_sandbox_context", + }, + "sandbox": { + "hostname": os.getenv("HOSTNAME", ""), + "workload": os.getenv("CENTAUR_WORKLOAD", ""), + "harness_type": os.getenv("CENTAUR_HARNESS_TYPE", ""), + "observability_enabled": os.getenv("CENTAUR_SANDBOX_OBSERVABILITY_ENABLED", ""), + "repo_cache_enabled": os.getenv("CENTAUR_SANDBOX_REPO_CACHE_ENABLED", ""), + "cwd": str(Path.cwd()), + }, + "postgres": { + "status": "unavailable", + "error": postgres_error, + }, + } + if include_observability: + result["observability"] = self._observability( + thread_keys=[str(value) for value in thread_keys if value], + execution_ids=[], + window_hours=window_hours, + logs_limit=logs_limit, + ) + return result + async def _collect_state( self, conn: asyncpg.Connection, @@ -1059,6 +1250,13 @@ async def _investigate_slack_thread_async( parsed = parse_slack_reference(reference) if parsed.get("status") != "ok": return parsed + authz = self._authorize_thread_candidates( + [str(value) for value in parsed.get("thread_key_candidates") or [] if value], + target=reference, + ) + if authz.get("status") != "ok": + return authz + parsed = self._apply_self_scope(parsed, authz) conn = await self._connect() try: @@ -1066,6 +1264,7 @@ async def _investigate_slack_thread_async( finally: await conn.close() + result["scope"] = authz.get("scope") if include_observability: result["observability"] = self._observability( thread_keys=result.get("thread_keys") or parsed.get("thread_key_candidates") or [], @@ -1137,6 +1336,15 @@ async def _search_sessions_async( status: str, limit: int, ) -> dict[str, Any]: + if not _operator_mode_enabled(): + return { + "status": "error", + "error": ( + "search-sessions is an operator-only command because it scans multiple " + f"threads. Use `centaur-investigator self`, or set " + f"{CENTAUR_INVESTIGATOR_OPERATOR_ENV}=1 only in an operator sandbox." + ), + } conn = await self._connect() try: rows = await conn.fetch( diff --git a/tools/infra/centaur_investigator/pyproject.toml b/tools/infra/centaur_investigator/pyproject.toml index a7b5c21d7..517cb2a75 100644 --- a/tools/infra/centaur_investigator/pyproject.toml +++ b/tools/infra/centaur_investigator/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "centaur_investigator" -description = "Investigate Centaur Slack threads and sessions with readonly Postgres access" +description = "Self-scoped Centaur sandbox and thread investigation" version = "0.1.0" requires-python = ">=3.11" dependencies = [ diff --git a/tools/infra/centaur_investigator/tests/test_client.py b/tools/infra/centaur_investigator/tests/test_client.py index 584f27313..567d7af01 100644 --- a/tools/infra/centaur_investigator/tests/test_client.py +++ b/tools/infra/centaur_investigator/tests/test_client.py @@ -235,6 +235,7 @@ async def fake_connect(*args, **kwargs): return fake monkeypatch.setattr(centaur_client.asyncpg, "connect", fake_connect) + monkeypatch.setenv("CENTAUR_INVESTIGATOR_OPERATOR_MODE", "1") result = CentaurInvestigatorClient("postgresql://example").investigate_slack_thread( "https://example.slack.com/archives/C123/p1777910337403889", @@ -309,6 +310,7 @@ def fake_load_module(module_name: str, path: Path): monkeypatch.setattr(centaur_client.asyncpg, "connect", fake_connect) monkeypatch.setattr(centaur_client, "_safe_load_module", fake_load_module) + monkeypatch.setenv("CENTAUR_INVESTIGATOR_OPERATOR_MODE", "1") result = CentaurInvestigatorClient("postgresql://example").investigate_slack_thread( "https://example.slack.com/archives/C123/p1777910337403889", @@ -320,3 +322,80 @@ def fake_load_module(module_name: str, path: Path): assert "thread_trace" not in str(result["observability"]) assert "execution_logs" not in str(result["observability"]) assert "raw_payload" not in str(result) + + +def test_self_state_uses_current_thread_and_disables_broad_like_queries(monkeypatch) -> None: + fake = _FakeConnection() + + async def fake_connect(*args, **kwargs): + return fake + + monkeypatch.setattr(centaur_client.asyncpg, "connect", fake_connect) + monkeypatch.setenv("CENTAUR_THREAD_KEY", "slack:C123:1777910337.403889") + monkeypatch.delenv("CENTAUR_INVESTIGATOR_OPERATOR_MODE", raising=False) + + result = CentaurInvestigatorClient("postgresql://example").self_state( + include_observability=False, + ) + + assert result["status"] == "ok" + assert result["scope"] == "self" + assert result["parsed"]["thread_key"] == "slack:C123:1777910337.403889" + assert result["postgres"]["nearby_sessions"]["rows"] == [] + + fetch_args = [args for _query, args in fake.fetch_calls if args] + assert fetch_args + assert all("%:C123:1777910337.403889" not in args for args in fetch_args) + assert all("%:C123:%" not in args for args in fetch_args) + + +def test_cross_thread_investigation_is_denied_without_operator_mode(monkeypatch) -> None: + async def fake_connect(*args, **kwargs): + raise AssertionError("cross-thread denial should happen before Postgres connects") + + monkeypatch.setattr(centaur_client.asyncpg, "connect", fake_connect) + monkeypatch.setenv("CENTAUR_THREAD_KEY", "slack:C123:1777910337.403889") + monkeypatch.delenv("CENTAUR_INVESTIGATOR_OPERATOR_MODE", raising=False) + + result = CentaurInvestigatorClient("postgresql://example").investigate_slack_thread( + "https://example.slack.com/archives/C999/p1777910337403889", + include_observability=False, + ) + + assert result["status"] == "error" + assert "self-scoped by default" in result["error"] + + +def test_self_state_falls_back_to_local_context_when_postgres_unavailable(monkeypatch) -> None: + async def fake_connect(*args, **kwargs): + raise RuntimeError("permission denied for table sessions") + + monkeypatch.setattr(centaur_client.asyncpg, "connect", fake_connect) + monkeypatch.setattr(centaur_client, "_safe_load_module", lambda *args, **kwargs: None) + monkeypatch.setenv("CENTAUR_THREAD_KEY", "slack:C123:1777910337.403889") + monkeypatch.delenv("CENTAUR_INVESTIGATOR_OPERATOR_MODE", raising=False) + + result = CentaurInvestigatorClient("postgresql://example").self_state( + include_observability=True, + ) + + assert result["status"] == "ok" + assert result["scope"] == "self" + assert result["analysis"]["primary_source"] == "local_sandbox_context" + assert result["postgres"]["status"] == "unavailable" + assert "permission denied" in result["postgres"]["error"] + assert result["observability"]["vlogs"]["status"] == "skipped" + + +def test_search_sessions_requires_operator_mode(monkeypatch) -> None: + async def fake_connect(*args, **kwargs): + raise AssertionError("search-sessions denial should happen before Postgres connects") + + monkeypatch.setattr(centaur_client.asyncpg, "connect", fake_connect) + monkeypatch.setenv("CENTAUR_THREAD_KEY", "slack:C123:1777910337.403889") + monkeypatch.delenv("CENTAUR_INVESTIGATOR_OPERATOR_MODE", raising=False) + + result = CentaurInvestigatorClient("postgresql://example").search_sessions() + + assert result["status"] == "error" + assert "operator-only" in result["error"] diff --git a/tools/infra/vlogs/cli.py b/tools/infra/vlogs/cli.py index a0cc436d0..36e038deb 100644 --- a/tools/infra/vlogs/cli.py +++ b/tools/infra/vlogs/cli.py @@ -1,6 +1,7 @@ """CLI for VictoriaLogs queries.""" import json +import os import typer from dotenv import load_dotenv @@ -12,6 +13,24 @@ app = typer.Typer(name="vlogs", help="VictoriaLogs CLI for LogsQL queries and log exploration") console = Console() +OPERATOR_ENVS = ("CENTAUR_OBSERVABILITY_OPERATOR_MODE", "CENTAUR_INVESTIGATOR_OPERATOR_MODE") + + +def _operator_mode_enabled() -> bool: + return any( + os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} + for name in OPERATOR_ENVS + ) + + +def _require_operator(command: str) -> None: + if _operator_mode_enabled(): + return + console.print( + f"[red]vlogs {command} is operator-only. Use `centaur-investigator self` " + "for current-sandbox debugging.[/red]" + ) + raise typer.Exit(1) def get_client(): @@ -29,6 +48,7 @@ def query_logs( json_output: bool = typer.Option(False, "--json", help="Output as JSON"), ): """Run a LogsQL query against VictoriaLogs.""" + _require_operator("query") client = get_client() result = client.query(query=query, limit=limit, start=start, end=end) @@ -55,6 +75,7 @@ def list_fields( json_output: bool = typer.Option(False, "--json", help="Output as JSON"), ): """List all known field names.""" + _require_operator("fields") client = get_client() result = client.field_names(query=query, start=start, end=end) @@ -76,6 +97,7 @@ def field_values( json_output: bool = typer.Option(False, "--json", help="Output as JSON"), ): """Get all values for a field.""" + _require_operator("field-values") client = get_client() result = client.field_values(field, query=query, limit=limit, start=start, end=end) @@ -95,6 +117,7 @@ def list_streams( json_output: bool = typer.Option(False, "--json", help="Output as JSON"), ): """Find log streams matching a query.""" + _require_operator("streams") client = get_client() result = client.streams(query=query, start=start, end=end) From 449c0422db04810861365bf0e18018917e50ebef Mon Sep 17 00:00:00 2001 From: "fineas-bot[bot]" <258147136+fineas-bot[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:08:13 -0400 Subject: [PATCH 2/2] Remove raw vlogs prompt example Keeps the sandbox prompt consistent with the new self-scoped observability guidance by replacing the remaining raw vlogs example with centaur-investigator self. --- services/sandbox/SYSTEM_PROMPT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sandbox/SYSTEM_PROMPT.md b/services/sandbox/SYSTEM_PROMPT.md index e6215a77f..91196d2fa 100644 --- a/services/sandbox/SYSTEM_PROMPT.md +++ b/services/sandbox/SYSTEM_PROMPT.md @@ -139,7 +139,7 @@ | twitter search ethereum --limit 20 | linear search "bug in auth" | notion search "meeting notes" -| vlogs query 'level:error AND _stream:{service="api"}' --limit 20 +| centaur-investigator self --json [Tool discovery — discover before you call] |IMPORTANT: Before using any unfamiliar tool CLI, run ` --help` to see commands, parameters, and descriptions.