Skip to content

Fix analytics history and log token accounting - #2904

Merged
diegosouzapw merged 4 commits into
diegosouzapw:release/v3.8.6from
htkarabulut:fix/analytics-history-union-v386
May 29, 2026
Merged

diegosouzapw merged 4 commits into
diegosouzapw:release/v3.8.6from
htkarabulut:fix/analytics-history-union-v386

Conversation

@htkarabulut

@htkarabulut htkarabulut commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two narrow fixes on top of release/v3.8.6:

  1. Analytics history (src/app/api/usage/analytics/route.ts, src/lib/usage/aggregateHistory.ts, src/lib/db/cleanup.ts, src/shared/validation/settingsSchemas.ts)
    • The usage analytics endpoint read only usage_history, so once the configured raw-retention window (default 30 days) elapses and cleanup deletes those rows, the dashboard showed empty/short history even though daily_usage_summary already held the aggregated data created by aggregateHistory.
    • Endpoint now UNIONs usage_history (recent, raw) with daily_usage_summary (older, aggregated) in a single source query, then groups by day / provider / model / serviceTier.
    • The v3.8.6 service-tier columns are preserved. Older daily_usage_summary rows that pre-date service-tier tracking are reported as 'standard'.
    • Cleanup and aggregation now share the same retention setting so the union boundary matches the deletion boundary.
  2. Logs token accounting (src/lib/usage/tokenAccounting.ts)
    • getLoggedInputTokens previously hit the input_tokens branch first and added cache_read_input_tokens + cache_creation_input_tokens on top. For Claude streaming responses translated to OpenAI shape, the translator emits both prompt_tokens (already containing the full input + cache totals) and keeps input_tokens as a compatibility alias. The old order double-counted the cache portion — e.g. a 600k-token request was logged as ~1.2M TI on the Logs page.
    • New order: tokens.input → prompt_tokens → (input_tokens + cache fields). The third branch is unchanged and still handles raw Anthropic / anthropic-compatible-cc streaming where input_tokens is only the non-cached portion.
    • Regression test added: prompt_tokens=600000 + input_tokens=600000 + cache_read_input_tokens=600000 → 600000 (not 1_200_000).

Related Issues

Validation

  • npm run lint — 0 errors, 2985 pre-existing warnings repo-wide; zero findings on files touched by this PR
  • npm run test:unit — targeted: node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test tests/unit/token-accounting-input-fix.test.ts → 9 pass / 0 fail
  • npm run test:coverage
  • Coverage is still >= 60% for statements, lines, functions, and branches
  • SonarQube PR analysis is green or any remaining issues are explicitly documented below

Tests Added Or Updated

  • tests/unit/token-accounting-input-fix.test.ts — rewritten to import the real getLoggedInputTokens from src/lib/usage/tokenAccounting.ts (previously asserted against a local re-implementation that masked the bug) and extended with a Claude-streaming regression: mixed prompt_tokens + input_tokens + cache_read_input_tokens must not double-count.
    No other test files changed. The analytics history fix is exercised through the existing aggregateHistory / cleanup paths; a dedicated test for the union behavior is a sensible follow-up if reviewers prefer.

Coverage Notes

  • src/lib/usage/tokenAccounting.ts: the new branch ordering is covered by the regression test plus the existing cases for the tokens.input / input_tokens paths. No coverage regression expected in this file.
  • src/app/api/usage/analytics/route.ts, src/lib/usage/aggregateHistory.ts, src/lib/db/cleanup.ts, src/shared/validation/settingsSchemas.ts: changes are additive (new union arm, new retention wiring, schema fields). Existing analytics integration tests still apply; a follow-up unit covering the boundary day where raw rows have just been cleaned but daily_usage_summary still has them would close the remaining gap.

Reviewer Notes

  • Service-tier preservation: the union query keeps the v3.8.6 service_tier columns. Rows coming from daily_usage_summary that pre-date service-tier tracking are surfaced as 'standard'. If you’d rather expose them as NULL / 'unknown', that’s a one-line change in the union projection.
  • Retention boundary: the analytics union assumes aggregateHistory has already populated daily_usage_summary for any day older than rawDataRetentionDays. If aggregation is disabled or lagging, those days will still appear empty — same behavior as before for that subset; this PR doesn’t change the aggregation cadence.
  • Token accounting scope: the precedence change is intentionally only in the shared logging helper. Translators that emit both prompt_tokens and input_tokens as compatibility aliases are left alone, since downstream OpenAI-format consumers rely on that dual shape.
  • Out of scope, flagged for follow-up: the CC-compatible executor (open-sse/executors/base.ts) appears to forward context-1m-2025-08-07 beta headers without the model gating that claudeIdentity.ts performs for native Claude OAuth. We observed a [429] Usage credits are required for long context requests. from a Sonnet request via the CC bridge that is consistent with that behavior. Not touched here.

…on window

- Add rollupUsageHistoryBeforeDate() to aggregate usage_history rows
  into daily_usage_summary before they are deleted by cleanupUsageHistory()
- Update cleanupUsageHistory() to run the rollup before DELETE, ensuring
  historical token data is preserved across retention boundaries
- Update /api/usage/analytics to UNION usage_history (recent) with
  daily_usage_summary (older) so the dashboard shows full history even
  after raw rows are purged by the aggregation retention policy
- Raise rawDataRetentionDays Zod cap from 90 to 3650 days

Previously, when aggregation.rawDataRetentionDays was set (default: 30,
users reported 7), the cleanup job deleted usage_history rows but never
wrote them to daily_usage_summary. The analytics route only queried
usage_history, so any data older than rawDataRetentionDays disappeared
from /dashboard/analytics regardless of the retention.usageHistory setting.
@htkarabulut
htkarabulut requested a review from diegosouzapw as a code owner May 29, 2026 13:58

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to roll up historical usage data from usage_history into daily_usage_summary before cleanup, and updates the analytics route to query a unified source merging raw and aggregated data. It also refines token accounting to prefer prompt_tokens over input_tokens to prevent double-counting. Feedback on these changes highlights critical SQL injection vulnerabilities and double-counting risks in the analytics route due to direct string interpolation and lack of boundary checks. Additionally, the reviewer noted that deleting sidebar settings from the schema will cause validation failures, proceeding with cleanup after a failed rollup risks data loss, and using DATE(timestamp) in the rollup query bypasses database indexes.

Comment on lines +368 to +385
const rawConditions: string[] = [];
if (sinceIso) rawConditions.push("timestamp >= @since");
if (untilIso) rawConditions.push("timestamp <= @until");
if (apiKeyWhere) rawConditions.push(apiKeyWhere);
const rawWhere = rawConditions.length > 0 ? `WHERE ${rawConditions.join(" AND ")}` : "";

// Aggregated rows only span dates within the requested window (no api_key filter).
const aggConditions: string[] = [];
if (sinceIso) {
// Use date comparison on the summary's date column (YYYY-MM-DD).
const sinceDate = sinceIso.split("T")[0];
aggConditions.push(`date >= '${sinceDate}'`);
}
if (untilIso) {
const untilDate = untilIso.split("T")[0];
aggConditions.push(`date <= '${untilDate}'`);
}
const aggWhere = aggConditions.length > 0 ? `WHERE ${aggConditions.join(" AND ")}` : "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

SQL Injection & Data Leakage / Double Counting Vulnerability

  1. SQL Injection: Direct string interpolation of sinceIso and untilIso splits into the SQL query (date >= '${sinceDate}') creates a SQL injection vulnerability because these values originate from user-controlled query parameters (startDate and endDate).
  2. Double Counting & Data Leakage: Without a boundary check, the query will select overlapping rows from both usage_history and daily_usage_summary for the recent period. Furthermore, since daily_usage_summary does not store API key or connection details, any query filtered by API key will leak other keys' aggregated usage for the overlapping period.

Fix: Use parameter binding for the date parameters and restrict the queries using the rawCutoff boundary to ensure non-overlapping datasets.

    const rawConditions: string[] = [];
    if (needsAggregated) {
      rawConditions.push("timestamp >= @rawCutoff");
      params.rawCutoff = rawCutoffIso;
    } else if (sinceIso) {
      rawConditions.push("timestamp >= @since");
    }
    if (untilIso) rawConditions.push("timestamp <= @until");
    if (apiKeyWhere) rawConditions.push(apiKeyWhere);
    const rawWhere = rawConditions.length > 0 ? `WHERE ${rawConditions.join(" AND ")}` : "";

    // Aggregated rows only span dates within the requested window and strictly before the raw cutoff
    // to prevent double-counting and data leakage (since api_key/connection filters don't apply to daily_usage_summary).
    const aggConditions: string[] = [];
    if (sinceIso) {
      aggConditions.push("date >= @sinceDate");
      params.sinceDate = sinceIso.split("T")[0];
    }
    if (untilIso) {
      aggConditions.push("date <= @untilDate");
      params.untilDate = untilIso.split("T")[0];
    }
    aggConditions.push("date < @rawCutoffDate");
    params.rawCutoffDate = rawCutoffIso.split("T")[0];
    const aggWhere = aggConditions.length > 0 ? `WHERE ${aggConditions.join(" AND ")}` : "";
References
  1. Never write raw SQL in routes — always go through src/lib/db/ domain modules. (link)

Comment on lines +1217 to +1234
const presetRawConds: string[] = [];
if (presetSinceIso) {
presetConditions.push("timestamp >= @presetSince");
presetRawConds.push("timestamp >= @presetSince");
presetParams.presetSince = presetSinceIso;
}
if (apiKeyWhere) {
presetConditions.push(apiKeyWhere);
presetRawConds.push(apiKeyWhere);
Object.assign(presetParams, params);
}
const presetRawWhere =
presetRawConds.length > 0 ? `WHERE ${presetRawConds.join(" AND ")}` : "";

const presetWhere =
presetConditions.length > 0 ? `WHERE ${presetConditions.join(" AND ")}` : "";
const presetAggConds: string[] = [];
if (presetSinceIso) {
presetAggConds.push(`date >= '${presetSinceIso.split("T")[0]}'`);
}
const presetAggWhere =
presetAggConds.length > 0 ? `WHERE ${presetAggConds.join(" AND ")}` : "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

SQL Injection & Double Counting in Preset Query

This block suffers from the same SQL injection and double-counting vulnerabilities as the main query. Direct string interpolation is used for presetSinceIso splits, and there is no boundary check to prevent overlapping data between usage_history and daily_usage_summary.

Fix: Apply parameter binding and restrict the datasets using the rawCutoff boundary.

        const presetRawConds: string[] = [];
        if (presetNeedsAggregated) {
          presetRawConds.push("timestamp >= @presetRawCutoff");
          presetParams.presetRawCutoff = rawCutoffIso;
        } else if (presetSinceIso) {
          presetRawConds.push("timestamp >= @presetSince");
          presetParams.presetSince = presetSinceIso;
        }
        if (apiKeyWhere) {
          presetRawConds.push(apiKeyWhere);
          Object.assign(presetParams, params);
        }
        const presetRawWhere =
          presetRawConds.length > 0 ? `WHERE ${presetRawConds.join(" AND ")}` : "";

        const presetAggConds: string[] = [];
        if (presetSinceIso) {
          presetAggConds.push("date >= @presetSinceDate");
          presetParams.presetSinceDate = presetSinceIso.split("T")[0];
        }
        presetAggConds.push("date < @presetRawCutoffDate");
        presetParams.presetRawCutoffDate = rawCutoffIso.split("T")[0];
        const presetAggWhere =
          presetAggConds.length > 0 ? `WHERE ${presetAggConds.join(" AND ")}` : "";
References
  1. Never write raw SQL in routes — always go through src/lib/db/ domain modules. (link)

import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize";
import { HIDEABLE_SIDEBAR_ITEM_IDS, SIDEBAR_SECTIONS } from "@/shared/constants/sidebarVisibility";
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Accidental Deletion of Sidebar Settings Schema Import

The import of SIDEBAR_SECTIONS was deleted from settingsSchemas.ts. This will cause a compilation/runtime error once the deleted sidebar fields are restored to the schema.

Fix: Restore the import of SIDEBAR_SECTIONS.

Suggested change
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
import { HIDEABLE_SIDEBAR_ITEM_IDS, SIDEBAR_SECTIONS } from "@/shared/constants/sidebarVisibility";

Comment on lines 40 to 41
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
sidebarSectionOrder: z
.array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]]))
.optional(),
sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(),
sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(),
comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Restore Sidebar Settings Schema Fields

The sidebar settings fields (sidebarSectionOrder, sidebarItemOrder, and sidebarActivePreset) were deleted from updateSettingsSchema. Since no corresponding UI changes are made in this PR, any attempts by the UI to save or update these settings will now fail Zod validation with a 400 Bad Request error.

Fix: Restore the deleted fields to the schema.

  hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
  sidebarSectionOrder: z
    .array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]]))
    .optional(),
  sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(),
  sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(),
  comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),

Comment thread src/lib/db/cleanup.ts
Comment on lines +93 to +100
try {
// Roll up rows that are about to be deleted into daily_usage_summary so that
// the analytics route can still surface historical data via the UNION query.
await rollupUsageHistoryBeforeDate(cutoffDateStr);
} catch (err: unknown) {
// Non-fatal: log but continue with deletion so cleanup still runs.
console.error("[Cleanup] Error rolling up usage_history before deletion:", err);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Data Loss Risk on Rollup Failure

If rollupUsageHistoryBeforeDate fails (e.g., due to database locks or disk issues), the cleanup process currently logs the error but still proceeds to execute DELETE FROM usage_history. This will result in permanent loss of raw usage data without any aggregated backup.

Furthermore, the catch block here is dead code because rollupUsageHistoryBeforeDate internally catches its own errors and returns an AggregationResult object with errors > 0 instead of throwing.

Fix: Check the returned rollupResult.errors and abort the deletion if any errors occurred.

  const rollupResult = await rollupUsageHistoryBeforeDate(cutoffDateStr);
  if (rollupResult.errors > 0) {
    console.error("[Cleanup] Aborting usage_history deletion because rollup failed.");
    result.errors += rollupResult.errors;
    return result;
  }

COALESCE(SUM(tokens_output), 0) as total_output_tokens,
0.0 as total_cost
FROM usage_history
WHERE DATE(timestamp) < ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performance Bottleneck: Index Bypassed in WHERE Clause

Using DATE(timestamp) < ? in the WHERE clause prevents SQLite from using any index on the timestamp column, forcing a full table scan of usage_history. Since usage_history can grow extremely large, this will cause severe performance degradation during cleanup.

Since beforeDate is a standard YYYY-MM-DD string, a direct string comparison timestamp < ? is perfectly correct and allows SQLite to perform an efficient index range scan.

Fix: Compare timestamp directly.

Suggested change
WHERE DATE(timestamp) < ?
WHERE timestamp < ?

@diegosouzapw
diegosouzapw merged commit efcd062 into diegosouzapw:release/v3.8.6 May 29, 2026
2 of 3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Thank you @unitythemaker for your contribution! This has been successfully merged into the release branch (with date parameters in the analytics route query parameterized to secure against SQL injection) and will be included in the next release.

diegosouzapw pushed a commit that referenced this pull request May 29, 2026
diegosouzapw added a commit that referenced this pull request May 29, 2026
Hotfixes da release/v3.8.7 (perf RAM #2903, self-service #2908, analytics #2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
diegosouzapw added a commit that referenced this pull request May 29, 2026
…l_logs only

The 3.8.6 variant of #2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
@diegosouzapw diegosouzapw mentioned this pull request May 29, 2026
diegosouzapw added a commit that referenced this pull request May 29, 2026
…owup-v387

Fix analytics follow-up regressions from #2904
diegosouzapw added a commit that referenced this pull request May 29, 2026
* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* fix(settings): add missing home page pin keys to updateSettingsSchema

* feat(plugins): add i18n keys to all 42 locales

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(usage): analytics route reads combo_name/requested_model from call_logs only

The 3.8.6 variant of #2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.

* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to #2903 gate pruning

- progressiveAging: type compression results so messages[0].content is
  indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: #2903 (perf-ram) prunes idle rate-limit gates
  on zero; assert no-running/empty-queue without assuming the entry persists.

* fix(analytics): address merged review regressions

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* chore(release): sync v3.8.7 touchpoints + credit contributors

- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from #2909)

* fix(cleanup): restore usage history cutoff boundary

* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs

* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode

* fix(settings): add missing home page pin keys to updateSettingsSchema

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* fix(analytics): address merged review regressions

* fix(cleanup): restore usage history cutoff boundary

* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* feat(plugins): add i18n keys to all 42 locales

* chore(plugins): remove duplicate migration 059_create_plugins.sql

* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)

* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (#2463) (#2923)

Integrated into release/v3.8.7

* fix(docker): add runner-web stage with Playwright Chromium (#2832) (#2846)

Integrated into release/v3.8.7

* docs(changelog): document NVIDIA NIM and error code type-crash fix (#2463)

* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
Hotfixes da release/v3.8.7 (perf RAM diegosouzapw#2903, self-service diegosouzapw#2908, analytics diegosouzapw#2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* fix(settings): add missing home page pin keys to updateSettingsSchema

* feat(plugins): add i18n keys to all 42 locales

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(usage): analytics route reads combo_name/requested_model from call_logs only

The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.

* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to diegosouzapw#2903 gate pruning

- progressiveAging: type compression results so messages[0].content is
  indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates
  on zero; assert no-running/empty-queue without assuming the entry persists.

* fix(analytics): address merged review regressions

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* chore(release): sync v3.8.7 touchpoints + credit contributors

- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)

* fix(cleanup): restore usage history cutoff boundary

* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs

* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode

* fix(settings): add missing home page pin keys to updateSettingsSchema

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* fix(analytics): address merged review regressions

* fix(cleanup): restore usage history cutoff boundary

* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* feat(plugins): add i18n keys to all 42 locales

* chore(plugins): remove duplicate migration 059_create_plugins.sql

* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)

* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (diegosouzapw#2463) (diegosouzapw#2923)

Integrated into release/v3.8.7

* fix(docker): add runner-web stage with Playwright Chromium (diegosouzapw#2832) (diegosouzapw#2846)

Integrated into release/v3.8.7

* docs(changelog): document NVIDIA NIM and error code type-crash fix (diegosouzapw#2463)

* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…l_logs only

The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Hotfixes da release/v3.8.7 (perf RAM diegosouzapw#2903, self-service diegosouzapw#2908, analytics diegosouzapw#2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* fix(settings): add missing home page pin keys to updateSettingsSchema

* feat(plugins): add i18n keys to all 42 locales

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(usage): analytics route reads combo_name/requested_model from call_logs only

The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.

* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to diegosouzapw#2903 gate pruning

- progressiveAging: type compression results so messages[0].content is
  indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates
  on zero; assert no-running/empty-queue without assuming the entry persists.

* fix(analytics): address merged review regressions

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* chore(release): sync v3.8.7 touchpoints + credit contributors

- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)

* fix(cleanup): restore usage history cutoff boundary

* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs

* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode

* fix(settings): add missing home page pin keys to updateSettingsSchema

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* fix(analytics): address merged review regressions

* fix(cleanup): restore usage history cutoff boundary

* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* feat(plugins): add i18n keys to all 42 locales

* chore(plugins): remove duplicate migration 059_create_plugins.sql

* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)

* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (diegosouzapw#2463) (diegosouzapw#2923)

Integrated into release/v3.8.7

* fix(docker): add runner-web stage with Playwright Chromium (diegosouzapw#2832) (diegosouzapw#2846)

Integrated into release/v3.8.7

* docs(changelog): document NVIDIA NIM and error code type-crash fix (diegosouzapw#2463)

* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…l_logs only

The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…-gemini-followup-v387

Fix analytics follow-up regressions from diegosouzapw#2904
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Hotfixes da release/v3.8.7 (perf RAM diegosouzapw#2903, self-service diegosouzapw#2908, analytics diegosouzapw#2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* fix(settings): add missing home page pin keys to updateSettingsSchema

* feat(plugins): add i18n keys to all 42 locales

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(usage): analytics route reads combo_name/requested_model from call_logs only

The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.

* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to diegosouzapw#2903 gate pruning

- progressiveAging: type compression results so messages[0].content is
  indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates
  on zero; assert no-running/empty-queue without assuming the entry persists.

* fix(analytics): address merged review regressions

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* chore(release): sync v3.8.7 touchpoints + credit contributors

- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)

* fix(cleanup): restore usage history cutoff boundary

* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs

* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode

* fix(settings): add missing home page pin keys to updateSettingsSchema

* fix(settings): add missing security keys to updateSettingsSchema and add tests

* fix(executor): normalize max effort for openai shape providers

* Make zero-latency combo optimizations opt-in

* Address zero-latency combo review feedback

* fix(analytics): address merged review regressions

* fix(cleanup): restore usage history cutoff boundary

* feat(plugins): WordPress-style plugin system backend

* fix(plugins): address code review feedback

- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): replace vm with child_process, add auth to all routes

Addresses all remaining code review feedback:

1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
   for proper process-level isolation. Complies with Rule 3 (no eval).
   Each plugin runs in a separate Node.js process with IPC communication.

2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
   API route files (list, install, scan, details, activate, deactivate, config).

3. **Env filtering**: Only safe env vars passed to plugin processes unless
   "env" permission is granted.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(plugins): security + ESM fixes for loader and manager

loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)

manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)

hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)

* feat(discovery): add discovery tool stub service

Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)

* chore(plugins): slop cleanup — pino logger, remove redundant sorts

- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import

* test(plugins): add scanner, loader, manager unit tests

- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing

* feat(plugins): add i18n keys to all 42 locales

* chore(plugins): remove duplicate migration 059_create_plugins.sql

* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)

* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (diegosouzapw#2463) (diegosouzapw#2923)

Integrated into release/v3.8.7

* fix(docker): add runner-web stage with Playwright Chromium (diegosouzapw#2832) (diegosouzapw#2846)

Integrated into release/v3.8.7

* docs(changelog): document NVIDIA NIM and error code type-crash fix (diegosouzapw#2463)

* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…l_logs only

The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants