Skip to content
Merged
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
63 changes: 59 additions & 4 deletions apps/loopover-ui/src/lib/selfhost-env-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
},
{
name: "ANTHROPIC_API_KEY",
firstReference: "src/selfhost/ai.ts",
firstReference: "src/selfhost/ai-config.ts",
},
{
name: "BACKUP_ACKNOWLEDGED",
Expand Down Expand Up @@ -261,6 +261,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
name: "LOOPOVER_API_TOKEN",
firstReference: "src/selfhost/preflight.ts",
},
{
name: "LOOPOVER_CENTRAL_POSTHOG_KEY",
firstReference: "src/selfhost/posthog.ts",
},
{
name: "LOOPOVER_ENABLE_PAGERDUTY",
firstReference: "src/services/notify-pagerduty.ts",
Expand Down Expand Up @@ -305,6 +309,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
name: "MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS",
firstReference: "src/selfhost/maintenance-admission.ts",
},
{
name: "MAINTENANCE_ADMISSION_MAX_HOST_LOAD",
firstReference: "src/selfhost/maintenance-admission.ts",
},
{
name: "MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS",
firstReference: "src/selfhost/maintenance-admission.ts",
Expand Down Expand Up @@ -351,7 +359,7 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
},
{
name: "OPENAI_API_KEY",
firstReference: "src/selfhost/ai.ts",
firstReference: "src/selfhost/ai-config.ts",
},
{
name: "OPENAI_COMPATIBLE_AI_API_KEY",
Expand Down Expand Up @@ -433,10 +441,18 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
name: "PAGERDUTY_MIN_SEVERITY",
firstReference: "src/services/notify-pagerduty.ts",
},
{
name: "PAGERDUTY_REPO_COOLDOWN_MINUTES",
firstReference: "src/services/notify-pagerduty.ts",
},
{
name: "PAGERDUTY_REPO_MIN_SEVERITY",
firstReference: "src/services/notify-pagerduty.ts",
},
{
name: "PAGERDUTY_REPO_ROUTING_KEYS",
firstReference: "src/services/notify-pagerduty.ts",
},
{
name: "PAGERDUTY_ROUTING_KEY",
firstReference: "src/services/notify-pagerduty.ts",
Expand Down Expand Up @@ -517,10 +533,30 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
name: "QUEUE_DEAD_LETTER_AUTO_RETRY_MAX_EXTRA_ATTEMPTS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "QUEUE_DEAD_LETTER_REVIVE_INTERVAL_MS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "QUEUE_PROCESSING_TIMEOUT_MS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "QUEUE_RATE_LIMIT_JITTER_MS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "QUEUE_RECOVERY_JITTER_MS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "QUEUE_STARTUP_JITTER_MIN_JOBS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "QUEUE_STARTUP_JITTER_MS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "REDEPLOY_COMPANION_SOCKET_PATH",
firstReference: "src/server.ts",
Expand Down Expand Up @@ -557,6 +593,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
name: "REVIEW_AUDIT_S3_SECRET_ACCESS_KEY",
firstReference: "src/server.ts",
},
{
name: "SCHEDULED_ENQUEUE_JITTER_MS",
firstReference: "src/selfhost/queue-common.ts",
},
{
name: "SELFHOST_BUNDLE_ALL",
firstReference: "scripts/build-selfhost.ts",
Expand All @@ -573,6 +613,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
name: "SETUP_OUTPUT_PATH",
firstReference: "src/server.ts",
},
{
name: "SLACK_REPO_WEBHOOKS",
firstReference: "src/services/notify-discord.ts",
},
{
name: "SLACK_WEBHOOK_URL",
firstReference: "src/services/notify-discord.ts",
Expand Down Expand Up @@ -607,7 +651,7 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `AI_VISION_MODEL` | `src/server.ts` |",
"| `ANTHROPIC_AI_BASE_URL` | `src/selfhost/ai.ts` |",
"| `ANTHROPIC_AI_MODEL` | `src/selfhost/ai.ts` |",
"| `ANTHROPIC_API_KEY` | `src/selfhost/ai.ts` |",
"| `ANTHROPIC_API_KEY` | `src/selfhost/ai-config.ts` |",
"| `BACKUP_ACKNOWLEDGED` | `src/server.ts` |",
"| `BROWSER_WS_ENDPOINT` | `src/selfhost/stubs/puppeteer.ts` |",
"| `CLAUDE_AI_EFFORT` | `src/selfhost/ai.ts` |",
Expand Down Expand Up @@ -646,6 +690,7 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `HOME` | `src/selfhost/ai.ts` |",
"| `INTERNAL_JOB_TOKEN` | `src/selfhost/preflight.ts` |",
"| `LOOPOVER_API_TOKEN` | `src/selfhost/preflight.ts` |",
"| `LOOPOVER_CENTRAL_POSTHOG_KEY` | `src/selfhost/posthog.ts` |",
"| `LOOPOVER_ENABLE_PAGERDUTY` | `src/services/notify-pagerduty.ts` |",
"| `LOOPOVER_ENABLE_UNSAFE_CODEX_REVIEWER` | `src/selfhost/ai.ts` |",
"| `LOOPOVER_MCP_TOKEN` | `src/selfhost/preflight.ts` |",
Expand All @@ -657,6 +702,7 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `MAINTENANCE_ADMISSION_ENABLED` | `src/selfhost/maintenance-admission.ts` |",
"| `MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING` | `src/selfhost/maintenance-admission.ts` |",
"| `MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS` | `src/selfhost/maintenance-admission.ts` |",
"| `MAINTENANCE_ADMISSION_MAX_HOST_LOAD` | `src/selfhost/maintenance-admission.ts` |",
"| `MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS` | `src/selfhost/maintenance-admission.ts` |",
"| `MAINTENANCE_ADMISSION_MAX_LIVE_PENDING` | `src/selfhost/maintenance-admission.ts` |",
"| `MAINTENANCE_ADMISSION_MAX_PENDING` | `src/selfhost/maintenance-admission.ts` |",
Expand All @@ -668,7 +714,7 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `OLLAMA_AI_MODEL` | `src/selfhost/ai.ts` |",
"| `OPENAI_AI_BASE_URL` | `src/selfhost/ai.ts` |",
"| `OPENAI_AI_MODEL` | `src/selfhost/ai.ts` |",
"| `OPENAI_API_KEY` | `src/selfhost/ai.ts` |",
"| `OPENAI_API_KEY` | `src/selfhost/ai-config.ts` |",
"| `OPENAI_COMPATIBLE_AI_API_KEY` | `src/selfhost/ai.ts` |",
"| `OPENAI_COMPATIBLE_AI_BASE_URL` | `src/selfhost/ai.ts` |",
"| `OPENAI_COMPATIBLE_AI_MODEL` | `src/selfhost/ai.ts` |",
Expand All @@ -689,7 +735,9 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `OTEL_TRACES_SAMPLER_ARG` | `src/selfhost/otel.ts` |",
"| `PAGERDUTY_COOLDOWN_MINUTES` | `src/services/notify-pagerduty.ts` |",
"| `PAGERDUTY_MIN_SEVERITY` | `src/services/notify-pagerduty.ts` |",
"| `PAGERDUTY_REPO_COOLDOWN_MINUTES` | `src/services/notify-pagerduty.ts` |",
"| `PAGERDUTY_REPO_MIN_SEVERITY` | `src/services/notify-pagerduty.ts` |",
"| `PAGERDUTY_REPO_ROUTING_KEYS` | `src/services/notify-pagerduty.ts` |",
"| `PAGERDUTY_ROUTING_KEY` | `src/services/notify-pagerduty.ts` |",
"| `PGPOOL_MAX` | `src/selfhost/queue-common.ts` |",
"| `PGVECTOR_ENABLED` | `src/server.ts` |",
Expand All @@ -710,7 +758,12 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `QUEUE_BACKGROUND_CONCURRENCY` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_CONCURRENCY` | `src/selfhost/pg-queue.ts` |",
"| `QUEUE_DEAD_LETTER_AUTO_RETRY_MAX_EXTRA_ATTEMPTS` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_DEAD_LETTER_REVIVE_INTERVAL_MS` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_PROCESSING_TIMEOUT_MS` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_RATE_LIMIT_JITTER_MS` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_RECOVERY_JITTER_MS` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_STARTUP_JITTER_MIN_JOBS` | `src/selfhost/queue-common.ts` |",
"| `QUEUE_STARTUP_JITTER_MS` | `src/selfhost/queue-common.ts` |",
"| `REDEPLOY_COMPANION_SOCKET_PATH` | `src/server.ts` |",
"| `REDEPLOY_COMPANION_TOKEN` | `src/server.ts` |",
"| `REDIS_URL` | `src/selfhost/preflight.ts` |",
Expand All @@ -720,9 +773,11 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
"| `REVIEW_AUDIT_S3_ENDPOINT` | `src/server.ts` |",
"| `REVIEW_AUDIT_S3_REGION` | `src/server.ts` |",
"| `REVIEW_AUDIT_S3_SECRET_ACCESS_KEY` | `src/server.ts` |",
"| `SCHEDULED_ENQUEUE_JITTER_MS` | `src/selfhost/queue-common.ts` |",
"| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.ts` |",
"| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.ts` |",
"| `SELFHOST_SETUP_TOKEN` | `src/selfhost/preflight.ts` |",
"| `SETUP_OUTPUT_PATH` | `src/server.ts` |",
"| `SLACK_REPO_WEBHOOKS` | `src/services/notify-discord.ts` |",
"| `SLACK_WEBHOOK_URL` | `src/services/notify-discord.ts` |",
].join("\n");
90 changes: 90 additions & 0 deletions scripts/gen-selfhost-env-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ function collectEnvReads(source: string, fileName: string): EnvRead[] {
// whose body reads `env[x]` can be resolved back to the concrete var names -- src/selfhost/preflight.ts's
// CRITICAL_SECRET_VARS loop reads four tokens (GITHUB_WEBHOOK_SECRET etc.) this way and nowhere else (#8652).
const literalArrays = collectLiteralStringArrays(sourceFile);
// Same-file wrapper functions that forward a string PARAMETER (not a literal) into an already-recognized
// env-name sink -- src/selfhost/queue-common.ts's `envDurationMs(name)` -> `parsePositiveIntEnv(name, ...)`
// and maintenance-admission.ts's `parsePositiveFloatEnv(name)` -> `process.env[name]`. The var name is only a
// literal at the wrapper's CALL sites, so each such wrapper is treated like a literal-arg helper (#8651).
const localWrappers = collectParamForwardingWrappers(sourceFile);
const visit = (node: ts.Node) => {
if (ts.isPropertyAccessExpression(node) && isEnvContainer(node.expression)) {
addRead(node.name.text);
Expand All @@ -101,6 +106,11 @@ function collectEnvReads(source: string, fileName: string): EnvRead[] {
}
} else if (ts.isForOfStatement(node)) {
for (const name of envReadingForOfArrayLiterals(node, literalArrays)) addRead(name);
} else if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && localWrappers.has(node.expression.text)) {
for (const argIndex of localWrappers.get(node.expression.text)!) {
const arg = node.arguments[argIndex];
if (arg && ts.isStringLiteralLike(arg)) addRead(arg.text);
}
}
ts.forEachChild(node, visit);
};
Expand Down Expand Up @@ -141,6 +151,86 @@ function isProcessEnvNameHelperCall(node: ts.CallExpression): boolean {
return ts.isIdentifier(node.expression) && PROCESS_ENV_NAME_HELPERS.has(node.expression.text) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0]!);
}

// Discover same-file wrapper functions that forward a string parameter, unmodified, into an already-recognized
// env-name sink, and return a map of wrapper name -> the parameter index(es) that carry the env-var name at the
// wrapper's call sites. A "sink" is a direct `env[param]`/`process.env[param]` read, an envString/parse*Env
// helper called with the parameter as its name argument, a literal-arg helper, or another already-discovered
// wrapper (resolved to a fixpoint so wrapper-of-wrapper chains are also caught). Generalizes to any such
// forwarding function -- no wrapper name is special-cased (#8651).
function collectParamForwardingWrappers(sourceFile: ts.SourceFile): Map<string, number[]> {
const candidates: { name: string; params: ts.NodeArray<ts.ParameterDeclaration>; body: ts.Node }[] = [];
const walk = (node: ts.Node) => {
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
candidates.push({ name: node.name.text, params: node.parameters, body: node.body });
} else if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer &&
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
) {
candidates.push({ name: node.name.text, params: node.initializer.parameters, body: node.initializer.body });
}
ts.forEachChild(node, walk);
};
walk(sourceFile);

const wrappers = new Map<string, number[]>();
let changed = true;
while (changed) {
changed = false;
for (const candidate of candidates) {
if (wrappers.has(candidate.name)) continue;
const indexes: number[] = [];
candidate.params.forEach((param, index) => {
if (ts.isIdentifier(param.name) && bodyForwardsParamAsEnvName(candidate.body, param.name.text, wrappers)) indexes.push(index);
});
if (indexes.length > 0) {
wrappers.set(candidate.name, indexes);
changed = true;
}
}
}
return wrappers;
}

// True if `body` forwards the identifier `paramName` as an env-var NAME anywhere: a direct `env[paramName]`
// read, or a call passing `paramName` at the name-argument position of a recognized helper / discovered wrapper.
function bodyForwardsParamAsEnvName(body: ts.Node, paramName: string, knownWrappers: Map<string, number[]>): boolean {
let found = false;
const walk = (node: ts.Node) => {
if (found) return;
if (ts.isElementAccessExpression(node) && isEnvContainer(node.expression) && ts.isIdentifier(node.argumentExpression) && node.argumentExpression.text === paramName) {
found = true;
return;
}
if (ts.isCallExpression(node) && callForwardsParamAsName(node, paramName, knownWrappers)) {
found = true;
return;
}
ts.forEachChild(node, walk);
};
walk(body);
return found;
}

// True if `node` calls a recognized name-helper (envString / process-env-name / literal-arg) or an
// already-discovered wrapper with the identifier `paramName` at that callee's env-var-name argument position.
function callForwardsParamAsName(node: ts.CallExpression, paramName: string, knownWrappers: Map<string, number[]>): boolean {
if (!ts.isIdentifier(node.expression)) return false;
const callee = node.expression.text;
const argIsParam = (index: number): boolean => {
const arg = node.arguments[index];
return arg !== undefined && ts.isIdentifier(arg) && arg.text === paramName;
};
if (callee === "envString") return argIsParam(1);
if (PROCESS_ENV_NAME_HELPERS.has(callee)) return argIsParam(0);
const literalArgIndexes = ENV_NAME_LITERAL_ARG_HELPERS.get(callee);
if (literalArgIndexes) return literalArgIndexes.some((index) => argIsParam(index));
const wrapperArgIndexes = knownWrappers.get(callee);
if (wrapperArgIndexes) return wrapperArgIndexes.some((index) => argIsParam(index));
return false;
}

function isEnvNameLiteralArgHelperCall(node: ts.CallExpression): boolean {
if (!ts.isIdentifier(node.expression)) return false;
const argIndexes = ENV_NAME_LITERAL_ARG_HELPERS.get(node.expression.text);
Expand Down
56 changes: 56 additions & 0 deletions test/unit/selfhost-env-reference-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,59 @@ describe("AI review-pipeline self-host env vars (#6993)", () => {
expect(byName.get("AI_MAX_OUTPUT_TOKENS")).toBe("src/services/ai-review.ts");
});
});

describe("same-file parameter-forwarding wrapper functions (#8651)", () => {
function collectFrom(source: string): string[] {
const root = mkdtempSync(join(tmpdir(), "gt-env-reference-8651-"));
mkdirSync(join(root, "src", "selfhost"), { recursive: true });
writeFileSync(join(root, "src", "selfhost", "wrappers.ts"), source);
return collectSelfHostEnvVars({ rootDir: root }).map((row) => row.name);
}

it("detects a wrapper forwarding its name parameter into parsePositiveIntEnv / process.env / envString / a chained wrapper", () => {
const source = [
// wrapper -> parsePositiveIntEnv (PROCESS_ENV_NAME_HELPERS); the console.log/plainCall decoys exercise the
// non-identifier-callee and unrecognized-callee branches of callForwardsParamAsName.
"function wrapInt(name, fallback) {",
" console.log(name);",
" plainCall(name);",
" return parsePositiveIntEnv(name, { min: 0, fallback });",
"}",
// wrapper -> direct process.env[name] read.
"function wrapFloat(name) {",
" return process.env[name];",
"}",
// wrapper-of-wrapper: forwards into wrapMap (declared AFTER it), forcing a second fixpoint iteration.
"function wrapOuter(k) {",
" return wrapMap(env, k);",
"}",
// wrapper -> envString(env, envName) with the param at arg index 1.
"function wrapMap(env, envName) {",
" return envString(env, envName);",
"}",
// wrapper -> a literal-arg helper (resolveLocalStoreDbPath reads env at arg index 1).
"function wrapLocal(p) {",
" return resolveLocalStoreDbPath(base, p);",
"}",
// arrow-function and function-expression wrapper forms.
"const wrapArrow = (name) => parsePositiveIntEnv(name, { fallback: 2 });",
"const wrapFnExpr = function (name) { return parsePositiveIntEnv(name, { fallback: 9 }); };",
// NOT a wrapper: never forwards its parameter into an env sink.
"function notWrapper(name) {",
" return name.length;",
"}",
// Call sites with literal names -> collected. Non-literal and non-wrapper calls -> ignored.
'const a = wrapInt("ALPHA_MS", 1);',
'const b = wrapFloat("BETA_LOAD");',
'const c = wrapOuter("DELTA_CHAIN");',
'const d = wrapMap(env, "GAMMA_MAP");',
'const e = wrapArrow("EPSILON_MS");',
'const f = wrapLocal("ZETA_DB");',
'const g = wrapFnExpr("ETA_MS");',
"const h = wrapInt(dynamicName, 3);",
'const i = notWrapper("NOT_ENV");',
"",
].join("\n");
expect(collectFrom(source)).toEqual(["ALPHA_MS", "BETA_LOAD", "DELTA_CHAIN", "EPSILON_MS", "ETA_MS", "GAMMA_MAP", "ZETA_DB"]);
});
});
Loading