DriftWatch

Configuration

Every environment variable and typed config field, for both the SDK and the reference server.

DriftWatch has two typed, Zod-validated config objects — one owned by the SDK, one owned by the reference server — plus one file that isn't env-driven at all (model-client.ts). Nothing else in the codebase reads process.env directly; every function takes its config as a plain typed parameter, so you can build one however your app already manages config (env, a parsed .env, a literal object in tests, convict/t3-env/etc).

SDK config — DriftWatchConfig

Schema: packages/sdk/src/config/schema.ts. Env loader: loadDriftWatchConfigFromEnv().

import { DriftWatchConfigSchema, loadDriftWatchConfigFromEnv } from '@driftwatch/sdk';

const config = loadDriftWatchConfigFromEnv(); // convenience: reads process.env

// or build one however you want, validated against the same schema:
const config = DriftWatchConfigSchema.parse({
  telemetry: { serviceName: 'checkout-agent', environment: 'production' },
  agent: { maxSteps: 12 },
  driftDetection: { signozBaseUrl: 'https://signoz.internal' },
});

telemetry

Env varFieldDefaultNotes
OTEL_EXPORTER_OTLP_ENDPOINTotlpEndpointhttp://localhost:4318OTLP/HTTP collector base URL (traces at /v1/traces, metrics at /v1/metrics).
OTEL_SERVICE_NAMEserviceNamedriftwatchReported as OTel service.name on every span/metric.
— (npm_package_version)serviceVersion0.1.0Set automatically by npm/pnpm when run via a package script.
NODE_ENVenvironmentdevelopmentFree-form deployment label (deployment.environment resource attribute).
OTEL_CAPTURE_PAYLOADScapturePayloadstrue (set env to exactly 0 to disable)Whether raw prompt text and tool-call inputs get attached to spans. See security.md.

agent

Env varFieldDefaultNotes
AGENT_MAX_STEPSmaxSteps8Upper bound on the generateText tool-use loop's step count — also your hard cap on cost per /run call.

driftDetection

Env varFieldDefaultNotes
SIGNOZ_URLsignozBaseUrlhttp://localhost:8080SigNoz query-service API base (not the OTLP collector port).
SIGNOZ_API_KEYsignozApiKey''Sent as the SIGNOZ-API-KEY header. Generate in SigNoz UI → Settings → API Keys.

Server config — ServerConfig

Schema: packages/server/src/config/server-config.ts. Specific to the reference Fastify app, not the SDK — kept separate deliberately.

Env varFieldDefaultNotes
PORTport3000
HOSThost0.0.0.0
LOG_LEVELlogLevelinfoFastify/pino log level.
BODY_LIMITbodyLimitBytes131072 (128 KiB)Hard cap on request body size.
TRUST_PROXYtrustProxyfalse ('1'true)Enable when the server sits behind a reverse proxy/load balancer — otherwise request.ip and the local-network auth fallback see the proxy's address, not the client's.
AUTH_TOKENauthToken''Bearer token required on /run and /drift. Empty = local-only mode. See security.md.
MAX_PROMPT_BYTESmaxPromptBytes8192Per-request prompt size cap, independent of BODY_LIMIT.
DRIFT_DRY_RUNdriftDryRunfalse ('1'true)/drift uses built-in fixtures instead of querying SigNoz.
RATE_LIMIT_MAXrateLimitMax30Max requests per client per window on /run and /drift.
RATE_LIMIT_WINDOW_MSrateLimitWindowMs60000Window size in ms for the above.

Boolean env vars are parsed as === '1', not JS truthiness — DRIFT_DRY_RUN=0 is falsy, but so is DRIFT_DRY_RUN=false (any value other than the literal string '1' is false). Same for OTEL_CAPTURE_PAYLOADS, but inverted: only the literal string '0' disables it, anything else (including unset) keeps the default true.

Inline guardrails — agent (SDK, per-request)

Synchronous, per-/run caps enforced inside the generateText loop, so a runaway request aborts before the async drift loop could ever see it. Fields live on DriftWatchConfig.agent (packages/sdk/src/config/schema.ts).

Env varFieldDefaultNotes
AGENT_MAX_TOKENS_PER_TASKmaxTokensPerTask0 (off)Abort a single run once cumulative tokens (summed across steps) cross this cap.
AGENT_MAX_COST_USDmaxCostUsd0 (off)Optional USD cap per run, derived from the per-1k prices below.
AGENT_PRICE_PER_1K_INPUTpricePer1kInput0Price per 1k input tokens, used to derive cost.
AGENT_PRICE_PER_1K_OUTPUTpricePer1kOutput0Price per 1k output tokens, used to derive cost.
AGENT_ON_EXCEEDonExceedstopstop halts the loop; flag finishes the run but marks guardrailTriggered on the result + root span.

Autopilot — ServerConfig (Loop 2)

The drift-triggered remediation loop and its channels. All server-owned.

Env varFieldDefaultNotes
REDIS_URLredisUrl''When set, shared state (approvals, agent state, history, leader lock) lives in Redis so multiple processes coordinate. Unset = in-memory single-process store.
AUTOPILOT_ENABLEDautopilotEnabledfalse ('1'true)Master switch for the autonomous scheduler.
AUTOPILOT_MODEautopilotModeshadowshadow logs intended actions only; enforce dispatches notifications and queues control actions for approval.
AUTOPILOT_SCAN_INTERVAL_MSscanIntervalMs60000How often the scheduler runs a drift cycle (also the leader-lock TTL).
AUTOPILOT_COOLDOWN_MScooldownMs300000Dedup window so the same action does not re-fire repeatedly during a burst of drift.
AUTOPILOT_APPROVAL_TIMEOUT_MSapprovalTimeoutMs600000How long a pending control-action approval waits before the safe default applies.
AUTOPILOT_APPROVAL_TIMEOUT_DECISIONapprovalTimeoutDecisionrejectedSafe default applied on approval timeout.
AUTOPILOT_SWITCH_MODEL_TOswitchModelToMODEL_FALLBACKModel id an approved switch_model action switches the agent to. Must be a key in model-client.ts's registry. See alerts-and-actions.md → Model switching.
AUTOPILOT_POLICIESpoliciesJson''Inline policy JSON (array of {when, do} rules). Takes precedence over the file.
AUTOPILOT_POLICIES_FILEpoliciesFile''Path to a policies.json file. Falls back to a built-in default when both are empty.

Notification / approval channels

Approvals are channel-agnostic — the same pending approval can be resolved from the console, a Slack button, or a Telegram button, because all three mutate the one shared store. See alerts-and-actions.md and security.md.

Env varFieldNotes
SLACK_WEBHOOK_URLslackWebhookUrlIncoming-webhook URL for posting Block Kit messages with Approve/Reject buttons.
SLACK_SIGNING_SECRETslackSigningSecretVerifies X-Slack-Signature (HMAC + timestamp window) on POST /integrations/slack/actions.
TELEGRAM_BOT_TOKENtelegramBotTokenBot token used to send inline-keyboard messages and answerCallbackQuery.
TELEGRAM_CHAT_IDtelegramChatIdChat the bot posts approval prompts to.
TELEGRAM_SECRET_TOKENtelegramSecretTokenVerified against X-Telegram-Bot-Api-Secret-Token on POST /integrations/telegram/webhook.
DRIFT_WEBHOOK_URLwebhookUrlGeneric webhook that receives the raw drift verdict as JSON (notify-only, no buttons).

Policy format

A policy is an array of rules; each rule's when conditions are ANDed, and matching rules contribute their do actions. Actions split into notify (notify_slack/notify_telegram/notify_webhook — dispatched automatically) and control (pause_agent/resume_agent/rollback/throttle/switch_model — which require an approval). Evaluation is a pure function in packages/sdk/src/autopilot/policy.ts.

[
  { "when": { "severity": "high" }, "do": ["notify_slack", "notify_telegram", "pause_agent"] },
  { "when": { "severity": "medium" }, "do": ["notify_slack", "notify_webhook"] },
  { "when": { "tokenSpendDeltaPct": 100 }, "do": ["notify_webhook"] }
]

Model client

Not part of either schema above — deliberately not env-var driven for which provider, only for which model string. There is exactly one file to touch: packages/server/src/config/model-client.ts.

This deployment targets Qwen Cloud (OpenAI-compatible endpoint) via @ai-sdk/openai's createOpenAI factory. Credentials come from .env, never hardcoded:

import { createOpenAI } from '@ai-sdk/openai';
const qwenCloud = createOpenAI({
  baseURL: process.env.QWEN_BASE_URL ?? 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
  apiKey: process.env.QWEN_API_KEY ?? '',
});
export const modelClient = qwenCloud(process.env.MODEL ?? 'qwen3.7-max');

Set QWEN_API_KEY (and optionally MODEL) in your .env. To use a different provider, see server.md → Model client.

Env varDefaultNotes
QWEN_BASE_URLhttps://dashscope-intl.aliyuncs.com/compatible-mode/v1Qwen Cloud OpenAI-compatible base URL.
QWEN_API_KEY''Your Qwen Cloud API key. Required to make real model calls.
MODELqwen3.7-maxPrimary model id — e.g. qwen3.7-max / qwen3.7-plusplus / qwen3.7-plusturbo.
MODEL_FALLBACK''Optional cheaper model id Autopilot can switch the agent to. Setting it registers the model and becomes the default AUTOPILOT_SWITCH_MODEL_TO.

Any other provider — install exactly that one package, swap two lines:

// pnpm --filter @driftwatch/server add @ai-sdk/anthropic
import { anthropic } from '@ai-sdk/anthropic';
export const modelClient = anthropic(process.env.MODEL ?? 'claude-3-5-sonnet-latest');

Same pattern for @ai-sdk/google or any OpenAI-compatible endpoint (Ollama, vLLM, Together, Groq, DeepSeek, ...) — just change baseURL.

The server calls assertModelClientIsConfigured at startup and refuses to boot without a modelClient — there's no implicit fallback beyond what you put in this file.

Skills (tools)

Also not env-driven — runAgentTask takes tools as a plain parameter. The reference server's demo skills live in packages/server/src/tools.ts (get_weather, search_docs); swap in your own DB lookups, HTTP calls, vector search, following the same tool() + withSkillExecutionSpan pattern so calls still show up in metrics.

Full .env.example

See packages/server/.env.example for every variable above in one file, ready to copy to packages/server/.env.

On this page