Back to Codexbar

Local provider plugins

docs/plugins.md

0.49.015.5 KB
Original Source

Local provider plugins

CodexBar can load one local JavaScript or TypeScript file as a provider. Put a .js or .ts file in ~/.config/codexbar/providers/, or choose Settings → Plugins → Install…. Each file declares its complete authority and settings schema in a manifest, fetches through CodexBar's sandboxed host API, and returns a generic usage snapshot.

Plugins are local files only. CodexBar has no plugin catalog, does not download plugin code or assets, and does not resolve imports. A plugin cannot use Node, browser globals, subprocesses, local files, databases, OAuth, WebViews, or arbitrary native APIs. The maximum source size is 1 MiB.

Minimal plugin

js
defineProvider({
  id: "acme-usage",
  name: "Acme Usage",
  icon: { monogram: "AC", tint: "#336699" },
  endpoints: ["https://api.example.com"],
  auth: { type: "bearer", secret: "API_KEY" },
  settings: [
    { key: "API_KEY", title: "API key", subtitle: "Create one in Acme settings.", type: "secure" },
  ],
  async fetchUsage(ctx) {
    const response = await ctx.http.getJSON("https://api.example.com/v1/usage");
    return {
      primary: {
        usedPercent: response.json.used_percent,
        resetsAt: response.json.resets_at,
        windowMinutes: 300,
      },
      details: [{
        title: "Usage",
        rows: [{ label: "Requests", value: String(response.json.requests) }],
      }],
    };
  },
});

Manifest reference

defineProvider must be called exactly once with an object containing:

  • id: 1–64 lowercase ASCII letters, digits, or hyphens. It must not match a built-in provider or another installed plugin.
  • name: trimmed display name, 1–80 UTF-8 bytes.
  • icon (optional): {monogram, tint}. monogram is 1–3 characters; tint is #RRGGBB. The fallback is the first letter of name with a neutral tint. File/SVG icons are not supported.
  • endpoints: 1–16 declared network origins. A fixed endpoint is a normalized HTTPS origin such as https://api.example.com (no path, query, fragment, or user info). A settings-derived endpoint is {setting: "BASE_URL", policy: "https"}, {setting: "BASE_URL", policy: "https-or-loopback-http"}, or {setting: "BASE_URL", policy: "https-or-private-network-http"}. Its setting must be declared as plain. https-or-loopback-http preserves the unauthenticated loopback-only rule. https-or-private-network-http also permits authenticated HTTP for loopback, RFC 1918 IPv4, IPv4 link-local, IPv6 unique-local/link-local, and .local targets, but only through the separate typed approval described below. Public targets always require HTTPS.
  • auth (optional): one of the forms below. The named secret must be a declared secure setting.
  • settings: up to 32 setting definitions. Keys contain 1–64 ASCII letters, digits, or underscores and start with a letter. Each entry has key, title, optional subtitle, and type: "plain" | "secure" (default secure).
  • capabilities (optional): "browser-cookies" and "http-status". With "http-status", the plugin observes non-2xx responses itself instead of the host failing the request.
  • cookieDomains: required with browser-cookies; a non-empty list of normalized DNS host names.
  • fetchUsage(ctx): function returning a snapshot object or a promise for one.

Authentication forms:

js
auth: { type: "bearer", secret: "API_KEY" }
auth: { type: "x-api-key", secret: "API_KEY" }
auth: { type: "header", header: "X-Custom-Key", secret: "API_KEY" }
auth: { type: "authorization-scheme", scheme: "Token", secret: "API_KEY" }

The host owns the authentication header; plugin request options cannot override it. Authenticated public origins must be HTTPS; authenticated private-network HTTP requires https-or-private-network-http plus typed approval. Secure settings can be overridden for CLI use with CODEXBAR_PLUGIN_<PLUGIN_ID>_<SETTING_KEY>, uppercased with non-alphanumeric characters replaced by underscores. For example, acme-usage and API_KEY use CODEXBAR_PLUGIN_ACME_USAGE_API_KEY.

ctx API

ctx exists only during fetchUsage. CodexBar uses QuickJS on every platform; both QuickJS and the Apple-only JavaScriptCore rollback engine provide ECMAScript built-ins but no browser or Node environment. Intl is engine-dependent and unavailable in QuickJS, so portable third-party plugins must use the host helpers below instead of ECMA-402. fetch, XMLHttpRequest, timers, require, process, and filesystem APIs are unavailable.

  • await ctx.http.getJSON(url, opts?) performs GET and returns {status, headers, json}.
  • await ctx.http.get(url, opts?) performs GET and returns {status, headers, bodyText}.
  • await ctx.http.postJSON(url, {body, headers?}) performs JSON POST. body must be JSON-serializable.
  • opts.headers accepts string values. Plugins cannot replace their declared auth header. opts.timeoutSeconds sets a hard request deadline from 1 through 30 seconds; the default is 15 seconds.
  • ctx.settings.get(key) reads a declared plain setting.
  • ctx.settings.getSecret(key) reads a declared secure setting. Missing values return null; kind mismatches and undeclared keys throw.
  • ctx.fail creates classified errors for authenticationExpired, missingCredential, permissionDenied, rateLimited, providerUnavailable, parseFailure, networkFailure, and apiFailure. Throw the returned error, for example throw ctx.fail.rateLimited("Provider rate limit reached"); ordinary errors retain generic mapping. Every plugin automatically gets one delayed retry when a request returns 408, 429, 500, 502, 503, or 504. A numeric Retry-After header sets the delay; otherwise the delay is 1 second, and the host clamps it to 10 seconds. A plugin that needs provider-specific handling—such as a non-numeric Retry-After, quota data in the error body, or a vendor retry field—declares http-status, receives the response, and throws ctx.fail.rateLimited(message, {retryAfterSeconds}) or another transient classified failure. Both paths share one retry budget and never retry the retry. Cancellation during the delay stops the retry.
  • await ctx.browser.cookieHeader(domain) returns a cookie header only with the browser-cookies capability and for a declared domain. The app imports from Chrome only. Cookie values are secret-equivalent and redacted.
  • ctx.html.metaContent(html, name) returns the first matching quoted meta value or null.
  • ctx.html.matchFirst(html, regexSource, flags?) returns the first capture/full match or null.
  • ctx.log(...values) writes to the instance-scoped plugin log. Known secrets and cookie values are redacted.
  • ctx.cache.get(key) and ctx.cache.set(key, value, ttlSeconds) provide a per-runtime memory cache. TTL is capped at 24 hours.
  • ctx.date.now(), iso(text), unixSeconds(number), and unixMillis(number) create JavaScript dates. now() uses the host refresh clock.
  • ctx.date.nowMillis() returns the same host refresh clock as Unix epoch milliseconds — use it for arithmetic that should stay deterministic under fixture clocks (the z.ai quota-rate row does).
  • ctx.date.nextDailyReset(timeZoneIdentifier, hour) returns the next wall-clock reset in an IANA time zone.
  • ctx.env.timeZone is the host's current IANA time-zone identifier; zero-offset GMT aliases are normalized to UTC.
  • ctx.format.number(value, options?), usd(value), and monthDay(date) provide deterministic formatting on both engines. Number options support minimumFractionDigits and maximumFractionDigits.
  • ctx.jwt.decode(token) decodes (but does not authenticate) a JWT JSON payload.
  • ctx.pct(used, limit) returns a finite percentage clamped to 0–100; non-positive limits map to 100.

User-plugin requests run in an ephemeral session with no ambient cookies, credential store, or URL cache. Redirects are rejected, the timeout is 15 seconds, Accept-Encoding: identity is sent, compressed responses always fail, and response bytes are capped at 1 MiB. By default, the host rejects non-2xx responses and automatically retries 408, 429, 500, 502, 503, and 504 once, using a numeric Retry-After delay or 1 second when absent, clamped to 10 seconds. With http-status, the plugin instead receives {status, headers, ...} and owns classification, including any request for the same single delayed retry. Request URLs must match a declared, approved origin.

js
capabilities: ["http-status"],
async fetchUsage(ctx) {
  const response = await ctx.http.getJSON("https://api.example.com/usage");
  if (response.status === 429) {
    const retryAfterSeconds = Number(response.headers["retry-after"] || 1);
    throw ctx.fail.rateLimited("Rate limited", { retryAfterSeconds });
  }
}

Declaring http-status changes the approval binding, so an installed plugin requires re-approval after adding it.

Bundled first-party providers that have cut over to JavaScript use the shared runtime's 20-second hung-script watchdog. A timeout fails that refresh and discards the poisoned worker so the next refresh starts with a fresh context; this is production-default and does not depend on CODEXBAR_JS_PROVIDERS. QuickJS enforces the watchdog in-engine with JS_SetInterruptHandler, caps the runtime heap at 64 MiB, and caps the JavaScript stack at 2 MiB. The interrupt terminates evaluation on its confined thread; timed-out scripts do not leave an abandoned evaluation thread behind. On Apple platforms, CODEXBAR_PLUGIN_ENGINE=jsc selects the JavaScriptCore rollback engine; the same rollback is available in Settings → Debug → Provider Plugins and takes effect after restarting CodexBar. JavaScriptCore has no public interrupt API, so a timed-out rollback-engine context is discarded but its abandoned evaluation thread can remain alive until process exit.

Snapshot result

Return at least one rate window, cost object, detail section, or non-empty identity field:

js
return {
  primary: { usedPercent: 25, resetsAt: new Date(), windowMinutes: 300 },
  secondary: { usedPercent: 40, resetsAt: "2026-08-10T00:00:00Z", windowMinutes: 10080 },
  tertiary: { usedPercent: 5 },
  extraWindows: [{ id: "daily", title: "Daily", window: { usedPercent: 12 } }],
  cost: { used: 8.5, limit: 20, currency: "USD", period: "This month", balance: 11.5 },
  identity: { email: "[email protected]", organization: "Acme", loginMethod: "API key", accountID: "123" },
  subscriptionRenewsAt: "2026-09-01T00:00:00Z",
  dataConfidence: "exact", // exact | estimated | percentOnly | unknown
  details: [{
    title: "Usage summary",
    rows: [{ label: "Requests", value: "1,240", secondaryValue: "Last 30 days" }],
    chart: {
      kind: "bars", // bars | line
      title: "Daily spend",
      unit: "USD",
      points: [{ label: "2026-08-01", value: 4.25 }],
    },
  }],
};

Percentages must be finite and are clamped to 0–100. Window minutes are positive integers. Cost requires finite used and a three-letter uppercase currency. Dates are JavaScript Date values or ISO-8601 strings. Snapshot identity is always scoped to the manifest's instance ID. Data confidence defaults to unknown. Details allow at most 8 sections, 24 rows per section, 120 chart points, and 120 characters per detail string. Wrong types and limit violations fail the whole fetch instead of truncating it. An identity-only snapshot is useful for balance-only or zero-usage provider states and renders its available account, organization, plan/login-method, and account-ID fields in the menu and CLI. An empty object, an empty identity object, or metadata such as confidence and subscription dates without displayable usage or identity remains invalid.

TypeScript

codexbar-plugin.d.ts is the canonical authoring contract for defineProvider, the ctx host API, manifests, and usage snapshots. Bundled plugins may use that contract directly as .ts sources. Scripts/regenerate-plugin-js.sh transpiles them with the vendored Sucrase build into committed sibling .js files; the runtime continues to load only those JavaScript files, so bundled TypeScript has no runtime compilation cost. make check verifies both the TypeScript contract and generated-file freshness.

For bundled-plugin work, run make format after editing TypeScript so the committed JavaScript is regenerated. Do not edit a generated sibling .js file directly.

TypeScript files are transpiled by the selected plugin engine with the bundled Sucrase 3.35.1 build using its typescript transform. Use ordinary type syntax but no module imports, JSX, decorators, or runtime TypeScript features that require module resolution. Transpiled output is cached in ~/Library/Caches/CodexBar/plugins/ under a filename containing the SHA-256 of the source and the Sucrase version. An unchanged file is a cache hit; any source or compiler-version change produces a new key. Transpile failures appear as that plugin's Settings error.

Install, approve, run, and delete

  1. Open Settings → Plugins and choose Install…, or copy one .js/.ts file into the providers directory.
  2. CodexBar validates the source and manifest without network, file, cookie, or secret capabilities.
  3. The approval sheet lists exact normalized origins, auth mode, capabilities, secure setting names, and cookie domains.
  4. For loopback, IP-literal, or .local origins, type every normalized origin exactly before approval.
  5. Enter manifest settings and enable the plugin. Its refresh result appears in its generic menu card.

Approval records live outside plugin files under ~/Library/Application Support/CodexBar/plugin-approvals.json. A change to instance ID, normalized origins, auth mode/header, secure setting names, capabilities, or cookie domains invalidates approval before the next request. There is no bulk approval or import path.

Bundled first-party plugins do not use the interactive plugin-approval flow. The private-network HTTP policy is therefore accepted for bundled code only for LLM Proxy and LiteLLM, whose existing Swift providers already permit exactly those targets. Other bundled providers fail manifest validation if they request that policy.

codexbar plugins list shows locally discovered plugins. codexbar plugins fetch <id> displays the same approval fields and can approve only from an interactive terminal; redirected/headless input fails closed. Browser-cookie plugins are app-only and fail closed in the CLI.

Delete from Settings with Delete…. CodexBar removes the plugin file, matching TypeScript cache output, approval, per-instance settings and secrets, and per-instance usage history. Invalid plugin files are listed with their validation error and can also be deleted.

Security and limitations

Treat a plugin like code you run locally, even though its host capabilities are narrow. Read the manifest and source, verify every origin, and avoid installing files from untrusted repositories. Approval grants the listed origin network authority; DNS changes after approval are outside CodexBar's threat model. Secrets are never placed in URLs or logged, redirects cannot forward authentication, and undeclared settings/cookies/origins fail closed.

Plugins support the macOS app plus the macOS and Linux CLIs. They are excluded from widgets and all built-in-provider-only surfaces (status feeds, token accounts, OAuth, browser automation, storage probes, local cost scanners, and provider specific payloads). Rendering is limited to generic snapshots and declarative details. There are no remote catalogs, downloaded plugins/assets, custom SVGs, imports, arbitrary local I/O, or compatibility fallback from an unknown ID to a built-in provider.