Skip to content

chore(activity-tracking): wire in or retire terminal/file/browser activity tracking + secret_usage audit (backend) #16466

Description

@mrveiss

Problem

#16464 ports terminal_activities, file_activities, browser_activities, and secret_usage into the canonical Alembic chain (they're real ORM models, models/activities.py, and their FK cascade to users.id was breaking user/org hard-deletes regardless of whether anything writes to them). But three of the four backend writer modules have zero real callers:

  • integrations/terminal_tracking.py — zero callers
  • integrations/file_tracking.py — zero callers
  • integrations/browser_tracking.py — zero callers
  • knowledge/activity_types.py (writes SecretUsageModel/secret_usage) — zero callers

(integrations/desktop_tracking.py is the exception — it's live, called from api/vnc_proxy.py.)

This mirrors the frontend side of the same feature: useActivityTracking.ts/useSessionActivityLogger (the local activity-tracking composables SessionActivity.type: 'terminal'|'file'|'browser'|'desktop' maps onto) were retired in #16443 for the same reason — zero real callers.

Fix

Per dead-code-review discipline, one of:

  1. Wire each of the 3 tracking modules + knowledge/activity_types.py into real call sites (terminal command execution, file operations, browser automation), matching how desktop_tracking.py is wired into api/vnc_proxy.py.
  2. If this activity-tracking feature ([Backend] Activity Entity Types with User Attribution (#608 Phase 4) #871/[Feature] User-Centric Session Tracking for All UI Components #608 Phase 4) is genuinely superseded or abandoned, make a deliberate retire decision — but only after confirming no capability gap, and note the decision on this issue.

Acceptance criteria

Discovered During

#16464 (porting orphaned tables into the canonical Alembic chain) — the tables are now real regardless of this issue's outcome; this is purely about whether the backend code that would write to 3 of them is finished or dead.

Activity

  1. added this to the v0.9.0 milestone on Sep 12, 2026
  2. mrveiss commented on Sep 14, 2026

    @mrveiss
    OwnerAuthor

    Owner decision, 2026-09-14: wire in, do not retire. #16475, which proposed retiring, was closed unmerged.

    The facts from #16475's analysis this work starts from:

    Added criterion:

    • The recorded activity can be read: an authenticated API endpoint lists a session's or user's terminal, file, browser and secret-usage activity, scoped to the caller's access. Tested.
  3. mrveiss commented on Sep 14, 2026

    @mrveiss
    OwnerAuthor

    Plan (per the owner's ordering rule: wire in as-is, no redesign/consolidation/trimming — that's a follow-up)

    Investigated every call site before writing any code. One PR, four wiring jobs + the read endpoint, following integrations/desktop_tracking.py + api/vnc_proxy.py's existing live pattern throughout: local import of the tracker, best-effort uuid.UUID(current_user.get("user_id")) extraction that silently skips if missing/invalid, a get_async_session_factory()-obtained session where the route has no DB dependency already, call placed after the real operation succeeds, all exceptions caught and logged — never raised. Tracking must never break the primary operation.

    1. Terminal (integrations/terminal_tracking.py — mechanical, no open questions)

    Wire track_command_execution(...) into api/agent_terminal.py, after service.execute_command(...) (agent_terminal.py:497) and after service.approve_command(...) (agent_terminal.py:532) — both routes already have current_user: dict = Depends(get_current_user) in scope (agent_terminal.py:475-479), so this stays at the API layer rather than threading a DB session through AgentTerminalService's process-wide singleton. session.get_user_id() (services/agent_terminal/models.py:56-58) returns the conversation id, not a real user UUID — do not use it; current_user["user_id"] from the route is the real identity.

    Secret-usage detection needs no separate wiring: utils/activity_tracker.py's track_terminal_activity (which track_command_execution calls) already invokes detect_secret_usage/_detect_secrets_in_command internally (activity_tracker.py:114) — calling the tracker wires the whole chain, SECRET_PATTERNS included.

    2. File (integrations/file_tracking.py — mechanical, no open questions)

    api/files.py already has the right five call sites via its existing security_layer.audit_log(...) calls, each with user_data (from get_auth_middleware().check_file_permissions(...)) already in scope:

    • upload_file → _log_upload_audit (files.py:714) → track_file_upload
    • download_file → audit at files.py:759-770 → track_file_download
    • _delete_file_item (files.py:600) / _delete_directory_item (files.py:633) → track_file_operation(operation="delete")
    • rename_file_or_directory → _log_rename_audit (files.py:834) → track_file_operation(operation="rename")
    • create_directory → _log_directory_create_audit (files.py:1070) → track_file_operation(operation="create")

    api/files.py has no DB import today — add the same best-effort get_async_session_factory() session vnc_proxy.py uses, not Depends, so no route signature changes.

    3. Browser (integrations/browser_tracking.py — one real decision point)

    Real automation call sites exist and are unambiguous (api/browser_mcp.py's navigate_mcp/click_mcp/fill_mcp, api/research_browser.py's research_url/handle_session_action/navigate_session), but neither router has an authenticated caller in scope today — both are gated only by dependencies=[Depends(check_admin_permission)], which returns a bool and injects nothing.

    Decision, flagged before coding rather than after: add current_user: dict = Depends(get_current_user) alongside the existing admin-gate dependency on these six endpoints, then extract current_user.get("user_id") exactly like the other three trackers. This does not broaden who can call the endpoints (still admin-gated) — it only captures which admin called it, matching the identity-extraction pattern every other tracker already uses. Treating this as in-scope "wiring", not a redesign, since it adds a dependency rather than changing any existing behavior. If this reads as too much surface change for this PR, the alternative is resolving user_id via security/session_ownership.py:193's get_session_owner(conversation_id) for the two research_browser.py routes (which already key off conversation_id) and leaving browser_mcp.py's three routes unwired until they gain a session/user concept — happy to go either way, proceeding with the Depends(get_current_user) approach unless told otherwise.

    4. Read endpoint (new AC)

    New GET /activity endpoint (new small router, api/activity.py), following api/mobile_devices.py:249-256's self-ownership pattern exactly: Depends(get_current_user) + Depends(get_db_session), select(Model).where(Model.user_id == user_id) for each of the four tables (TerminalActivityModel, FileActivityModel, BrowserActivityModel, SecretUsageModel — all in models/activities.py), merged and paginated. An optional session_id query param filters further within the caller's own rows (not a cross-user session-participant view) — satisfies "a session's or user's activity, scoped to the caller's access" without the larger permission-graded surface api/collaboration.py:287-350's participant-scoped pattern would add; that stays a candidate for a follow-up if a real need for viewing other participants' activity in a shared session shows up.

    Not in this PR (follow-up territory per the ordering rule)

    Any consolidation of the three now-live trackers' near-duplicate shape, any redesign of knowledge/activity_types.py's unused Pydantic schemas now that the ORM models are what's actually written, and the browser_mcp.py/research_browser.py auth-surface question above if the alternative is preferred instead.

    Verification plan

    • Unit tests per tracker call site (mock the tracker function, assert it's called with the right args after the real operation, assert it's NOT called/doesn't raise on tracker failure).
    • One test per table confirming a row lands via the wired path (integration-style, matching how desktop_tracking.py's existing tests verify vnc_proxy.py's wiring).
    • Read-endpoint tests: own rows returned, another user's rows never returned, session_id filter narrows correctly, unauthenticated request rejected.
  4. modified the milestones: v0.9.0, v0.10.0 on Sep 17, 2026
  5. mrveiss commented on Sep 20, 2026

    @mrveiss
    OwnerAuthor

    Retire decision, with evidence — landing via vehicle-v090-2026-09-20-orphans (orphaned-branch consolidation, owner directive).

    Re-verified the "zero callers" premise myself against current main (751497a30), not trusting the 4-month-old branch's own claim:

    ```
    $ git grep -l "integrations.terminal_tracking|integrations/terminal_tracking" -- '.py' | grep -v _test.py
    $ git grep -l "integrations.file_tracking|integrations/file_tracking" -- '
    .py' | grep -v _test.py
    $ git grep -l "integrations.browser_tracking|integrations/browser_tracking" -- '.py' | grep -v _test.py
    $ git grep -l "knowledge.activity_types|knowledge/activity_types" -- '
    .py' | grep -v _test.py
    ```

    All four: zero real callers, today. integrations/desktop_tracking.py remains the live exception (wired into api/vnc_proxy.py), untouched. The frontend side of the same feature was already retired for the same reason in #16443.

    Per this issue's own AC ("wired... OR a deliberate retire decision is recorded here with evidence"): retiring. terminal_tracking.py, file_tracking.py, browser_tracking.py, knowledge/activity_types.py and their test files are removed, ACTIVITY_TRACKING_README.md trimmed, repo_tests/kb_read_visibility_guard_test.py's floor lowered to match, #16466's own retire commit referenced in store_authority's write-site list. The tables themselves (#16464's canonical-Alembic port) are untouched — this is purely about the dead writer code, not the schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions