← reports

#4387 · SQLite credential lookup omitted from Go usage collector

Bug Medium priority Medium effort providers · provider-acp · opencode GitHub issue

September 30, 2026 · trusted base 0e7b518f135d43005dae201ef34ebb3001607eb4

Verdict: REPRODUCED · Root-cause confidence: high · reproduction label: confirmed-repro

1. TL;DR

The usage collector can claim that OpenCode Go is unauthenticated when a usable key exists in OpenCode's v2 SQLite credential storage. The collector only considers explicit environment keys, an active Console account, and legacy JSON credentials. A real temporary SQLite database with an active v2 key and empty Console account tables returns unauthenticated without issuing any network request. Giving the same synthetic key to the legacy JSON path makes the test collect all three usage windows. This was repeated from a second clean checkout by the same investigator; no live credentials or real OpenCode account were used.

2. Claims vs findings

Reported claimFindingEvidence
Go usage reports unauthenticated when credentials exist only in v2 storage.Verified at collector boundaryThe added regression fails with the exact unauthenticated result in both clean runs.
v2 stores credentials in a credential table.Verified upstream schemaRead-only inspection of the upstream v2.0.18 tag confirms integration_id, JSON value, and active fields; API keys use type key.
The collector does not query that table.VerifiedTrusted base code only queries account and account_state; the v2-only test makes zero requests.
Legacy JSON credentials work around the lookup gap.Verified with synthetic dataThe legacy control passes using the same synthetic key and returns three windows.
A live v2 connection works in threads while UI and CLI usage fail.Unverified end to endNo real provider session, service account, CLI request, or hosted usage endpoint was exercised. The exact production collector reached by usage collection was tested directly.

3. Environment

4. Minimal reproduction

  1. Clone the trusted repository and check out the exact base commit:
    git clone https://github.com/get-bb/bb.git bb-4387
    cd bb-4387
    git checkout --detach 0e7b518f135d43005dae201ef34ebb3001607eb4
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. Copy the complete regression test embedded in this report to packages/provider-bridge-acp/src/bridge/opencode-usage.issue-4387.test.ts. It creates an active opencode-go key in a real SQLite database matching v2 storage; the old Console account tables are empty, and the first case has no legacy JSON file.
  3. Run the owning package's tests with Turbo:
    pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- --reporter=verbose
  4. Expected: the v2-only case returns status ok and 5 hour, Weekly, and Monthly windows, making one stubbed request. Actual, verbatim from both runs:
    {"scenario":"v2-only","result":{"supported":true,"usage":{"status":"unauthenticated"}},"requests":0}
    Test Files  1 failed | 19 passed (20)
         Tests  1 failed | 350 passed (351)
    The sole failure is the new v2 lookup regression. The legacy control and all 349 pre-existing tests pass. Exit code is 1, as expected for this diagnostic regression.

The test also verifies that the collector leaves the SQLite database byte-for-byte unchanged. All key values are deliberately synthetic; no key value is included in the result log.

Complete regression test
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { readOpenCodeGoUsage } from "./opencode-usage.js";

const syntheticKey = "synthetic-4387-not-a-real-key";
const usageWindow = {
  status: "ok",
  percent: 25,
  resetsAt: "2026-10-01T00:00:00.000Z",
};
const fetchUsage = vi.fn<typeof fetch>();
let directory: string;
let databasePath: string;
let environment: NodeJS.ProcessEnv;

beforeEach(async () => {
  directory = await fs.mkdtemp(path.join(os.tmpdir(), "slopcop-4387-data-"));
  const dataDirectory = path.join(directory, "opencode");
  await fs.mkdir(dataDirectory);
  databasePath = path.join(dataDirectory, "opencode.db");
  environment = { HOME: directory, XDG_DATA_HOME: directory };
  const database = new DatabaseSync(databasePath);
  try {
    database.exec(
      "CREATE TABLE account (id TEXT PRIMARY KEY, email TEXT NOT NULL, url TEXT NOT NULL, access_token TEXT NOT NULL, token_expiry INTEGER); CREATE TABLE account_state (id INTEGER PRIMARY KEY, active_account_id TEXT, active_org_id TEXT); CREATE TABLE credential (id TEXT PRIMARY KEY, integration_id TEXT, label TEXT NOT NULL, value TEXT NOT NULL, connector_id TEXT, method_id TEXT, active INTEGER, time_created INTEGER NOT NULL, time_updated INTEGER)",
    );
    database
      .prepare(
        "INSERT INTO credential (id, integration_id, label, value, active, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
      )
      .run(
        "cred_synthetic4387",
        "opencode-go",
        "synthetic",
        JSON.stringify({ type: "key", key: syntheticKey }),
        1,
        1,
        1,
      );
  } finally {
    database.close();
  }
  fetchUsage.mockReset();
  fetchUsage.mockImplementation(async () =>
    Response.json({
      usage: {
        rolling: usageWindow,
        weekly: usageWindow,
        monthly: usageWindow,
      },
    }),
  );
  vi.stubGlobal("fetch", fetchUsage);
});

afterEach(async () => {
  vi.unstubAllGlobals();
  await fs.rm(directory, { recursive: true, force: true });
});

it("collects usage with an active v2 SQLite key and no legacy credentials", async () => {
  const originalDatabase = await fs.readFile(databasePath);
  const result = await readOpenCodeGoUsage(environment);
  expect(await fs.readFile(databasePath)).toEqual(originalDatabase);
  console.log(
    JSON.stringify({ scenario: "v2-only", result, requests: fetchUsage.mock.calls.length }),
  );
  expect(result).toMatchObject({
    supported: true,
    usage: {
      status: "ok",
      windows: [
        { label: "5 hour", usedPercent: 25 },
        { label: "Weekly", usedPercent: 25 },
        { label: "Monthly", usedPercent: 25 },
      ],
    },
  });
  expect(fetchUsage).toHaveBeenCalledTimes(1);
});

it("collects the same usage when the synthetic key is present in legacy auth", async () => {
  await fs.writeFile(
    path.join(directory, "opencode", "auth.json"),
    JSON.stringify({ "opencode-go": { type: "api", key: syntheticKey } }),
    { mode: 0o600 },
  );
  const result = await readOpenCodeGoUsage(environment);
  console.log(
    JSON.stringify({ scenario: "legacy-control", result, requests: fetchUsage.mock.calls.length }),
  );
  expect(result).toMatchObject({
    supported: true,
    usage: {
      status: "ok",
      windows: [
        { label: "5 hour", usedPercent: 25 },
        { label: "Weekly", usedPercent: 25 },
        { label: "Monthly", usedPercent: 25 },
      ],
    },
  });
  expect(fetchUsage).toHaveBeenCalledTimes(1);
});

5. Root cause

The OpenCode branch in getAcpProviderUsage calls readOpenCodeGoUsage. The provider usage source preserves the collected status for consumers; it does not discover provider credentials itself.

readAccount opens SQLite read-only but only joins account and account_state. Empty account tables return null, regardless of any active v2 credential. readApiKey only considers OPENCODE_API_KEY, OPENCODE_AUTH_CONTENT, and auth.json. With those absent, the collector returns unauthenticated before fetch:

account = env.OPENCODE_API_KEY?.trim() ? null : await readAccount(env);
apiKey = account?.access_token ?? (await readApiKey(env));
if (apiKey === null)
  return { supported: true, usage: { status: "unauthenticated" } };

This is credential discovery incompatibility, not a rejected token or a network error: the v2-only run makes zero requests. Upstream's credential schema stores integration identity, an active flag, and a JSON value. Its value schema distinguishes key credentials from OAuth credentials. The create path marks a newly created integration credential active. A future fix must handle the v2 key discriminator rather than applying the legacy api discriminator unchanged.

6. Proposed fix and automation decision

Add an explicitly validated, read-only v2 key lookup in this collector. Preserve existing environment/account/legacy precedence and endpoint restrictions. Restrict candidate rows to the intended Go or shared integration and the provider's active-selection semantics; do not leak keys or send unrelated/OAuth credentials to the usage service. Handle missing tables and malformed values deliberately, and retain legacy fallback behavior.

Regression coverage should include active versus inactive credentials, Go versus shared integration precedence, key versus OAuth values, invalid JSON, older databases, explicit environment overrides, active Console accounts, and byte-preserving reads. No dependency or migration is necessary to investigate this lookup.

No automatic pull request: extending the credential sources used to authenticate usage requests changes authentication handling, which this automation's simple-fix policy explicitly excludes. The report and failing regression are published for a separately authorized fix. No production changes were attempted, committed, or pushed.

7. Verification

The same investigator created a second clean temporary worktree at 0e7b518f135d43005dae201ef34ebb3001607eb4, checked its initially empty git status, copied only the new regression, and ran a frozen install. The exact command was:

pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run test --filter=@bb/provider-bridge-acp --force -- --reporter=verbose

The second run used newly created temporary data directories and forced execution rather than trusting a cached test result. It reproduced the same unauthenticated result, zero requests, and sole regression failure: 350 passing tests and 1 failing test. This is a repeated reproduction by the same agent, not an independent review. No report correction was needed. No ports were allocated.

The first trusted checkout's full build passed: 62 successful tasks, 62 total. Both package test runs passed all pre-existing tests. The repository was fetched again after reproduction; no newer collector fix appeared on origin/main. All BB code links above were checked against the pinned file line numbers. The report has no screenshots because this is a nonvisual credential-discovery failure.

8. Related issues and pull requests

Issue #1957 concerns usage visibility for this provider; #3499 is a different OpenCode compatibility problem, not this lookup defect. GitHub's issue cross-reference timeline contained no linked PR; an open-PR search for this issue also returned no results on September 30, 2026. No PR code was checked out or executed.

9. Appendix and trust boundary

First run:  Test Files  1 failed | 19 passed (20)
                 Tests  1 failed | 350 passed (351)
Second run: Test Files  1 failed | 19 passed (20)
                 Tests  1 failed | 350 passed (351)
Base build:      Tasks  62 successful, 62 total

The complete reproduction test and relevant output are embedded above. Raw logs and the standalone test are retained outside the public reports repository, in accordance with its publication policy.

Commands used: GitHub read-only issue/property/label/PR queries; fetch canonical main; detached checkout; frozen pnpm install; Turbo build and package tests; detached temporary worktree; read-only upstream schema inspection at a pinned release; git diff --check; and report publication. Original user credentials and unrelated files were not read.

Issue content was treated as untrusted claims. Its proposed implementation was not executed or copied. Test code was authored from trusted BB collector behavior and read-only upstream schema evidence. Type Bug, Priority Medium, Effort Medium, and existing area/plugin labels were preserved.

AGENT GENERATED · direct investigation and repeated clean verification