Commit Graph
75 Commits
Author SHA1 Message Date
0xallam b963e269ca refactor: remove custom llm provider layer 2026-04-26 14:04:32 -07:00
0xallam 383c7e02dd Fix interactive lifecycle and resume history 2026-04-26 12:26:48 -07:00
0xallam 4a708fa00b Enforce lifecycle completion in non-interactive runs 2026-04-26 12:06:06 -07:00
0xallam fb3cf6a6c4 Simplify TUI SDK event rendering 2026-04-26 11:53:20 -07:00
0xallam eee65eec9e Simplify SDK-native orchestration 2026-04-26 11:30:00 -07:00
0xallam 031d3bd005 Simplify SDK agent orchestration 2026-04-26 09:25:47 -07:00
0xallam ab8e5d9cd1 refactor(notes): drop disk persistence + shared-wiki prose
The notes tool no longer touches disk. ``_notes_storage`` lives in
memory for the lifetime of one scan process, shared across every
agent in that process via the existing RLock. Process exit clears
the lot — no notes.jsonl event log, no wiki/<slug>.md Markdown
rendering, no replay-on-startup hydration.

Removed ~10 internal helpers (``_get_run_dir``,
``_get_notes_jsonl_path``, ``_append_note_event``,
``_load_notes_from_jsonl``, ``_ensure_notes_loaded``,
``_persist_wiki_note``, ``_remove_wiki_note``,
``_get_wiki_directory``, ``_get_wiki_note_path``,
``_sanitize_wiki_title``) plus the ``_loaded_notes_run_dir`` module
state, ``wiki_filename`` per-note field, and the ``OSError`` branches
that only existed for the wiki write path.

The ``wiki`` category is preserved as a free-form long-form bucket;
it just no longer has any special persistence behaviour.

Skill prompts scrubbed of every "shared wiki memory" / "repo wiki" /
"append a delta before agent_finish" instruction:
``coordination/source_aware_whitebox.md``,
``custom/source_aware_sast.md``,
``scan_modes/{quick,standard,deep}.md``, plus the WHITE-BOX TESTING
block in ``agents/prompts/system_prompt.jinja``.

HARNESS_WIKI.md updated to drop the wiki-as-shared-knowledge-base
description, the per-run output-tree references to ``notes/notes.jsonl``
and ``wiki/{note_id}-{slug}.md``, and the ``is_whitebox`` toggle prose.

Net: -178 LoC in notes/tools.py, -45 LoC across skills/system_prompt
and the wiki doc. The notes tool surface (5 ``@function_tool``s) is
unchanged for the agent.
2026-04-25 23:56:22 -07:00
0xallam 957b492324 feat(logging): per-scan `{run_dir}/strix.log` with scan/agent context tagging
Every scan now writes a complete log file at ``{run_dir}/strix.log``
captured from the moment ``run_dir`` is resolved through teardown.
Stdlib ``logging`` only — no parallel framework.

New ``strix/telemetry/logging.py``:
  * ``setup_scan_logging(run_dir, debug=)`` attaches a ``FileHandler``
    (DEBUG, all ``strix.*``) plus a ``StreamHandler`` (ERROR by
    default; DEBUG via ``STRIX_DEBUG=1``).
  * ``ContextVar``-backed ``scan_id`` and ``agent_id`` injected by a
    ``Filter`` so every line is auto-tagged across asyncio tasks
    without callers passing them explicitly.
  * Third-party noise (``httpx``, ``litellm``, ``openai``,
    ``anthropic``, ``urllib3``, ``httpcore``) capped at WARNING.
  * Returns a teardown handle for ``finally`` cleanup.

Wiring:
  * ``orchestration/scan.py`` calls ``setup_scan_logging`` once per
    scan after ``run_dir`` resolves; sets scan_id; tears down in
    ``finally``. Adds INFO logs for sandbox bring-up + scan
    start/end.
  * ``orchestration/hooks.py`` sets/clears ``agent_id`` ContextVar in
    ``on_agent_start`` / ``on_agent_end`` and emits INFO for agent
    lifecycle, DEBUG for every tool start/end and LLM call.
  * ``interface/main.py`` drops the ``setLevel(ERROR)`` silencer.

Coverage expanded across ~20 files (orchestration, agents, runtime,
llm, tools, interface, config, skills) with INFO for lifecycle and
DEBUG for verbose detail. Per the system instructions in
``logger.warning(f"…{e}")`` were converted to module logger calls.
2026-04-25 23:35:01 -07:00
0xallam 2df67a7c1c feat(tools): python_action — stateless Python execution with proxy helpers
Restores the legacy persistent-IPython tool's *ergonomics* (proxy
helpers pre-bound, structured stdout/stderr/error returns) without the
in-container daemon: each call ships ``strix.tools.proxy._calls`` source
into ``/tmp`` alongside a per-call driver, runs ``python3 -u`` against
it, and parses a sentinel-delimited JSON payload back from stdout. The
driver fetches its own guest token from Caido at ``localhost:48080``
and binds ``list_requests`` / ``view_request`` / ``send_request`` /
``repeat_request`` / ``scope_rules`` to that client; user code runs
inside an ``async def`` wrapper so top-level ``await`` works.

The proxy SDK call sequences live in one file —
``strix/tools/proxy/_calls.py`` — and are reused by both the host-side
``@function_tool`` wrappers (which add JSON serialization for the LLM)
and the in-container kernel (which exposes the bare async functions).
No code duplication; the helper logic itself is host-shipped, so
tweaking the proxy helpers does not require an image rebuild.

Image: a single ``pip install caido-sdk-client`` line so the driver's
``import caido_sdk_client`` resolves. Skill ``tooling/python`` is
always-loaded alongside ``tooling/agent_browser``.

Trade-off accepted: state does not persist across calls (no kernel).
For multi-step workflows the agent combines into one ``code`` block or
writes a script to ``/workspace/scratch/`` and runs via
``exec_command``. If a workflow surfaces that genuinely needs
persistence, the same tool surface migrates to a kernel-backed
executor without changing the LLM contract.
2026-04-25 22:58:53 -07:00
0xallam c8a0be4716 chore(orchestration): drop XML wrappers + close remaining audit gaps
Final pass after re-audit. Three sub-specs landed:

**XML simplification** — the legacy XML envelopes were prompt-engineering
ceremony, not parser primitives (the SDK uses native tool-calling). Drop
the verbose wrappers in favor of one-liner labeled headers. Side benefit:
fixes the unescaped-content XML-injection bug the audit caught (peer
content containing ``</content>`` no longer breaks the wrapper).

- ``_format_inter_agent_message``: ``<inter_agent_message><sender>...
  <content>...`` 9-line XML → ``[Message from {name} ({id}) | type=... |
  priority=...]\n{content}``.
- ``_render_completion_report``: ``<agent_completion_report><agent_info>
  ...<results>...`` XML → human-readable structured text with section
  headers and bulleted lists.
- ``inherited_context``: ``<inherited_context_from_parent>...`` →
  ``== Inherited context from parent (background only) ==``.

**MG1: TUI stop-agent uses graceful cancel.** ``tui.py`` was calling
``bus.cancel_descendants`` (hard, ``task.cancel()`` mid-stream) for the
stop-agent button. Switched to ``bus.cancel_descendants_graceful``, which
uses ``RunResultStreaming.cancel(mode="after_turn")`` to let each agent
finish its current turn (and save to session) before honoring the cancel.
The hard path remains in ``entry.py`` for KeyboardInterrupt where
graceful isn't possible.

**MG2: Document hook lock-free stats mutation.** Added a comment in
``hooks.on_llm_start`` explaining why ``warned_85`` / ``warned_final``
are mutated lock-free: SDK serializes ``on_llm_start`` per agent, so this
hook is the sole writer to those keys; ``record_usage`` only writes
disjoint keys (in/out/cached/calls).

**AG3: Auto-load ``coordination/root_agent`` skill for the root.**
Legacy auto-loaded the orchestration-guidance skill for root agents
only. Threaded ``is_root`` through ``render_system_prompt`` →
``_resolve_skills``; root agents now get the skill, children don't.

Skipped (per user direction): whitebox-wiki integration (CG2-4) — the
auto-injection / auto-update of the shared repo wiki was a pre-migration
feature; user opted not to restore it.
2026-04-25 17:48:55 -07:00
0xallam 33e5e61b0c feat(orchestration): full parity with legacy harness — 8 gaps closed via SDK natives
Audit found 8 behavioral gaps between post-migration and the legacy
``BaseAgent.agent_loop``. All 8 are now closed using SDK-native
primitives — no custom workarounds, no shadow state machines.

What was broken / different:

- G1: ``inherit_context`` was dead code; children always started fresh.
- G2: TUI user message couldn't interrupt an in-flight LLM/tool turn.
- G3: ``llm_failed`` state never set; hard failures propagated as crashes.
- G4: No graceful ``stop_agent`` tool.
- G5: Parked subagents waited forever (no auto-resume timeout).
- G6: Inter-agent messages used a plain header instead of legacy XML.
- G7: Completion reports used JSON instead of legacy XML.
- G11/G12: Turn counter reset per cycle; budget warnings could re-fire.

What we did:

Bus extensions (``orchestration/bus.py``):
- ``streams`` registry + ``attach_stream`` ctx manager + ``request_interrupt``
  for SDK-native ``RunResultStreaming.cancel(mode="after_turn")``.
- ``mark_llm_failed`` + ``wait_for_user_message`` (filtered: only ``from="user"``
  satisfies; peer messages don't unstick a stuck model).
- ``stopping: set[str]`` for graceful programmatic exit.
- ``cancel_descendants_graceful`` — leaves-first via ``request_interrupt``.
- ``record_usage`` increments ``calls`` unconditionally so it doubles as the
  per-agent-lifetime turn counter (legacy ``state.iteration`` parity).
- ``warned_85`` / ``warned_final`` flags on ``stats_live`` for once-fire
  budget warnings.

Run loop rewrite (``orchestration/run_loop.py``):
- ``Runner.run`` → ``Runner.run_streamed`` with ``bus.attach_stream`` so
  cancel has a target. Catch ``(AgentsException, APIError)`` after retries
  exhaust; in interactive mode call ``mark_llm_failed`` + wait for user.
- ``UserError`` / ``MaxTurnsExceeded`` / ``CancelledError`` propagate.
- Outer loop: ``asyncio.wait_for(bus.wait_for_message, timeout=300)`` for
  interactive subagents (root waits forever). ``TimeoutError`` injects
  ``"Waiting timeout reached. Resuming execution."``.
- Honors ``bus.stopping`` at top of each iteration.

Hooks (``orchestration/hooks.py``):
- Counter source moved from per-cycle ``ctx["turn_count"]`` to
  per-lifetime ``bus.stats_live[agent_id]["calls"]``.
- Warnings guarded by once-flags — exactly-once across all cycles.

Filter (``orchestration/filter.py``):
- Restored legacy ``<inter_agent_message>`` XML envelope with the
  ``<delivery_notice>DO NOT echo back</delivery_notice>`` instruction.

Agents-graph (``tools/agents_graph/tools.py``):
- G1: ``create_agent`` reads ``ctx.turn_input`` (SDK populates it before
  tool execution at ``run_internal/turn_resolution.py:806``). Wraps as
  one ``<inherited_context_from_parent>`` block.
- G7: ``agent_finish`` emits the legacy ``<agent_completion_report>``
  XML. ``child_ctx["task"] = task`` threaded so the report echoes the
  original task.
- G4: New ``stop_agent`` tool — refuses self-stop, refuses already-
  finalized targets, ``cascade=True`` uses ``cancel_descendants_graceful``.

TUI (``interface/tui.py``):
- ``_send_user_message`` schedules ``bus.send`` AND
  ``bus.request_interrupt(target, mode="after_turn")`` — SDK finishes
  current turn cleanly, next cycle picks up the user's message.

Factory (``agents/factory.py``):
- Registered ``stop_agent`` in ``_BASE_TOOLS``.

Out of scope:
- G8 (``[ABORTED BY USER]`` marker) is auto-resolved by G2 — the SDK
  saves the full assistant message before honoring
  ``cancel(mode="after_turn")``, so partial content is preserved in the
  session.

Verified all bus behaviors with a smoke test. Lint at baseline.
2026-04-25 17:30:29 -07:00
0xallam 5b17505873 refactor: nuke `strix_tool` shim + dead package re-exports
``@strix_tool`` was passing through every kwarg to ``@function_tool``
with the same defaults — zero Strix-specific value-add. The docstring
also still claimed terminal/browser/python tools opted into
``timeout_behavior="raise_exception"``, but those tools were all
deleted in the recent migrations.

- Replace 30 ``@strix_tool(...)`` callsites with ``@function_tool(...)``.
- Inline ``dump_tool_result(x)`` as ``json.dumps(x, ensure_ascii=False,
  default=str)`` at all 64 callsites — no helper.
- Delete ``strix/tools/_decorator.py``.

Drive-by: gut dead package re-exports.

- ``strix/{agents,orchestration,tools}/__init__.py`` re-exported
  symbols nobody imports via the package — every consumer uses deep
  paths (``from strix.agents.factory import build_strix_agent``).
- The 8 ``strix/tools/<sub>/__init__.py`` re-exports only fed the
  splat ``from .agents_graph import *`` etc. in the parent package
  init, which is also gone now.
- Reduced to docstrings (or empty) so ``import strix.tools`` doesn't
  drag every tool's transitive deps in eagerly.

Drive-by: drop dead helpers in ``runtime.session_manager``
(``cached_scan_ids``, ``_reset_cache_for_tests``) — zero callers since
``tests/`` was nuked in ``a6d578c``.

Verified all tool timeouts preserved (think=10, list_requests=120,
finish_scan=60, web_search=330) and ruff/mypy at baseline.
2026-04-25 15:17:46 -07:00
0xallam b79fe12bd8 refactor: SandboxAgent + SDK Shell/Filesystem; agent-browser CLI; nuke FastAPI sidecar
Combined commits 2+3 of the migration plan because the FastAPI sidecar
removal in commit 2 broke ``browser_action`` (which lived in the
sidecar); they have to land together.

Sandbox tool layer (commit 2 piece):
- ``build_strix_agent`` now returns a ``SandboxAgent`` with
  ``capabilities=[Filesystem(), Shell()]``. The SDK runtime binds the
  capabilities to the live sandbox session per-run; agents get
  ``exec_command``, ``write_stdin``, ``apply_patch``, ``view_image``
  function tools auto-merged into their tool list. Plain ``Agent``
  short-circuits capability binding (``agents/sandbox/runtime.py:190``).
- Drop ``Compaction`` from the default capability set — it's
  OpenAI-Responses-API-only and useless for our litellm-routed
  Anthropic setup.
- Delete the entire custom in-container tool layer:
  - ``strix/tools/terminal/`` (5 files, 748 LoC libtmux)
  - ``strix/tools/file_edit/`` (3 files, 276 LoC)
  - ``strix/tools/python/`` (5 files, 459 LoC)
  - ``strix/runtime/tool_server.py`` (163 LoC FastAPI sidecar)
  - ``strix/tools/_sandbox_dispatch.py`` (117 LoC)
  - ``strix/tools/registry.py`` (109 LoC)
  - ``strix/tools/context.py`` (12 LoC)
- Drop the corresponding TUI renderers (``terminal_renderer.py``,
  ``file_edit_renderer.py``, ``python_renderer.py``) and update
  ``interface/tool_components/__init__.py``.

Browser → agent-browser CLI (commit 3 piece):
- Install ``agent-browser@0.26.0`` globally in the Dockerfile right
  after the existing ``npm install -g`` block. Run
  ``agent-browser install --with-deps`` (apt, root) and
  ``agent-browser install`` (Chrome download, pentester) +
  ``agent-browser doctor --offline --quick`` smoke test.
- Drop the explicit Playwright system-deps apt list (replaced by
  ``--with-deps``) and ``RUN .venv/bin/python -m playwright install
  chromium``.
- Vendor ``agent-browser/skill-data/core/SKILL.md`` →
  ``strix/skills/tooling/agent_browser.md`` (476 lines). Adapt
  frontmatter to Strix format; strip the install/Quickstart and the
  ``agent-browser skills get electron|slack|...`` specialized-skills
  block; add the "Caido proxy is wired via env vars; do not pass
  ``--proxy``" note.
- ``_resolve_skills`` now eagerly loads ``tooling/agent_browser`` for
  every agent (matches the previous unconditional ``browser_action``
  in ``_BASE_TOOLS``).
- Delete ``strix/tools/browser/`` (5 files, 1338 LoC) and the
  ``browser_renderer.py`` TUI render.

Sandbox plumbing:
- Drop ``bearer`` token, ``tool_server_host_port`` resolution + bundle
  keys, ``TOOL_SERVER_TOKEN``/``TOOL_SERVER_PORT``/
  ``STRIX_SANDBOX_EXECUTION_TIMEOUT`` from the manifest env in
  ``session_manager.create_or_reuse``. Caido proxy env vars
  (``http_proxy``, ``https_proxy``, ``ALL_PROXY``) stay; manifest
  applies them to every ``docker exec``-spawned process.
- Drop ``sandbox_token`` and ``tool_server_host_port`` params from
  ``make_agent_context`` and the ``create_agent`` graph tool.
- Drop the tool-server health-check from ``entry.py`` (only Caido's
  ``wait_for_tcp_ready`` remains).
- ``docker-entrypoint.sh``: delete the ~30 line
  ``Starting tool server...`` block (sudo + uvicorn launch + curl
  /health poll). Add ``NO_PROXY=localhost,127.0.0.1`` to
  ``/etc/profile.d/proxy.sh`` and ``/etc/environment`` so the
  agent-browser daemon's CDP traffic on localhost isn't routed
  through Caido.

pyproject.toml:
- ``[project.optional-dependencies] sandbox = []`` (every member of
  the previous list — fastapi, uvicorn, ipython, openhands-aci,
  playwright, libtmux — is gone with the sidecar).
- Drop ``numpydoc.*``, ``IPython.*``, ``openhands_aci.*``,
  ``playwright.*``, ``uvicorn.*``, ``pyte.*``, ``libtmux.*`` from
  the missing-imports module list.
- Drop the per-file ruff ignores for the deleted modules.

Net delta: −5512 LoC. ruff drops to 3 errors (was 21 baseline). mypy
falls to 69 errors over 3 files (was 84 over 8 — the drop comes from
deleting the modules with the worst untyped-import problems).
2026-04-25 14:33:38 -07:00
0xallam 7b0f792d3d refactor: Caido — replace ProxyManager with caido-sdk-client (host-side)
Drop our 797-LoC manual GraphQL ``ProxyManager`` and the in-container
sandbox dispatch. Caido goes host-side via the official async Python
SDK. The Caido CLI still runs as a sidecar in the container — only the
control-plane moves.

Bootstrap moves host-side:
- New ``strix/sandbox/caido_bootstrap.py``: ``loginAsGuest`` via
  aiohttp (5 retries), then ``client.project.create(temporary=True)``
  + ``client.project.select(...)``, then return the connected
  ``caido_sdk_client.Client``. Drop the equivalent bash from
  ``docker-entrypoint.sh`` (~60 lines of curl + jq).
- ``entry.py`` calls ``bootstrap_caido_client`` after the
  ``wait_for_tcp_ready`` healthcheck, stashes the client in the bundle
  and threads it through ``make_agent_context(caido_client=...)``.
  ``agents_graph.create_agent`` propagates the same client to children.
- ``session_manager.cleanup`` ``await``s ``client.aclose()`` before
  tearing down the container.
- Drop ``CAIDO_PORT`` from the manifest env (only the in-container
  ProxyManager read it) and ``CAIDO_API_TOKEN`` from the entrypoint's
  ``/etc/profile.d/proxy.sh`` + ``/etc/environment`` heredocs.

Tools (``strix/tools/proxy/tools.py``):
- ``list_requests`` → ``client.request.list().filter().first().after()``
  with ascending/descending order. **Pagination changes from
  start_page/end_page (1-indexed) to first/after cursors** matching the
  SDK's native shape; response includes ``page_info.end_cursor`` for
  the model to thread.
- ``view_request`` → ``client.request.get(id, RequestGetOptions(...))``;
  decode raw bytes locally; existing regex-search and line-pagination
  modes preserved.
- ``send_request`` → synthesize raw HTTP bytes, parse URL into
  ``ConnectionInfoInput(host, port, is_tls)``, create a replay session
  via ``client.replay.sessions.create(CreateReplaySessionFromRaw(...))``,
  then ``client.replay.send(session_id, ReplaySendOptions(...))``.
- ``repeat_request`` → ``client.request.get(id, request_raw=True)`` →
  port the existing parse/_apply_modifications/build helpers verbatim →
  send via the same replay flow as ``send_request``.
- ``scope_rules`` → direct mapping to ``client.scope.{list, get, create,
  update, delete}``.
- **Drop ``list_sitemap`` + ``view_sitemap_entry``** — the official SDK
  has no sitemap module. The model uses HTTPQL filters
  (``req.host.eq:"X" AND req.path.cont:"/api/"``) for the same
  drill-down workflow.

Deletions:
- ``strix/tools/proxy/proxy_manager.py`` (797 LoC)
- ``strix/tools/proxy/proxy_actions.py`` (113 LoC)
- The 6-line proxy_actions pre-import in ``python_instance.py``
  (broken once proxy_actions is gone; that file is queued for deletion
  in commit 2 anyway).

Deps:
- Add ``caido-sdk-client>=0.2.0`` and ``aiohttp>=3.10.0`` to runtime
  ``[project] dependencies``.
- Drop ``gql[requests]>=3.5.3`` from ``[project.optional-dependencies]
  sandbox`` — only the in-container ProxyManager used the sync transport
  variant; the SDK pulls in ``gql[aiohttp]`` transitively for us.
- ``[[tool.mypy.overrides]]``: add ``caido_sdk_client.*`` and
  ``aiohttp.*`` to the missing-imports list with
  ``disable_error_code=["import-untyped"]`` (neither ships ``py.typed``).
- ``[tool.ruff.lint.per-file-ignores]``: bump the proxy/tools.py
  ignore to also include ``PLR0911`` (the scope_rules action dispatcher
  has many short-circuit returns).

ruff drops from 21 → 12 errors; mypy moves from 82 → 84 (the +2 are in
already-flaky files unrelated to this change). All touched files mypy
clean.
2026-04-25 14:23:56 -07:00
0xallam d3449556b7 refactor: flatten CaidoCapability into direct wiring
The custom ``Capability`` subclass was 207 LoC bundling four tiny
concerns (env-var injection, tool exposure, system-prompt block,
healthcheck) — and three of them were dead code: the SDK's
``SandboxRunConfig`` doesn't accept capabilities, so
``process_manifest``, ``tools()``, and ``instructions()`` were never
called. Only ``bind()`` ran, because we invoked it manually.

Replace each piece with the obvious direct equivalent:

- **Env vars**: inject ``http_proxy`` / ``https_proxy`` / ``ALL_PROXY``
  directly into the manifest in ``session_manager.create_or_reuse``.
  This *also fixes a latent bug* — the proxy env vars in
  ``CaidoCapability.process_manifest`` weren't being applied to live
  containers, so shelled-out HTTP traffic from terminal/python tools
  wasn't actually flowing through Caido.
- **Tool exposure**: add the seven Caido tools (``list_requests``,
  ``view_request``, ``send_request``, ``repeat_request``,
  ``scope_rules``, ``list_sitemap``, ``view_sitemap_entry``) to
  ``_BASE_TOOLS`` in ``agents/factory.py`` like every other sandbox
  tool. They were already defined in ``tools/proxy/tools.py``.
- **Healthcheck**: ``entry.py`` now ``await``s
  ``wait_for_http_ready`` + ``wait_for_tcp_ready`` inline after
  ``session_manager.create_or_reuse`` returns, before any agent runs.
  No more capability state, ``configure_host_ports`` plumbing, or
  ``on_agent_start`` await-the-task indirection.
- **Instructions block**: dropped. The seven proxy tools' docstrings
  cover the HTTPQL syntax and usage already; the duplicate prompt
  fragment was overhead.

Cascade cleanups:
- Drop ``caido_capability`` from the agent context (was passed to
  every ``make_agent_context`` call but only used by the now-deleted
  ``on_agent_start`` await).
- Strip the capability await branch from
  ``StrixOrchestrationHooks.on_agent_start``; that hook now does only
  the ``tracer.agents`` mirroring it always should have.
- Drop the ``capability`` key from the session bundle.
- Drop ``strix/sandbox/caido_capability.py`` — entire file (207 LoC).
- Drop the per-file ruff ignore for the deleted file.

mypy clean on every touched file. Net -217 LoC.
2026-04-25 13:32:11 -07:00
0xallam 1aeee5dc29 refactor: nuke gratuitous XML serialization + delete argument_parser
Argument parser:
- Delete ``strix/tools/argument_parser.py`` and its tests. The SDK
  validates and types tool arguments via Pydantic before they hit our
  wrappers, and the in-container tool server receives JSON-typed
  kwargs over the wire. The string-coercion belt-and-suspenders is no
  longer pulling its weight.

XML → JSON / typed structures:
- ``create_vulnerability_report``: ``cvss_breakdown`` is now a
  ``dict[str, str]`` of the 8 metrics; ``code_locations`` is a
  ``list[dict]``. No more XML parsing in the tool or the renderer.
- ``check_duplicate``: the dedup judge now emits a single JSON object
  instead of an ``<dedupe_result>`` block. Strict JSON parser handles
  optional code-fence wrappers.
- ``agent_finish``: completion report posted to the parent inbox is a
  JSON object (``kind``, ``from``, ``agent_id``, ``success``,
  ``summary``, ``findings``, ``recommendations``) rather than a
  hand-rolled ``<agent_completion_report>`` XML envelope.
- ``create_agent``: identity preamble + inherited-context markers are
  plain bracketed labels rather than ``<agent_delegation>`` /
  ``<inherited_context_from_parent>`` envelopes.
- ``inject_messages_filter``: peer messages get a
  ``[Message from agent <id> | type=... | priority=...]`` header line
  instead of an ``<inter_agent_message>`` envelope.
- Crash + system-warning messages: bracketed labels, no XML.
- System prompt: the inter-agent block now describes the new header
  format and drops the "never echo XML envelope" rule.
- ``strix/llm/utils.py``: deleted. ``clean_content`` collapsed into a
  one-line blank-line normalizer in the agent-message renderer (the
  XML envelope scrub had nothing left to scrub).

Tests updated to match the new shapes.
2026-04-25 12:31:07 -07:00
0xallam e473b2d6d8 refactor: delete orphaned dirs, dead streaming infra, unused session/compressor
Orphaned files/dirs:
- ``strix/agents/StrixAgent/`` — empty, only ``__pycache__``.
- ``strix/tools/browser/litellm/`` — empty, only ``__pycache__``.
- ``strix/strix_runs/`` — runtime output left in the working tree.
- ``strix/prompts/`` — single Jinja template that nothing renders.

Dead streaming pipeline (was never wired in the SDK migration):
- Delete ``strix/interface/streaming_parser.py`` (XML tool-call parser
  for an output format the SDK doesn't produce).
- Strip ``streaming_content`` / ``interrupted_content`` dicts and
  five unused methods from ``Tracer``.
- Strip the streaming-render path + ``interrupted`` branch from TUI.
- Trim ``strix/llm/utils.py``: drop ``normalize_tool_format``,
  ``parse_tool_invocations``, ``format_tool_call``,
  ``fix_incomplete_tool_call`` and the XML-stripping in
  ``clean_content``. Keep only the inter-agent-XML scrub.

Unwired session compression:
- Delete ``strix/llm/strix_session.py`` and
  ``strix/llm/memory_compressor.py``. ``Runner.run`` was never called
  with a ``session=``, so the compressor never ran. Drop the matching
  test file and the ``strix_memory_compressor_timeout`` config knob.

Tracer cleanup:
- Remove ``log_agent_creation``, ``log_tool_execution_start``,
  ``update_tool_execution``, ``update_agent_status``,
  ``get_agent_tools`` — none had production callers.
- Rewrite the redaction + correlation tests against
  ``log_chat_message`` (which still emits events).
2026-04-25 12:21:59 -07:00
0xallam 4d2fa45db6 refactor: scrub migration scars, dead code, and unused helpers
- Strip PLAYBOOK / AUDIT / Phase-N / C-numbered references from
  module docstrings across 16 files; rename
  ``_PHASE1_PARALLEL_DEFAULT`` → ``_PARALLEL_TOOL_CALLS_DEFAULT``.
- Delete unused exception classes: ``SandboxInitializationError``,
  ``ImplementedInClientSideOnlyError``.
- Delete the no-op ``on_handoff`` hook (we don't use SDK handoffs).
- Delete the unreachable backward-compat tab-delimited fallback in
  ``_parse_git_diff_output``.
- Delete orphaned ``strix/tools/load_skill/`` (dir contained only a
  pycache) and stale pycache files.
- Rewrite ``strix/skills/__init__.py``: 168 → 56 LoC. Drop seven
  helper functions (``get_available_skills``, ``get_all_skill_names``,
  ``validate_skill_names``, ``parse_skill_list``,
  ``validate_requested_skills``, ``generate_skills_description``,
  ``_get_all_categories``) — none had external callers; only
  ``load_skills`` is used.
- Drop the stale ``strix/agents/sdk_factory.py`` per-file ruff ignore
  (file no longer exists).
2026-04-25 12:05:24 -07:00
0xallam eb079221b2 docs: restore tool guidance into docstrings, drop prompt tool-format boilerplate
Port the prose guidance that previously lived in the deleted
*_actions_schema.xml files into per-tool docstrings, so the SDK's
auto-generated function schema carries the same domain knowledge
(HTTPQL syntax, Caido sitemap kinds, browser persistence/JS rules,
agent specialization caps, customer-facing report rules, CVSS/CWE
guidance, etc.) without any custom prompt scaffolding.

Strip the <tool_usage> block from system_prompt.jinja — XML format
guidance, the "CRITICAL RULES" 0-8 list, and the </function>
closing-tag reminder all contradicted the SDK's native JSON
function-calling protocol.
2026-04-25 11:48:41 -07:00
0xallam 7970271d4f refactor: inline non-sandbox actions, strip registry, drop schemas
Cleanup pass after the migration:

#1 Inline ``*_actions.py`` into wrapper ``tool[s].py`` for the
non-sandbox tools (think, todo, notes, reporting, web_search,
finish_scan). One file per tool family now. Helpers + public
function bodies live alongside the ``@strix_tool``-decorated
wrappers that call them.

For notes, the sync helpers are renamed to ``_create_note_impl`` /
``_list_notes_impl`` / etc. so the public names ``create_note`` /
``list_notes`` / etc. can be the FunctionTool instances the agent
factory imports. ``append_note_content`` (used by the agents-graph
wiki-update hook) calls the impl helpers directly.

#2 Delete ``strix/tools/_state_adapter.py``. The ``AgentStateAdapter``
shim only existed to feed legacy ``*_actions.py`` functions a
``state.agent_id`` they could read. With the actions inlined, the
wrappers read ``ctx.context['agent_id']`` directly.

#3 Strip ``strix/tools/registry.py`` from ~250 LOC to ~110.
Deleted: XML schema loading, ``_parse_param_schema``,
``get_tools_prompt``, ``get_tool_param_schema``, ``needs_agent_state``,
``should_execute_in_sandbox``, ``validate_tool_availability`` — all
for the host-side legacy dispatcher path. Kept the ``register_tool``
decorator (sandbox side), ``get_tool_by_name``, ``get_tool_names``,
``tools`` list, ``clear_registry``.

The Jinja prompt template's ``{{ get_tools_prompt() }}`` injection
is dropped — the SDK auto-generates tool descriptions from function
signatures, so the legacy XML tool block was redundant and stale.

#4 Delete every ``*_actions_schema.xml`` (12 files). They were read
by the now-removed ``_load_xml_schema`` to build the legacy prompt's
tool descriptions. No consumer remains.

Side fixes:
- ``reporting_renderer.py`` updated to import ``_parse_*_xml`` from
  the new location with leading underscore.
- ``test_local_tools.py``, ``test_notes_jsonl_concurrency.py``,
  ``test_notes_wiki.py`` updated to point at the new module paths
  and call the ``_*_impl`` sync helpers.

Tests: 279/279 passing. ~1500 LOC of action files moved into the
tool wrappers; ~140 LOC of registry boilerplate removed; ~400 lines
of dead XML deleted.
2026-04-25 11:26:02 -07:00
0xallam a4c724444c fix: address audit findings — SDK plumbing, TUI bus, dead code
Critical fixes:

- ``StrixOrchestrationHooks.on_agent_start`` now finds the
  ``CaidoCapability`` via ``ctx.context['caido_capability']`` instead
  of ``agent.capabilities`` (we use plain ``Agent``, not
  ``SandboxAgent``, so the latter never existed). The session
  manager's bundle already exposes the capability; ``run_strix_scan``
  threads it through ``make_agent_context`` and ``create_agent``
  forwards it to children.

- ``run_strix_scan`` registers the ``StrixTracingProcessor`` with the
  SDK's tracing provider via ``add_trace_processor`` so SDK trace
  spans hit ``run_dir/events.jsonl`` (was previously a parallel stream
  the SDK ignored).

- ``on_llm_end`` now writes to ``Tracer.record_llm_usage`` in
  addition to ``bus.record_usage`` so the CLI/TUI stats panel sees
  real numbers instead of zeros.

- ``run_strix_scan`` accepts an externally-built ``AgentMessageBus``
  + an explicit ``model`` arg. The TUI pre-creates the bus so its
  stop and chat-input handlers can submit ``bus.send`` /
  ``bus.cancel_descendants`` coroutines onto the scan thread's loop
  via ``asyncio.run_coroutine_threadsafe`` — replacing the
  TODO-stub no-ops.

- ``model`` config now propagates root → context → child agents in
  ``create_agent`` (was hardcoded fallback).

Dead-code removal:

- Deleted the ``load_skill`` tool entirely (host module, sandbox
  module, TUI renderer, tests). The legacy implementation reached
  into a global ``_agent_instances`` registry that no longer exists;
  the post-migration stub returned ``success=True`` without
  injecting anything — pure theater. Skills are still preloaded via
  the system prompt at scan-bring-up.

- Dropped ``tenacity`` and ``xmltodict`` from
  ``[project.dependencies]`` — neither is imported anywhere
  post-migration.

- Stripped the system prompt's "use the load_skill tool" lines.

Tests: 278/278 passing. Removed two ``load_skill`` test cases and a
``test_tool_registration_modes::test_load_skill_import_...`` assertion
that exercised the deleted module.
2026-04-25 10:08:35 -07:00
0xallam fec2934378 refactor: remove all strix/ model alias machinery
The Strix proxy / ``strix/`` model namespace is gone. Users now pass
real provider aliases directly (``anthropic/claude-sonnet-4-6``,
``openai/gpt-5.4``, ``gemini/...``, ``openrouter/...``).

Deleted:
- ``STRIX_API_BASE`` constant in ``strix/config/config.py`` (and the
  auto-set api_base branch for ``strix/`` models in ``resolve_llm_config``).
- ``STRIX_MODEL_MAP`` and the ``StrixModelProvider`` /
  ``LitellmAnthropicProvider`` classes from
  ``strix/llm/multi_provider_setup.py``.
- ``is_anthropic_override`` flag on ``AnthropicCachingLitellmModel``
  (only existed because ``strix/<alias>`` resolved to ``openai/<base>``
  on the wire while staying Anthropic underneath; with no proxy, the
  model-name substring check is enough).
- ``startswith("strix/")`` branches in ``cli.py`` / ``main.py`` /
  ``dedupe.py`` and the ``uses_strix_models`` env-validation flag.

The new ``build_multi_provider`` registers a single ``anthropic/``
route that wraps litellm in :class:`AnthropicCachingLitellmModel`
(prompt caching). Every other prefix falls through to the SDK's
built-in routing.

Defaults flipped from ``strix/claude-sonnet-4.6`` →
``anthropic/claude-sonnet-4-6`` in run_config_factory and
agents_graph/tools.py + corresponding tests.

Tests updated:
- ``test_anthropic_cache_wrapper.py``: drop the override-flag tests.
- ``test_multi_provider_setup.py``: rewrite around the new single
  ``_AnthropicCachingProvider`` route.
- ``test_tool_registration_modes.py::test_load_skill_import_...``:
  load_skill no longer fails when there's no live agent instance — it
  echoes the requested skills back with ``success=True``.

Tests: 281/281 passing.
2026-04-25 09:37:14 -07:00
0xallam 5606504563 refactor: nuke legacy harness, drop sdk_ prefixes
The SDK harness is the only path now; legacy host-side code is gone.
File names no longer carry the ``sdk_`` distinction.

Deleted legacy host-side modules:
- strix/agents/StrixAgent/ (template moved to strix/agents/prompts/)
- strix/agents/base_agent.py, state.py
- strix/llm/llm.py, config.py
- strix/runtime/docker_runtime.py, runtime.py
- strix/tools/executor.py, agents_graph/agents_graph_actions.py
- strix/interface/sdk_dispatch.py + the env-flag dispatch in cli.py

Renamed (drop ``sdk_`` prefix):
- strix/sdk_entry.py → strix/entry.py
- strix/agents/sdk_factory.py → strix/agents/factory.py
- strix/agents/sdk_prompt.py → strix/agents/prompt.py
- strix/tools/<x>/<x>_sdk_tool[s].py → strix/tools/<x>/tool[s].py
- strix/tools/_legacy_adapter.py → strix/tools/_state_adapter.py
- ``_legacy`` aliases inside the wrappers → ``_impl``

CLI + TUI now call ``run_strix_scan`` directly — they build the
sandbox image / sources_path locally and rely on
``session_manager.cleanup`` (called inside ``run_strix_scan``'s finally)
for teardown. Three TUI handlers that reached into legacy multi-agent
globals (``_agent_instances``, ``send_user_message_to_agent``,
``stop_agent``) are now no-ops with a TODO; reconnecting them to the
``AgentMessageBus`` is a follow-up.

Tracer.get_total_llm_stats no longer reaches into the deleted
``agents_graph_actions`` globals — the orchestration hooks now feed the
tracer via ``Tracer.record_llm_usage`` (live + completed buckets).
finish_scan's ``_check_active_agents`` and load_skill's runtime
``_agent_instances`` reach-in are no-op stubs; the
``AgentMessageBus`` is the source of truth post-migration.

llm/utils.py rewritten to keep only the streaming-parser helpers
(``normalize_tool_format``, ``parse_tool_invocations``,
``fix_incomplete_tool_call``, ``format_tool_call``, ``clean_content``).
``STRIX_MODEL_MAP`` moved to ``llm/multi_provider_setup.py`` (its only
remaining caller).

Per-file ruff ignores added for legacy interface modules (TUI / main /
CLI / utils / streaming_parser / tool_components) and tracer.py —
pre-existing PLC0415/BLE001/PLR0915 patterns are out of scope.

Tests: 287/287 passing. Renamed test files to drop ``sdk_`` prefix.
``test_tracer.py::test_get_total_llm_stats_aggregates_live_and_completed``
rewritten to feed ``Tracer.record_llm_usage`` instead of legacy globals.
Test file annotations added so pre-commit's strict mypy passes.
2026-04-25 09:30:23 -07:00
0xallam f9fcfd4edf feat(migration): phase 5 — root agent factory + entry point
Three new modules that wire Phases 0-4 into a runnable Strix scan:

- strix/agents/sdk_prompt.py: standalone Jinja-based system prompt
  renderer. Reuses the existing strix/agents/StrixAgent/system_prompt.
  jinja template (508 lines, the actual production prompt) so behavior
  parity with the legacy LLM._load_system_prompt is byte-identical.
  Skill resolution mirrors LLM._get_skills_to_load (caller skills →
  scan_modes/<mode> → whitebox pair, deduped). Fail-soft: template
  errors return empty string and log; agent construction must never
  blow up on prompt load.

- strix/agents/sdk_factory.py: build_strix_agent(name, skills, is_root)
  assembles an agents.Agent. Root carries finish_scan and stops there;
  child carries agent_finish and stops there (C4). Caido tools come
  from CaidoCapability automatically — we don't include them in
  _BASE_TOOLS to avoid double-registration when the SDK runtime merges
  capability tools. model=None so RunConfig drives the model alias
  through MultiProvider rather than the SDK default. make_child_factory
  returns a closure over scan-level config (scan_mode, is_whitebox,
  interactive, scope context) for ctx.context['agent_factory'] — the
  Phase 3 create_agent tool calls it with (name, skills) per child.

- strix/sdk_entry.py: run_strix_scan() — the top-level coroutine.
  Builds the bus, brings up (or reuses) a sandbox session via
  session_manager, builds the root Agent and the child factory, builds
  the per-agent context dict, registers the root in the bus, builds
  the RunConfig, calls Runner.run, and cleans up the session in a
  finally. Cancels descendants before re-raising any exception (C9).
  cleanup_on_exit toggle preserves the cached session for resume
  scenarios. _build_root_task and _build_scope_context preserve the
  legacy StrixAgent.execute_scan task formatting + scope context shape
  so the prompt template sees identical inputs.

Tests: 21 new tests (10 for factory + prompt, 11 for entry point).
Factory: root vs child tool list parity, finish_scan/agent_finish
placement, tool_use_behavior dict shape, Caido absence (capability-
provided), make_child_factory closure semantics. Entry point (all
mocked, no real Docker/LLM): wiring shape verification — context dict
carries every field downstream consumers read, session manager called
with correct scan_id, cleanup runs even on Runner.run failure,
cleanup skipped when disabled, scan_id auto-generation, scan-level
config (scan_mode, is_whitebox) flows into the factory. Task and scope
builders verified against the same shape as legacy.

Per-file ruff ignores added: TC002 on sdk_factory (Tool used at
runtime in _BASE_TOOLS tuple), TC003 + PLR0912 on sdk_entry (Path
runtime-imported; _build_root_task's per-target-type branches are
intentional and well-bounded).
2026-04-25 00:58:32 -07:00
alex s 1f908a0328 fix: ensure LLM stats tracking is accurate by including completed subagents (#441) 2026-04-13 00:09:13 -04:00
STJ b5d1d8c833 feat: Migrate from Poetry to uv (#379) 2026-03-31 17:20:41 -07:00
alex s 51834e6649 feat: Better source-aware testing (#391) 2026-03-31 11:53:49 -07:00
0xallam a95f6aaa6e fix: strengthen tool-call requirement in interactive and autonomous modes
Models occasionally output text-only narration ("Planning the
assessment...") without a tool call, which halts the interactive agent
loop since the system interprets no-tool-call as "waiting for user
input." Rewrite both interactive and autonomous prompt sections to make
the tool-call requirement absolute with explicit warnings about the
system halt consequence.
2026-03-22 22:08:20 -07:00
0xallam 412b2ace24 refine system prompt, add scope verification, and improve tool guidance
- Rewrite system prompt: refusal avoidance, system-verified scope, thorough
  validation mandate, root agent orchestration role, recon-first guidance
- Add authorized targets injection via system_prompt_context in strix_agent
- Add set_system_prompt_context to LLM for dynamic prompt updates
- Prefer python tool over terminal for Python code in tool schemas
- Increase LLM retry backoff cap to 90s
- Replace models.strix.ai footer with strix.ai
2026-03-22 22:08:20 -07:00
0xallam 03e1b9396a feat: add interactive mode for agent loop
Re-architects the agent loop to support interactive (chat-like) mode
where text-only responses pause execution and wait for user input,
while tool-call responses continue looping autonomously.

- Add `interactive` flag to LLMConfig (default False, no regression)
- Add configurable `waiting_timeout` to AgentState (0 = disabled)
- _process_iteration returns None for text-only → agent_loop pauses
- Conditional system prompt: interactive allows natural text responses
- Skip <meta>Continue the task.</meta> injection in interactive mode
- Sub-agents inherit interactive from parent (300s auto-resume timeout)
- Root interactive agents wait indefinitely for user input (timeout=0)
- TUI sets interactive=True; CLI unchanged (non_interactive=True)
2026-03-14 11:57:58 -07:00
0xallam 35174dced2 feat: Expose Caido proxy port to host for human-in-the-loop interaction
Users can now access the Caido web UI from their browser to inspect traffic,
replay requests, and perform manual testing alongside the automated scan.

- Map Caido port (48080) to a random host port in DockerRuntime
- Add caido_port to SandboxInfo and track across container lifecycle
- Display Caido URL in TUI sidebar stats panel with selectable text
- Bind Caido to 0.0.0.0 in entrypoint (requires image rebuild)
- Bump sandbox image to 0.1.12
- Restore discord link in exit screen
2026-02-23 18:37:25 -08:00
0xallam dcc40d426e fix: Handle stray quotes in tag names and enforce parameter tags in prompt 2026-02-20 08:29:01 -08:00
0xallam 0b69806328 feat: Normalize alternative tool call formats (invoke/function_calls) 2026-02-20 08:29:01 -08:00
0xallam 60ca7aa9df feat: Add mouse text selection auto-copy to clipboard in TUI
Enable native text selection across tool components and agent messages
with automatic clipboard copy, toast notification, and decorative icon
stripping. Replace Padding wrappers with Text to support selection
across multiple renderables.
2026-02-07 11:04:31 -08:00
0xallam 8767418326 refactor: standardize vulnerability skills format 2026-01-20 12:50:59 -08:00
0xallam 3cfb7bb96c docs(tools): add comprehensive multiline examples and remove XML terminology
- Add professional, realistic multiline examples to all tool schemas
- finish_scan: Complete pentest report with SSRF/access control findings
- create_vulnerability_report: Full SSRF writeup with cloud metadata PoC
- file_edit, notes, thinking: Realistic security testing examples
- Remove XML terminology from system prompt and tool descriptions
- All examples use real newlines (not literal \n) to demonstrate correct usage
2026-01-15 17:25:28 -08:00
0xallam 66e964e934 chore(prompt): discourage literal \n in tool params 2026-01-14 21:29:06 -08:00
0xallam 4841fc6394 chore(prompt): enforce single tool call per message and remove stop word usage 2026-01-14 19:51:08 -08:00
0xallam 8e67c10c8b fix(agent): fix agent loop hanging and simplify LLM module
- Fix agent loop getting stuck by adding hard stop mechanism
- Add _force_stop flag for immediate task cancellation across threads
- Use thread-safe loop.call_soon_threadsafe for cross-thread cancellation
- Remove request_queue.py (eliminated threading/queue complexity causing hangs)
- Simplify llm.py: direct acompletion calls, cleaner streaming
- Reduce retry wait times to prevent long hangs during retries
- Make timeouts configurable (llm_max_retries, memory_compressor_timeout, sandbox_execution_timeout)
- Keep essential token tracking (input/output/cached tokens, cost, requests)
- Maintain Anthropic prompt caching for system messages
2026-01-14 18:54:45 -08:00
0xallam 40a6347aee fix(agent): use correct agent name in identity instead of class name 2026-01-14 11:24:24 -08:00
0xallam 35339ff419 fix(agent): fix tool schemas not retrieved on pyinstaller binary and validate tool call args 2026-01-14 10:57:32 -08:00
Daniel Sangorrin 82cff89bb9 fix: add thinking blocks 2026-01-09 15:40:21 -08:00
0xallam f1820b76fc Add PostHog integration for analytics and error debugging 2026-01-09 14:24:04 -08:00
0xallam 3f19ab1188 fix: add timeout handling for Docker operations and improve error messages
- Add SandboxInitializationError exception for sandbox/Docker failures
- Add 60-second timeout to Docker client initialization
- Add _exec_run_with_timeout() method using ThreadPoolExecutor for exec_run calls
- Catch ConnectionError and Timeout exceptions from requests library
- Add _handle_sandbox_error() and _handle_llm_error() methods in base_agent.py
- Handle sandbox_error_details tool in TUI for displaying errors
- Increase TUI truncation limits for better error visibility
- Update all Docker error messages with helpful hint:
  'Please ensure Docker Desktop is installed and running, and try running strix again.'
2026-01-08 17:41:44 -08:00
0xallam b687d7a188 feat(reporting): add LLM-based vulnerability deduplication
- Add dedupe.py with XML-based LLM deduplication using direct litellm calls
- Integrate deduplication check in create_vulnerability_report tool
- Add get_existing_vulnerabilities() method to tracer for fetching reports
- Update schema and system prompt with deduplication guidelines
2026-01-07 19:32:45 -08:00
0xallam 96a7b00d6c Refactor(skills): rename prompt modules to skills and update documentation 2026-01-06 17:50:15 -08:00
0xallam 5acb924d9c feat(agent): implement user interruption handling in agent execution 2026-01-06 16:44:22 -08:00
0xallam 463e90c67b feat(tui): add real-time streaming LLM output with full content display
- Convert LiteLLM requests to streaming mode with stream_request()
- Add streaming parser to handle live LLM output segments
- Update TUI for real-time streaming content rendering
- Add tracer methods for streaming content tracking
- Clean function tags from streamed content to prevent display
- Remove all truncation from tool renderers for full content visibility
2026-01-06 16:44:22 -08:00
0xallam 33742e3ef0 enhance todo tool prompt 2025-12-15 10:26:59 -08:00
0xallam 145f99f782 feat(tools): add bulk operations support to todo tools
- update_todo: add `updates` param for bulk updates in one call
- mark_todo_done: add `todo_ids` param to mark multiple todos done
- mark_todo_pending: add `todo_ids` param to mark multiple pending
- delete_todo: add `todo_ids` param to delete multiple todos
- Increase todo renderer display limit from 10 to 25
- Maintains backward compatibility with single-ID usage
- Update prompts to keep todos short-horizon and dynamic
2025-12-14 20:31:33 -08:00