@driftgard/node 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,10 +27,31 @@ const result = await dg.evaluate({
27
27
  if (result.evaluation.allowed) {
28
28
  console.log("Safe to return to user");
29
29
  } else {
30
+ // Use the fallback message if configured in your control pack
31
+ if (result.fallback) {
32
+ console.log("Show to user:", result.fallback.message);
33
+ }
30
34
  console.log("Blocked:", result.evaluation.violations);
31
35
  }
32
36
  ```
33
37
 
38
+ ## Conversation tracking
39
+
40
+ Link evaluations within an agent session using `session_id` and `parent_evaluation_id`:
41
+
42
+ ```typescript
43
+ const result = await dg.evaluate({
44
+ project_id: "your-project-id",
45
+ prompt: "Transfer $500 to account 12345",
46
+ response: "I've initiated the transfer.",
47
+ model_id: "gpt-4o",
48
+ session_id: "sess_abc123", // groups evals in a conversation
49
+ parent_evaluation_id: "eval_prev_id", // chains to the previous eval
50
+ });
51
+ ```
52
+
53
+ This enables chain depth protection (prevents infinite agent loops) and lets you trace evaluation lineage in the dashboard.
54
+
34
55
  ## A/B experiments
35
56
 
36
57
  Tag evaluations with an `experiment_id` to compare governance metrics across models:
@@ -67,13 +88,15 @@ const result = await dg.evaluate({
67
88
  ```
68
89
 
69
90
  All fields in `usage` are optional. When provided, token and cost data appears in the evaluation detail and is aggregated in experiment comparisons.
70
- ```
71
91
 
72
92
  ## Features
73
93
 
74
94
  - Single `evaluate()` method — send prompt/response, get verdict
95
+ - Failure mode: `fail-open` or `fail-closed` when API is unreachable
96
+ - Circuit breaker: skips API after consecutive failures, auto-recovers
97
+ - Idempotency: deduplicates retried requests via `x-idempotency-key`
75
98
  - Auto-retry with exponential backoff on 5xx and network errors
76
- - Typed errors: `AuthError`, `RateLimitError`, `FeatureNotAvailableError`
99
+ - Typed errors: `AuthError`, `RateLimitError`, `FeatureNotAvailableError`, `ChainDepthExceededError`
77
100
  - Full TypeScript types for requests and responses
78
101
  - Zero dependencies (uses native `fetch`)
79
102
 
@@ -85,13 +108,39 @@ const dg = new Driftgard({
85
108
  baseUrl: "https://api.driftgard.com", // optional
86
109
  timeout: 30000, // optional, ms (default 30s)
87
110
  maxRetries: 2, // optional (default 2)
111
+ failureMode: "open", // "open" = allow if API down, "closed" = block (default "open")
112
+ circuitBreaker: {
113
+ threshold: 5, // open circuit after 5 consecutive failures (default 5)
114
+ resetTimeoutMs: 30000, // try again after 30s (default 30000)
115
+ },
88
116
  });
89
117
  ```
90
118
 
119
+ ## Failure mode & circuit breaker
120
+
121
+ The SDK never throws on network/server errors during `evaluate()`. Instead, it returns a synthetic response:
122
+
123
+ ```typescript
124
+ const result = await dg.evaluate({ ... });
125
+
126
+ // Check where the decision came from
127
+ console.log(result.decision_source);
128
+ // "policy" — normal API evaluation
129
+ // "failure_mode" — API unreachable, failureMode applied
130
+ // "circuit_open" — circuit breaker open, failureMode applied
131
+ // "idempotency_cache" — duplicate request, cached result returned
132
+
133
+ // Monitor circuit breaker state
134
+ console.log(dg.circuitBreakerState);
135
+ // { state: "closed", failures: 0, openedAt: 0 }
136
+ ```
137
+
138
+ With `failureMode: "open"` (default), the SDK allows requests through when Driftgard is unavailable. With `failureMode: "closed"`, it blocks them with a fallback message.
139
+
91
140
  ## Error handling
92
141
 
93
142
  ```typescript
94
- import { Driftgard, AuthError, RateLimitError, FeatureNotAvailableError } from "@driftgard/node";
143
+ import { Driftgard, AuthError, RateLimitError, FeatureNotAvailableError, ChainDepthExceededError } from "@driftgard/node";
95
144
 
96
145
  try {
97
146
  const result = await dg.evaluate({ ... });
@@ -100,6 +149,9 @@ try {
100
149
  // Invalid or revoked API key (401)
101
150
  } else if (e instanceof RateLimitError) {
102
151
  // Too many requests (429)
152
+ } else if (e instanceof ChainDepthExceededError) {
153
+ // Agent loop detected — chain depth exceeded (429)
154
+ console.log(`Depth ${e.depth} exceeds max ${e.max}`);
103
155
  } else if (e instanceof FeatureNotAvailableError) {
104
156
  // API evaluate requires Compliance+ tier (403)
105
157
  }
package/dist/errors.d.ts CHANGED
@@ -13,3 +13,8 @@ export declare class FeatureNotAvailableError extends DriftgardError {
13
13
  tier: string;
14
14
  constructor(message?: string, tier?: string);
15
15
  }
16
+ export declare class ChainDepthExceededError extends DriftgardError {
17
+ depth: number;
18
+ max: number;
19
+ constructor(message?: string, depth?: number, max?: number);
20
+ }
package/dist/errors.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
3
+ exports.ChainDepthExceededError = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
4
4
  class DriftgardError extends Error {
5
5
  constructor(message, status, code) {
6
6
  super(message);
@@ -32,3 +32,12 @@ class FeatureNotAvailableError extends DriftgardError {
32
32
  }
33
33
  }
34
34
  exports.FeatureNotAvailableError = FeatureNotAvailableError;
35
+ class ChainDepthExceededError extends DriftgardError {
36
+ constructor(message = "Evaluation chain depth exceeded", depth = 0, max = 0) {
37
+ super(message, 429, "chain_depth_exceeded");
38
+ this.name = "ChainDepthExceededError";
39
+ this.depth = depth;
40
+ this.max = max;
41
+ }
42
+ }
43
+ exports.ChainDepthExceededError = ChainDepthExceededError;
package/dist/index.d.ts CHANGED
@@ -1,17 +1,34 @@
1
1
  import { DriftgardConfig, EvaluateRequest, EvaluateResponse } from "./types";
2
- export { DriftgardConfig, EvaluateRequest, EvaluateResponse } from "./types";
3
- export { Violation, EvaluationResult, HitlInfo } from "./types";
4
- export { DriftgardError, AuthError, RateLimitError, FeatureNotAvailableError } from "./errors";
2
+ export { DriftgardConfig, EvaluateRequest, EvaluateResponse, CircuitBreakerConfig } from "./types";
3
+ export { Violation, EvaluationResult, FallbackResponse, HitlInfo } from "./types";
4
+ export { DriftgardError, AuthError, RateLimitError, FeatureNotAvailableError, ChainDepthExceededError } from "./errors";
5
+ type CircuitState = "closed" | "open" | "half-open";
5
6
  export declare class Driftgard {
6
7
  private apiKey;
7
8
  private baseUrl;
8
9
  private timeout;
9
10
  private maxRetries;
11
+ private failureMode;
12
+ private cbThreshold;
13
+ private cbResetMs;
14
+ private cbState;
15
+ private cbFailures;
16
+ private cbOpenedAt;
10
17
  constructor(config: DriftgardConfig);
11
18
  /**
12
19
  * Evaluate a prompt/response pair against your active control pack.
20
+ * Handles circuit breaker and failure mode automatically.
13
21
  */
14
22
  evaluate(req: EvaluateRequest): Promise<EvaluateResponse>;
23
+ /** Generate a synthetic response based on failure mode. */
24
+ private syntheticResponse;
25
+ /** Get current circuit breaker state (for observability). */
26
+ get circuitBreakerState(): {
27
+ state: CircuitState;
28
+ failures: number;
29
+ openedAt: number;
30
+ };
31
+ private generateIdempotencyKey;
15
32
  private post;
16
33
  private delay;
17
34
  }
package/dist/index.js CHANGED
@@ -1,40 +1,130 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Driftgard = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
3
+ exports.Driftgard = exports.ChainDepthExceededError = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
4
4
  const errors_1 = require("./errors");
5
5
  var errors_2 = require("./errors");
6
6
  Object.defineProperty(exports, "DriftgardError", { enumerable: true, get: function () { return errors_2.DriftgardError; } });
7
7
  Object.defineProperty(exports, "AuthError", { enumerable: true, get: function () { return errors_2.AuthError; } });
8
8
  Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_2.RateLimitError; } });
9
9
  Object.defineProperty(exports, "FeatureNotAvailableError", { enumerable: true, get: function () { return errors_2.FeatureNotAvailableError; } });
10
+ Object.defineProperty(exports, "ChainDepthExceededError", { enumerable: true, get: function () { return errors_2.ChainDepthExceededError; } });
10
11
  const DEFAULT_BASE_URL = "https://api.driftgard.com";
11
12
  const DEFAULT_TIMEOUT = 30000;
12
13
  const DEFAULT_MAX_RETRIES = 2;
13
14
  const RETRY_DELAY_MS = 500;
15
+ const DEFAULT_CB_THRESHOLD = 5;
16
+ const DEFAULT_CB_RESET_MS = 30000;
14
17
  class Driftgard {
15
18
  constructor(config) {
19
+ this.cbState = "closed";
20
+ this.cbFailures = 0;
21
+ this.cbOpenedAt = 0;
16
22
  if (!config.apiKey)
17
23
  throw new Error("apiKey is required");
18
24
  this.apiKey = config.apiKey;
19
25
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
20
26
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
21
27
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
28
+ this.failureMode = config.failureMode ?? "open";
29
+ this.cbThreshold = config.circuitBreaker?.threshold ?? DEFAULT_CB_THRESHOLD;
30
+ this.cbResetMs = config.circuitBreaker?.resetTimeoutMs ?? DEFAULT_CB_RESET_MS;
22
31
  }
23
32
  /**
24
33
  * Evaluate a prompt/response pair against your active control pack.
34
+ * Handles circuit breaker and failure mode automatically.
25
35
  */
26
36
  async evaluate(req) {
27
- return this.post("/audit/evaluate", {
37
+ const idempotencyKey = req.idempotency_key || this.generateIdempotencyKey();
38
+ // Circuit breaker: if open, check if reset timeout has passed
39
+ if (this.cbState === "open") {
40
+ if (Date.now() - this.cbOpenedAt >= this.cbResetMs) {
41
+ this.cbState = "half-open";
42
+ }
43
+ else {
44
+ // Circuit is open — apply failure mode without calling API
45
+ return this.syntheticResponse(req, "circuit_open");
46
+ }
47
+ }
48
+ try {
49
+ const result = await this.post("/audit/evaluate", {
50
+ project_id: req.project_id,
51
+ prompt: req.prompt,
52
+ response: req.response,
53
+ model_id: req.model_id,
54
+ timestamp: req.timestamp || new Date().toISOString(),
55
+ ...(req.experiment_id ? { experiment_id: req.experiment_id } : {}),
56
+ ...(req.session_id ? { session_id: req.session_id } : {}),
57
+ ...(req.parent_evaluation_id ? { parent_evaluation_id: req.parent_evaluation_id } : {}),
58
+ ...(req.control_pack_id ? { control_pack_id: req.control_pack_id } : {}),
59
+ ...(req.control_pack_version ? { control_pack_version: req.control_pack_version } : {}),
60
+ ...(req.dry_run ? { dry_run: req.dry_run } : {}),
61
+ ...(req.usage ? { usage: req.usage } : {}),
62
+ }, idempotencyKey);
63
+ // Success — reset circuit breaker
64
+ this.cbFailures = 0;
65
+ this.cbState = "closed";
66
+ return result;
67
+ }
68
+ catch (e) {
69
+ // Auth, rate limit, chain depth, feature errors are real errors — don't trigger circuit breaker
70
+ if (e instanceof errors_1.AuthError || e instanceof errors_1.RateLimitError ||
71
+ e instanceof errors_1.ChainDepthExceededError || e instanceof errors_1.FeatureNotAvailableError) {
72
+ throw e;
73
+ }
74
+ // Network/server error — increment circuit breaker
75
+ this.cbFailures++;
76
+ if (this.cbFailures >= this.cbThreshold) {
77
+ this.cbState = "open";
78
+ this.cbOpenedAt = Date.now();
79
+ }
80
+ // Apply failure mode
81
+ return this.syntheticResponse(req, "failure_mode");
82
+ }
83
+ }
84
+ /** Generate a synthetic response based on failure mode. */
85
+ syntheticResponse(req, source) {
86
+ const allowed = this.failureMode === "open";
87
+ return {
88
+ ok: true,
28
89
  project_id: req.project_id,
29
- prompt: req.prompt,
30
- response: req.response,
31
- model_id: req.model_id,
32
- timestamp: req.timestamp || new Date().toISOString(),
33
- ...(req.experiment_id ? { experiment_id: req.experiment_id } : {}),
34
- ...(req.usage ? { usage: req.usage } : {}),
35
- });
90
+ evaluation_id: `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
91
+ active_control_pack_id: "",
92
+ active_control_pack_version: 0,
93
+ data_mode: "unknown",
94
+ telemetry_mode: "unknown",
95
+ execution_mode: "sync",
96
+ decision_action: allowed ? "log_only" : "enforce",
97
+ decision_source: source,
98
+ evaluation: {
99
+ allowed,
100
+ risk_score: allowed ? 0 : 10,
101
+ violations: allowed ? [] : [{
102
+ clause_id: "DRIFTGARD_UNAVAILABLE",
103
+ severity: "critical",
104
+ category: "system",
105
+ reason: `Driftgard API unavailable — fail-closed policy applied (${source})`,
106
+ }],
107
+ },
108
+ ...(allowed ? {} : {
109
+ fallback: {
110
+ message: "Service temporarily unavailable. Request blocked by fail-closed policy.",
111
+ code: "DRIFTGARD_UNAVAILABLE",
112
+ action: "show_message",
113
+ source,
114
+ escalated: false,
115
+ retry_after_ms: this.cbState === "open" ? this.cbResetMs : null,
116
+ },
117
+ }),
118
+ };
119
+ }
120
+ /** Get current circuit breaker state (for observability). */
121
+ get circuitBreakerState() {
122
+ return { state: this.cbState, failures: this.cbFailures, openedAt: this.cbOpenedAt };
36
123
  }
37
- async post(path, body, attempt = 0) {
124
+ generateIdempotencyKey() {
125
+ return `idem_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
126
+ }
127
+ async post(path, body, idempotencyKey, attempt = 0) {
38
128
  const controller = new AbortController();
39
129
  const timer = setTimeout(() => controller.abort(), this.timeout);
40
130
  try {
@@ -43,14 +133,20 @@ class Driftgard {
43
133
  headers: {
44
134
  "Content-Type": "application/json",
45
135
  "x-api-key": this.apiKey,
136
+ ...(idempotencyKey ? { "x-idempotency-key": idempotencyKey } : {}),
46
137
  },
47
138
  body: JSON.stringify(body),
48
139
  signal: controller.signal,
49
140
  });
50
141
  if (res.status === 401)
51
142
  throw new errors_1.AuthError();
52
- if (res.status === 429)
143
+ if (res.status === 429) {
144
+ const payload = await res.json().catch(() => ({}));
145
+ if (payload?.error === "chain_depth_exceeded") {
146
+ throw new errors_1.ChainDepthExceededError(payload.message, payload.depth, payload.max);
147
+ }
53
148
  throw new errors_1.RateLimitError();
149
+ }
54
150
  if (res.status === 403) {
55
151
  const payload = await res.json().catch(() => ({}));
56
152
  if (payload?.error === "feature_not_available") {
@@ -61,7 +157,7 @@ class Driftgard {
61
157
  // Retry on 5xx
62
158
  if (res.status >= 500 && attempt < this.maxRetries) {
63
159
  await this.delay(RETRY_DELAY_MS * Math.pow(2, attempt));
64
- return this.post(path, body, attempt + 1);
160
+ return this.post(path, body, idempotencyKey, attempt + 1);
65
161
  }
66
162
  const payload = await res.json();
67
163
  if (!res.ok) {
@@ -75,7 +171,7 @@ class Driftgard {
75
171
  // Network error — retry
76
172
  if (attempt < this.maxRetries) {
77
173
  await this.delay(RETRY_DELAY_MS * Math.pow(2, attempt));
78
- return this.post(path, body, attempt + 1);
174
+ return this.post(path, body, idempotencyKey, attempt + 1);
79
175
  }
80
176
  const msg = e instanceof Error ? e.message : String(e);
81
177
  throw new errors_1.DriftgardError(msg, 0, "network_error");
package/dist/types.d.ts CHANGED
@@ -1,8 +1,18 @@
1
+ export interface CircuitBreakerConfig {
2
+ /** Number of consecutive failures before opening the circuit. Default 5. */
3
+ threshold?: number;
4
+ /** Milliseconds to wait before trying again after circuit opens. Default 30000. */
5
+ resetTimeoutMs?: number;
6
+ }
1
7
  export interface DriftgardConfig {
2
8
  apiKey: string;
3
9
  baseUrl?: string;
4
10
  timeout?: number;
5
11
  maxRetries?: number;
12
+ /** What to do when Driftgard API is unreachable. "open" = allow, "closed" = block. Default "open". */
13
+ failureMode?: "open" | "closed";
14
+ /** Circuit breaker config. Skips API calls after consecutive failures. */
15
+ circuitBreaker?: CircuitBreakerConfig;
6
16
  }
7
17
  export interface EvaluateRequest {
8
18
  project_id: string;
@@ -11,6 +21,13 @@ export interface EvaluateRequest {
11
21
  model_id: string;
12
22
  timestamp?: string;
13
23
  experiment_id?: string;
24
+ session_id?: string;
25
+ parent_evaluation_id?: string;
26
+ control_pack_id?: string;
27
+ control_pack_version?: number;
28
+ dry_run?: boolean;
29
+ /** Caller-provided idempotency key. If omitted, the SDK generates one. */
30
+ idempotency_key?: string;
14
31
  usage?: {
15
32
  prompt_tokens?: number;
16
33
  completion_tokens?: number;
@@ -29,9 +46,15 @@ export interface EvaluationResult {
29
46
  allowed: boolean;
30
47
  risk_score: number;
31
48
  violations: Violation[];
49
+ /** Original policy decision before execution_mode override (only present when overridden). */
50
+ policy_allowed?: boolean;
32
51
  flags?: {
33
52
  pii_detected?: boolean;
34
53
  pii_in_prompt?: boolean;
54
+ secret_detected?: boolean;
55
+ adversarial_input?: boolean;
56
+ adversarial_score?: number;
57
+ dlp_findings_count?: number;
35
58
  meta_bypass_detected?: boolean;
36
59
  judge_used?: boolean;
37
60
  judge_called?: boolean;
@@ -39,6 +62,14 @@ export interface EvaluationResult {
39
62
  hard_blocked?: boolean;
40
63
  };
41
64
  }
65
+ export interface FallbackResponse {
66
+ message: string;
67
+ code: string;
68
+ action: string;
69
+ source: string;
70
+ escalated: boolean;
71
+ retry_after_ms?: number | null;
72
+ }
42
73
  export interface HitlInfo {
43
74
  queued: boolean;
44
75
  hitl_id?: string;
@@ -53,6 +84,23 @@ export interface EvaluateResponse {
53
84
  active_control_pack_version: number;
54
85
  data_mode: string;
55
86
  telemetry_mode: string;
56
- hitl: HitlInfo;
87
+ /** Server-side execution mode: sync, async, or hybrid. */
88
+ execution_mode?: string;
89
+ /** How the decision was applied: "enforce" or "log_only". */
90
+ decision_action?: string;
91
+ /** Where the decision came from: "policy", "failure_mode", "circuit_open". */
92
+ decision_source?: string;
93
+ dry_run?: boolean;
94
+ experiment_id?: string;
95
+ session_id?: string;
96
+ parent_evaluation_id?: string;
97
+ usage?: {
98
+ prompt_tokens?: number;
99
+ completion_tokens?: number;
100
+ total_tokens?: number;
101
+ cost?: number;
102
+ };
103
+ hitl?: HitlInfo;
57
104
  evaluation: EvaluationResult;
105
+ fallback?: FallbackResponse;
58
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftgard/node",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Official Driftgard Node.js SDK — evaluate LLM interactions against your compliance policy",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",