Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions apps/loopover-ui/src/lib/ams-env-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [
},
{
name: "LOOPOVER_MINER_CONFIG_DIR",
firstReference: "lib/deny-hook-synthesis.ts",
firstReference: "lib/laptop-init.ts",
defaultValue: "",
},
{
Expand All @@ -64,7 +64,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [
{
name: "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB",
firstReference: "lib/deny-hook-synthesis.ts",
defaultValue: "",
defaultValue: null,
},
{
name: "LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL",
Expand Down Expand Up @@ -106,6 +106,11 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [
firstReference: "lib/governor-state.ts",
defaultValue: null,
},
{
name: "LOOPOVER_MINER_LAPTOP_STATE_DB",
firstReference: "lib/laptop-init.ts",
defaultValue: null,
},
{
name: "LOOPOVER_MINER_LOG_LEVEL",
firstReference: "lib/logger.ts",
Expand Down Expand Up @@ -134,7 +139,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [
{
name: "LOOPOVER_MINER_ORB_EXPORT_DB",
firstReference: "lib/orb-export.ts",
defaultValue: "",
defaultValue: null,
},
{
name: "LOOPOVER_MINER_PLAN_STORE_DB",
Expand Down Expand Up @@ -241,12 +246,12 @@ export const AMS_ENV_REFERENCE_MARKDOWN = [
"| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.ts` | (none) |",
"| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.ts` | (none) |",
"| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.ts` | (none) |",
'| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.ts` | `""` |',
'| `LOOPOVER_MINER_CONFIG_DIR` | `lib/laptop-init.ts` | `""` |',
"| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.ts` | (none) |",
'| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.ts` | `""` |',
'| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.ts` | `""` |',
'| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.ts` | `""` |',
'| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | `""` |',
"| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | (none) |",
"| `LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL` | `lib/generate-egress-firewall-config.ts` | (none) |",
'| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.ts` | `""` |',
'| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.ts` | `""` |',
Expand All @@ -255,12 +260,13 @@ export const AMS_ENV_REFERENCE_MARKDOWN = [
"| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.ts` | (none) |",
"| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.ts` | (none) |",
"| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.ts` | (none) |",
"| `LOOPOVER_MINER_LAPTOP_STATE_DB` | `lib/laptop-init.ts` | (none) |",
'| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.ts` | `""` |',
"| `LOOPOVER_MINER_NEON_API_KEY` | `lib/egress-allowlist.ts` | (none) |",
"| `LOOPOVER_MINER_NEON_PARENT_BRANCH_ID` | `lib/egress-allowlist.ts` | (none) |",
"| `LOOPOVER_MINER_NEON_PROJECT_ID` | `lib/egress-allowlist.ts` | (none) |",
'| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.ts` | `""` |',
'| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | `""` |',
"| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | (none) |",
"| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.ts` | (none) |",
"| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.ts` | (none) |",
"| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.ts` | (none) |",
Expand Down
7 changes: 4 additions & 3 deletions packages/loopover-miner/docs/env-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ Generated by `npm run miner:env-reference`. Do not edit manually.
| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.ts` | (none) |
| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.ts` | (none) |
| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.ts` | (none) |
| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.ts` | `""` |
| `LOOPOVER_MINER_CONFIG_DIR` | `lib/laptop-init.ts` | `""` |
| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.ts` | (none) |
| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.ts` | `""` |
| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.ts` | `""` |
| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.ts` | `""` |
| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | `""` |
| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | (none) |
| `LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL` | `lib/generate-egress-firewall-config.ts` | (none) |
| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.ts` | `""` |
| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.ts` | `""` |
Expand All @@ -24,12 +24,13 @@ Generated by `npm run miner:env-reference`. Do not edit manually.
| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.ts` | (none) |
| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.ts` | (none) |
| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.ts` | (none) |
| `LOOPOVER_MINER_LAPTOP_STATE_DB` | `lib/laptop-init.ts` | (none) |
| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.ts` | `""` |
| `LOOPOVER_MINER_NEON_API_KEY` | `lib/egress-allowlist.ts` | (none) |
| `LOOPOVER_MINER_NEON_PARENT_BRANCH_ID` | `lib/egress-allowlist.ts` | (none) |
| `LOOPOVER_MINER_NEON_PROJECT_ID` | `lib/egress-allowlist.ts` | (none) |
| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.ts` | `""` |
| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | `""` |
| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | (none) |
| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.ts` | (none) |
| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.ts` | (none) |
| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.ts` | (none) |
Expand Down
19 changes: 3 additions & 16 deletions packages/loopover-miner/lib/deny-hook-synthesis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
// refresh + maintainer review before any synthesized rule takes effect. Approved rules merge with
// {@link DEFAULT_DENY_RULES}; unapproved proposals never block tool calls. No behavior change.
import { chmodSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { dirname } from "node:path";
import { DatabaseSync } from "node:sqlite";
import {
aggregateBlockerHistory,
Expand All @@ -24,6 +23,7 @@ import {
import type { DenyRuleProposal, SynthesisConfig } from "@loopover/engine";
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
import type { DenyRule } from "./deny-hooks.js";
import { resolveLocalStoreDbPath } from "./local-store.js";
import { DENY_HOOK_SYNTHESIS_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";

// Re-export the pure synthesis helpers from the engine so this module's public API is unchanged after #5667
Expand Down Expand Up @@ -86,20 +86,7 @@ function normalizeApiBaseUrl(apiBaseUrl?: string): string {
}

export function resolveDenyHookSynthesisDbPath(env: Record<string, string | undefined> = process.env): string {
const explicitPath = typeof env.LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB === "string"
? env.LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB.trim()
: "";
if (explicitPath) return explicitPath;

const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string"
? env.LOOPOVER_MINER_CONFIG_DIR.trim()
: "";
if (explicitConfigDir) return join(explicitConfigDir, defaultDbFileName);

const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()
? env.XDG_CONFIG_HOME.trim()
: join(homedir(), ".config");
return join(configHome, "loopover-miner", defaultDbFileName);
return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", env);
}

// `dbPath` is always a real string here: this function's only caller (initDenyHookSynthesisStore) already
Expand Down
5 changes: 3 additions & 2 deletions packages/loopover-miner/lib/laptop-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DatabaseSync } from "node:sqlite";
import { applySchemaMigrations } from "./schema-version.js";
import { reportCliFailure } from "./cli-error.js";
import { resolveGitHubToken } from "./github-token-resolution.js";
import { resolveLocalStoreDbPath } from "./local-store.js";

const githubApiBaseUrl = "https://api.github.com";
const githubApiVersion = "2022-11-28";
Expand Down Expand Up @@ -43,9 +44,9 @@ function resolveMinerStateDir(env: Record<string, string | undefined> = process.
return join(configHome, "loopover-miner");
}

/** Path to the laptop-mode SQLite bootstrap file inside the miner state directory. */
/** Path to the laptop-mode SQLite bootstrap file (shared local-store resolution + optional explicit override). */
export function resolveLaptopStateDbPath(env: Record<string, string | undefined> = process.env): string {
return join(resolveMinerStateDir(env), defaultDbFileName);
return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_LAPTOP_STATE_DB", env);
}

/** Create the state dir and SQLite file. Re-running is idempotent and never clobbers existing rows. */
Expand Down
18 changes: 3 additions & 15 deletions packages/loopover-miner/lib/orb-export.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { chmodSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { dirname } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { createHash, createHmac } from "node:crypto";
import { generateAnonSecret, hmacAnonymize as engineHmacAnonymize } from "@loopover/engine";
import { readPrOutcomes } from "./pr-outcome.js";
import type { NormalizedPrOutcomePayload, PrOutcomeLedgerReader } from "./pr-outcome.js";
import { initEventLedger } from "./event-ledger.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
import { resolveLocalStoreDbPath } from "./local-store.js";

// Optional anonymized Orb telemetry export (#4277, network send wired in #5681). The self-host Orb collector
// (src/selfhost/orb-collector.ts, #1255) is ALWAYS-ON for a maintainer's own instance; a miner runs on a
Expand Down Expand Up @@ -52,19 +52,7 @@ const CURSOR_KEY = "export_cursor";
const defaultDbFileName = "orb-export.sqlite3";

export function resolveOrbExportDbPath(env: Record<string, string | undefined> = process.env): string {
const explicitPath =
typeof env.LOOPOVER_MINER_ORB_EXPORT_DB === "string" ? env.LOOPOVER_MINER_ORB_EXPORT_DB.trim() : "";
if (explicitPath) return explicitPath;

const explicitConfigDir =
typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" ? env.LOOPOVER_MINER_CONFIG_DIR.trim() : "";
if (explicitConfigDir) return join(explicitConfigDir, defaultDbFileName);

const configHome =
typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()
? env.XDG_CONFIG_HOME.trim()
: join(homedir(), ".config");
return join(configHome, "loopover-miner", defaultDbFileName);
return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_ORB_EXPORT_DB", env);
}

// `dbPath` is always a real string here: this function's only caller (openOrbExportStore) already defaults its
Expand Down
25 changes: 21 additions & 4 deletions test/unit/miner-deny-hook-synthesis.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it } from "vitest";
Expand All @@ -21,6 +21,7 @@ import type { DenyRuleProposal } from "../../packages/loopover-engine/src/miner/
// #7525: normalizeRepoFullName is defined in the engine and re-exported unchanged by the miner-lib module
// above; import it from the engine source directly so the guard's src branches are the ones exercised.
import { normalizeRepoFullName } from "../../packages/loopover-engine/src/miner/deny-hook-synthesis";
import { resolveLocalStoreDbPath } from "../../packages/loopover-miner/lib/local-store.js";

const tempDirs: string[] = [];
const stores: Array<{ close(): void }> = [];
Expand Down Expand Up @@ -60,12 +61,28 @@ describe("resolveDenyHookSynthesisDbPath() (#4522)", () => {
"/custom/d.sqlite3",
);
expect(resolveDenyHookSynthesisDbPath({ LOOPOVER_MINER_CONFIG_DIR: "/custom/config" })).toBe(
"/custom/config/deny-hook-synthesis.sqlite3",
join("/custom/config", "deny-hook-synthesis.sqlite3"),
);
expect(resolveDenyHookSynthesisDbPath({ XDG_CONFIG_HOME: "/xdg" })).toBe(
"/xdg/loopover-miner/deny-hook-synthesis.sqlite3",
join("/xdg", "loopover-miner", "deny-hook-synthesis.sqlite3"),
);
expect(resolveDenyHookSynthesisDbPath({})).toMatch(/\/\.config\/loopover-miner\/deny-hook-synthesis\.sqlite3$/);
expect(resolveDenyHookSynthesisDbPath({})).toBe(
join(homedir(), ".config", "loopover-miner", "deny-hook-synthesis.sqlite3"),
);
});

it("stays byte-identical to resolveLocalStoreDbPath for representative env combinations (#8336)", () => {
const cases: Array<Record<string, string | undefined>> = [
{ LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB: "/custom/d.sqlite3" },
{ LOOPOVER_MINER_CONFIG_DIR: "/custom/config" },
{ XDG_CONFIG_HOME: "/xdg" },
{},
];
for (const env of cases) {
expect(resolveDenyHookSynthesisDbPath(env)).toBe(
resolveLocalStoreDbPath("deny-hook-synthesis.sqlite3", "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", env),
);
}
});
});

Expand Down
12 changes: 10 additions & 2 deletions test/unit/miner-laptop-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,23 @@ afterEach(() => {
describe("loopover-miner laptop init (#2329)", () => {
it("resolves the laptop SQLite path from the state-dir override and XDG fallback", () => {
expect(resolveLaptopStateDbPath({ LOOPOVER_MINER_CONFIG_DIR: "/custom/state" }))
.toBe("/custom/state/laptop-state.sqlite3");
.toBe(join("/custom/state", "laptop-state.sqlite3"));
expect(resolveLaptopStateDbPath({ XDG_CONFIG_HOME: "/xdg" }))
.toBe("/xdg/loopover-miner/laptop-state.sqlite3");
.toBe(join("/xdg", "loopover-miner", "laptop-state.sqlite3"));
});

it("falls back to ~/.config when neither LOOPOVER_MINER_CONFIG_DIR nor XDG_CONFIG_HOME is set", () => {
expect(resolveLaptopStateDbPath({})).toBe(join(homedir(), ".config", "loopover-miner", "laptop-state.sqlite3"));
});

it("honors LOOPOVER_MINER_LAPTOP_STATE_DB over config-dir and XDG fallbacks (#8336)", () => {
expect(resolveLaptopStateDbPath({
LOOPOVER_MINER_LAPTOP_STATE_DB: "/custom/laptop-state.sqlite3",
LOOPOVER_MINER_CONFIG_DIR: "/custom/state",
XDG_CONFIG_HOME: "/xdg",
})).toBe("/custom/laptop-state.sqlite3");
});

it("fresh init creates the state dir and SQLite file", () => {
const root = tempRoot();
const env = { LOOPOVER_MINER_CONFIG_DIR: join(root, "state") };
Expand Down
27 changes: 23 additions & 4 deletions test/unit/miner-orb-export.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

Expand All @@ -19,6 +19,7 @@ import {
sendAmsExportBatch,
} from "../../packages/loopover-miner/lib/orb-export.js";
import type { OrbExportOutcome, OrbExportRow } from "../../packages/loopover-miner/lib/orb-export.js";
import { resolveLocalStoreDbPath } from "../../packages/loopover-miner/lib/local-store.js";

let dir: string;
function storePath() {
Expand Down Expand Up @@ -210,10 +211,28 @@ describe("resolveOrbExportDbPath (#4277)", () => {
it("resolves the DB path from env override, miner config dir, XDG config, then the home default", () => {
expect(resolveOrbExportDbPath({ LOOPOVER_MINER_ORB_EXPORT_DB: "/custom/d.sqlite3" })).toBe("/custom/d.sqlite3");
expect(resolveOrbExportDbPath({ LOOPOVER_MINER_CONFIG_DIR: "/custom/config" })).toBe(
"/custom/config/orb-export.sqlite3",
join("/custom/config", "orb-export.sqlite3"),
);
expect(resolveOrbExportDbPath({ XDG_CONFIG_HOME: "/xdg" })).toBe("/xdg/loopover-miner/orb-export.sqlite3");
expect(resolveOrbExportDbPath({})).toMatch(/\/\.config\/loopover-miner\/orb-export\.sqlite3$/);
expect(resolveOrbExportDbPath({ XDG_CONFIG_HOME: "/xdg" })).toBe(
join("/xdg", "loopover-miner", "orb-export.sqlite3"),
);
expect(resolveOrbExportDbPath({})).toBe(
join(homedir(), ".config", "loopover-miner", "orb-export.sqlite3"),
);
});

it("stays byte-identical to resolveLocalStoreDbPath for representative env combinations (#8336)", () => {
const cases: Array<Record<string, string | undefined>> = [
{ LOOPOVER_MINER_ORB_EXPORT_DB: "/custom/d.sqlite3" },
{ LOOPOVER_MINER_CONFIG_DIR: "/custom/config" },
{ XDG_CONFIG_HOME: "/xdg" },
{},
];
for (const env of cases) {
expect(resolveOrbExportDbPath(env)).toBe(
resolveLocalStoreDbPath("orb-export.sqlite3", "LOOPOVER_MINER_ORB_EXPORT_DB", env),
);
}
});
});

Expand Down
Loading