Repository navigation
chore(activity-tracking): wire in or retire terminal/file/browser activity tracking + secret_usage audit (backend) #16466
Description
Activity
- linked a pull request that will close this issuechore(activity-tracking): retire the terminal/file/browser tracking hooks as dead code (#16466) #16475
on Sep 12, 2026 - added 6 commits that reference this issue
on Sep 12, 2026 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:
integrations/terminal_tracking.py,file_tracking.pyandbrowser_tracking.pyhave no callers. They need real call sites: terminal command execution, file operations and browser automation, matching howdesktop_tracking.pyis wired throughapi/vnc_proxy.py.knowledge/activity_types.pyis Pydantic schemas only, with no DB code. Thesecret_usagewrite path and the terminal secret detection (utils/activity_tracker.py:detect_secret_usage,_detect_secrets_in_command,SECRET_PATTERNS) exist but never run.- Nothing reads the four tables fix(migrations): 6 tables in the orphaned database/migrations/ chain were never applied to the live Alembic chain #16464 created. Rows nobody can see are half a feature, so this adds a read criterion.
- The frontend counterpart,
useActivityTracking.ts, was retired in impl gap: wire in the real-time collaborative-session UI (#608 phases 5-7 / #874) — 6 components + useSessionCollaboration, zero callers #16443. Any UI for this work is new, not a restore of that composable.
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.
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-effortuuid.UUID(current_user.get("user_id"))extraction that silently skips if missing/invalid, aget_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(...)intoapi/agent_terminal.py, afterservice.execute_command(...)(agent_terminal.py:497) and afterservice.approve_command(...)(agent_terminal.py:532) — both routes already havecurrent_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 throughAgentTerminalService'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'strack_terminal_activity(whichtrack_command_executioncalls) already invokesdetect_secret_usage/_detect_secrets_in_commandinternally (activity_tracker.py:114) — calling the tracker wires the whole chain,SECRET_PATTERNSincluded.2. File (
integrations/file_tracking.py— mechanical, no open questions)api/files.pyalready has the right five call sites via its existingsecurity_layer.audit_log(...)calls, each withuser_data(fromget_auth_middleware().check_file_permissions(...)) already in scope:upload_file→_log_upload_audit(files.py:714) →track_file_uploaddownload_file→ audit atfiles.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.pyhas no DB import today — add the same best-effortget_async_session_factory()sessionvnc_proxy.pyuses, notDepends, 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'snavigate_mcp/click_mcp/fill_mcp,api/research_browser.py'sresearch_url/handle_session_action/navigate_session), but neither router has an authenticated caller in scope today — both are gated only bydependencies=[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 extractcurrent_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 resolvinguser_idviasecurity/session_ownership.py:193'sget_session_owner(conversation_id)for the tworesearch_browser.pyroutes (which already key offconversation_id) and leavingbrowser_mcp.py's three routes unwired until they gain a session/user concept — happy to go either way, proceeding with theDepends(get_current_user)approach unless told otherwise.4. Read endpoint (new AC)
New
GET /activityendpoint (new small router,api/activity.py), followingapi/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 inmodels/activities.py), merged and paginated. An optionalsession_idquery 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 surfaceapi/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 thebrowser_mcp.py/research_browser.pyauth-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 verifyvnc_proxy.py's wiring). - Read-endpoint tests: own rows returned, another user's rows never returned,
session_idfilter narrows correctly, unauthenticated request rejected.
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.pyremains the live exception (wired intoapi/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.pyand their test files are removed,ACTIVITY_TRACKING_README.mdtrimmed,repo_tests/kb_read_visibility_guard_test.py's floor lowered to match,#16466's own retire commit referenced instore_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.- added a commit that references this issue
on Sep 21, 2026
Problem
#16464 ports
terminal_activities,file_activities,browser_activities, andsecret_usageinto the canonical Alembic chain (they're real ORM models,models/activities.py, and their FK cascade tousers.idwas 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 callersintegrations/file_tracking.py— zero callersintegrations/browser_tracking.py— zero callersknowledge/activity_types.py(writesSecretUsageModel/secret_usage) — zero callers(
integrations/desktop_tracking.pyis the exception — it's live, called fromapi/vnc_proxy.py.)This mirrors the frontend side of the same feature:
useActivityTracking.ts/useSessionActivityLogger(the local activity-tracking composablesSessionActivity.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:
knowledge/activity_types.pyinto real call sites (terminal command execution, file operations, browser automation), matching howdesktop_tracking.pyis wired intoapi/vnc_proxy.py.Acceptance criteria
terminal_tracking.py,file_tracking.py,browser_tracking.py,knowledge/activity_types.pyis either wired into a real call site, or a deliberate retire decision is recorded here with evidenceDiscovered 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.