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
22 changes: 22 additions & 0 deletions core/src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// site to async would be invasive. Browsers should pre-load values into a
// sync map before any engine call.

import { log } from "./logger.js";

type EnvProvider = (name: string) => string | undefined;

let provider: EnvProvider = (name) =>
Expand All @@ -20,3 +22,23 @@ export function setEnvProvider(fn: EnvProvider): void {
export function getEnv(name: string): string | undefined {
return provider(name);
}

/**
* Expands `${VAR}` references in header values via the configured env provider.
* Shared by agent (`target.headers`) and MCP (`target.urlHeaders`) targets so
* both surfaces resolve secrets the same way.
*/
export function expandEnvInHeaders(headers: Record<string, string>): Record<string, string> {
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(headers)) {
out[k] = v.replace(/\$\{([^}]+)\}/g, (_, name) => {
const trimmed = name.trim();
const value = getEnv(trimmed);
if (value === undefined) {
log.warn(`header "${k}" references undefined env var "${trimmed}" — sending it as empty.`);
}
return value ?? "";
});
}
return out;
}
4 changes: 2 additions & 2 deletions core/src/targets/agentTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
captureSessionFromResponse,
resolveSessionPlan,
} from "./httpClient.js";
import { getEnv } from "../lib/env.js";
import { expandEnvInHeaders, getEnv } from "../lib/env.js";
import { invokeLocalTargetScript } from "../lib/localScriptTarget.js";
import {
buildPropagatedHeaders,
Expand Down Expand Up @@ -122,7 +122,7 @@ async function callHttp(

const headers: Record<string, string> = { "Content-Type": "application/json" };
if (resolvedApiKey) headers["Authorization"] = `Bearer ${resolvedApiKey}`;
if (config.headers) Object.assign(headers, config.headers);
if (config.headers) Object.assign(headers, expandEnvInHeaders(config.headers));

const prop = options?.propagation;
const hasPropagation =
Expand Down
3 changes: 2 additions & 1 deletion core/src/targets/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import type { SessionConfig } from "../execute/types.js";
import { expandEnvInHeaders } from "../lib/env.js";

export const REQUEST_TIMEOUT_MS = 30_000;
export const RATE_LIMIT_BACKOFF_MS = 5_000;
Expand Down Expand Up @@ -216,7 +217,7 @@ export async function httpSend(
): Promise<HttpSendResult> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
if (config.headers) Object.assign(headers, config.headers);
if (config.headers) Object.assign(headers, expandEnvInHeaders(config.headers));
if (options.extraHeaders) Object.assign(headers, options.extraHeaders);

let body: Record<string, unknown>;
Expand Down
9 changes: 1 addition & 8 deletions core/src/targets/mcpTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { McpTargetConfig } from "../execute/types.js";
import { connectMcpClient, type McpConnectedClient } from "../mcp-client/createClient.js";
import type { ToolInfo } from "../generate/generateAttacks.js";
import type { ResourceInfo } from "../run/scanResources.js";
import { expandEnvInHeaders } from "../lib/env.js";

export interface McpToolCallResult {
response: string;
Expand All @@ -27,14 +28,6 @@ export async function createMcpTarget(config: McpTargetConfig): Promise<McpTarge
return new ConnectedMcpTarget(conn);
}

function expandEnvInHeaders(headers: Record<string, string>): Record<string, string> {
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(headers)) {
out[k] = v.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name.trim()] ?? "");
}
return out;
}

function buildServerConfig(config: McpTargetConfig): Parameters<typeof connectMcpClient>[0] {
if (config.transport === "stdio") {
if (!config.command) throw new Error("MCP stdio target requires command");
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ See [telemetry.md](telemetry.md) for Langfuse and Netra setup, config fields, an
| `target.requestFormat` | For HTTP | `"openai"`, `"json"`, or `"auto"` (default). Ignored when `target.stateful` is `false` (stateless mode forces OpenAI shape). |
| `target.model` | For HTTP / openai | Model name to send in the request body. |
| `target.apiKeyEnv` | No | Env var **name** holding the target's API key (e.g. `"TARGET_API_KEY"`). Never put the raw key here. |
| `target.headers` | No | Custom HTTP headers (e.g. `{"X-Api-Key": "secret"}`). Merged with built-in headers. |
| `target.headers` | No | Custom HTTP headers (e.g. `{"X-Api-Key": "${TARGET_API_KEY}"}`). Merged with built-in headers. Values support `${VAR}` expansion — use it instead of pasting secrets in directly. |
| `target.promptPath` | No | Dot-path for the prompt field (e.g. `"input.message"`). Defaults to top-level `prompt`. |
| `target.responsePath` | No | Dot-path to extract the reply (e.g. `"data.reply"`). Falls back to built-in chain. |
| `target.sessionIdField` | No | Legacy shorthand for `session.send = { in: "body", name: … }` (client-owned, body). Still honored. |
Expand Down
3 changes: 3 additions & 0 deletions runners/cli/src/ui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export interface UiServerHandle {

function resolveStaticDir(): string {
const candidates = [
// Bundled prod layout: esbuild flattens src/ into a single dist/index.js,
// so __dirname is already "dist" — ui-static sits right next to it.
path.join(__dirname, "ui-static"),
path.join(__dirname, "..", "ui-static"),
path.join(__dirname, "..", "..", "ui-static"),
path.join(process.cwd(), "dist", "ui-static"),
Expand Down
Loading