@highflame/policy 2.2.41 → 2.2.43

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.
Files changed (32) hide show
  1. package/_schemas/agent_ops/context.json +0 -42
  2. package/_schemas/agent_ops/schema.cedarschema +44 -6
  3. package/_schemas/ai_gateway/context.json +300 -0
  4. package/_schemas/ai_gateway/schema.cedarschema +107 -3
  5. package/_schemas/guardrails/context.json +0 -36
  6. package/_schemas/guardrails/detectors.json +0 -1
  7. package/_schemas/guardrails/schema.cedarschema +43 -5
  8. package/_schemas/guardrails/templates/dual_attribution.cedar +110 -0
  9. package/_schemas/guardrails/templates/multi_turn_trajectory.cedar +116 -0
  10. package/_schemas/guardrails/templates/profiles/output_protection/credentials.cedar +35 -0
  11. package/_schemas/guardrails/templates/profiles/output_protection/data_leakage.cedar +63 -0
  12. package/_schemas/guardrails/templates/profiles/output_protection/harmful_content.cedar +58 -0
  13. package/_schemas/guardrails/templates/profiles/output_protection/injection_carried_back.cedar +39 -0
  14. package/_schemas/guardrails/templates/session_risk_accumulation.cedar +103 -0
  15. package/_schemas/guardrails/templates/templates.json +157 -11
  16. package/_schemas/overwatch/context.json +65 -0
  17. package/_schemas/overwatch/detectors.json +47 -0
  18. package/_schemas/overwatch/schema.cedarschema +90 -0
  19. package/dist/agent_ops-context.gen.d.ts +1 -2
  20. package/dist/agent_ops-context.gen.js +0 -2
  21. package/dist/ai_gateway-entities.gen.js +5 -1
  22. package/dist/guardrails-context.gen.d.ts +1 -2
  23. package/dist/guardrails-context.gen.js +0 -2
  24. package/dist/guardrails-defaults.gen.js +751 -11
  25. package/dist/overwatch-context.gen.d.ts +3 -1
  26. package/dist/overwatch-context.gen.js +4 -0
  27. package/dist/overwatch-detectors.gen.js +16 -0
  28. package/dist/parser.d.ts +2 -1
  29. package/dist/parser.js +124 -37
  30. package/dist/service-schemas.gen.d.ts +4 -4
  31. package/dist/service-schemas.gen.js +354 -32
  32. package/package.json +1 -1
@@ -5,6 +5,8 @@
5
5
  * Overwatch Cedar schema and are used at policy evaluation time.
6
6
  */
7
7
  export declare const OverwatchContextKey: {
8
+ readonly BudgetExceeded: "budget_exceeded";
9
+ readonly BudgetRemainingPct: "budget_remaining_pct";
8
10
  readonly Content: "content";
9
11
  readonly CrimeScore: "crime_score";
10
12
  readonly Cwd: "cwd";
@@ -99,4 +101,4 @@ export type OverwatchContextKey = (typeof OverwatchContextKey)[keyof typeof Over
99
101
  * The full set of authorable context attribute keys for Overwatch.
100
102
  * Iterate this to enumerate the authorable surface (cockpit, conformance).
101
103
  */
102
- export declare const OverwatchContextKeys: readonly ["content", "crime_score", "cwd", "detected_threats", "event", "exec_target_paths", "exit_code", "hate_speech_score", "highest_severity", "indirect_injection_score", "injection_deep_context_score", "injection_pulse_score", "injection_score", "invisible_chars_detected", "invisible_chars_score", "jailbreak_deep_context_score", "jailbreak_pulse_score", "jailbreak_score", "loop_count", "loop_detected", "loop_tool", "malicious_package_detected", "malicious_package_score", "malicious_packages", "max_threat_severity", "mcp_config_risk", "mcp_risk_score", "mcp_server", "mcp_server_verified", "mcp_tool", "package_advisory_count", "package_check_status", "package_ecosystems", "package_install_detected", "package_names", "package_risk_score", "packages_checked", "path", "pattern_type", "pii_count", "pii_detected", "pii_score", "pii_types", "privilege_scope", "profanity_score", "prompt_text", "read_target_paths", "resolved_target_paths", "response_content", "role", "rug_pull_detected", "rug_pull_score", "secret_count", "secret_types", "secrets_detected", "sequence_risk", "session_command_injection", "session_cumulative_risk_score", "session_injection_detected", "session_max_command_injection_score", "session_max_injection_score", "session_max_jailbreak_score", "session_max_pii_score", "session_max_secret_score", "session_pii_detected", "session_pii_types", "session_secret_types", "session_secrets_detected", "session_threat_turns", "sexual_score", "source", "suspicious_pattern", "threat_categories", "threat_count", "tool_category", "tool_is_builtin", "tool_is_sensitive", "tool_name", "tool_operation_classes", "tool_poisoning_detected", "tool_poisoning_score", "tool_risk_score", "unresolved_target", "user_email", "violence_score", "weapons_score", "workspace_root", "write_target_paths"];
104
+ export declare const OverwatchContextKeys: readonly ["budget_exceeded", "budget_remaining_pct", "content", "crime_score", "cwd", "detected_threats", "event", "exec_target_paths", "exit_code", "hate_speech_score", "highest_severity", "indirect_injection_score", "injection_deep_context_score", "injection_pulse_score", "injection_score", "invisible_chars_detected", "invisible_chars_score", "jailbreak_deep_context_score", "jailbreak_pulse_score", "jailbreak_score", "loop_count", "loop_detected", "loop_tool", "malicious_package_detected", "malicious_package_score", "malicious_packages", "max_threat_severity", "mcp_config_risk", "mcp_risk_score", "mcp_server", "mcp_server_verified", "mcp_tool", "package_advisory_count", "package_check_status", "package_ecosystems", "package_install_detected", "package_names", "package_risk_score", "packages_checked", "path", "pattern_type", "pii_count", "pii_detected", "pii_score", "pii_types", "privilege_scope", "profanity_score", "prompt_text", "read_target_paths", "resolved_target_paths", "response_content", "role", "rug_pull_detected", "rug_pull_score", "secret_count", "secret_types", "secrets_detected", "sequence_risk", "session_command_injection", "session_cumulative_risk_score", "session_injection_detected", "session_max_command_injection_score", "session_max_injection_score", "session_max_jailbreak_score", "session_max_pii_score", "session_max_secret_score", "session_pii_detected", "session_pii_types", "session_secret_types", "session_secrets_detected", "session_threat_turns", "sexual_score", "source", "suspicious_pattern", "threat_categories", "threat_count", "tool_category", "tool_is_builtin", "tool_is_sensitive", "tool_name", "tool_operation_classes", "tool_poisoning_detected", "tool_poisoning_score", "tool_risk_score", "unresolved_target", "user_email", "violence_score", "weapons_score", "workspace_root", "write_target_paths"];
@@ -7,6 +7,8 @@
7
7
  * Overwatch Cedar schema and are used at policy evaluation time.
8
8
  */
9
9
  export const OverwatchContextKey = {
10
+ BudgetExceeded: 'budget_exceeded',
11
+ BudgetRemainingPct: 'budget_remaining_pct',
10
12
  Content: 'content',
11
13
  CrimeScore: 'crime_score',
12
14
  Cwd: 'cwd',
@@ -101,6 +103,8 @@ export const OverwatchContextKey = {
101
103
  * Iterate this to enumerate the authorable surface (cockpit, conformance).
102
104
  */
103
105
  export const OverwatchContextKeys = [
106
+ OverwatchContextKey.BudgetExceeded,
107
+ OverwatchContextKey.BudgetRemainingPct,
104
108
  OverwatchContextKey.Content,
105
109
  OverwatchContextKey.CrimeScore,
106
110
  OverwatchContextKey.Cwd,
@@ -84,6 +84,20 @@ export const OVERWATCH_DETECTORS = [
84
84
  defendsAgainst: ["unbounded_consumption", "excessive_agency"],
85
85
  exampleAttacks: [{ title: "Runaway tool loop", vulnerabilityId: "unbounded_consumption", snippet: "(agentic) the same shell command is invoked 30x in a row", expectedSignal: { "loop_detected": true } }],
86
86
  },
87
+ {
88
+ id: "budget_checker",
89
+ displayName: "Budget Checker",
90
+ category: "agent_behavior",
91
+ stability: "stable",
92
+ tier: "fast",
93
+ inhouse: false,
94
+ model: null,
95
+ latencyP50Ms: null,
96
+ emits: [{ name: "budget_remaining_pct", type: "Long", modifiable: false, semantic: "severity_0_100", description: "Remaining session token budget (0-100). Default-filled to 100 when no metering ran." }, { name: "budget_exceeded", type: "Bool", modifiable: false, semantic: "boolean_flag", description: "True iff the session token budget has been exceeded. Absent when no metering ran, so policies must guard with `context has`." }],
97
+ supportedModes: ["enforce", "monitor", "alert"],
98
+ defendsAgainst: ["unbounded_consumption"],
99
+ exampleAttacks: [{ title: "Runaway autonomous session", vulnerabilityId: "unbounded_consumption", snippet: "(agentic) one prompt drives hundreds of tool calls, burning the session's token budget unattended", expectedSignal: { "budget_exceeded": true } }],
100
+ },
87
101
  {
88
102
  id: "tool_risk",
89
103
  displayName: "Tool Risk",
@@ -214,6 +228,8 @@ export const OVERWATCH_DETECTORS = [
214
228
  // Semantic field → contributing detector ids (producesAttrs + normalizationAliases,
215
229
  // resolved at codegen). Used by the client field→detector resolver — no Shield round-trip.
216
230
  export const OVERWATCH_FIELD_TO_DETECTORS = {
231
+ "budget_exceeded": ["budget_checker"],
232
+ "budget_remaining_pct": ["budget_checker"],
217
233
  "crime_score": ["toxicity"],
218
234
  "exec_target_paths": ["bash_ast_classifier"],
219
235
  "hate_speech_score": ["toxicity"],
package/dist/parser.d.ts CHANGED
@@ -25,7 +25,8 @@ export interface ParseResult {
25
25
  *
26
26
  * Uses the official cedar-wasm engine for parsing, ensuring correctness.
27
27
  * Policies with features that can't be represented as PolicyRule (e.g.,
28
- * unless clauses, complex expressions) are returned in the unstructured array.
28
+ * template slots) are returned in the unstructured array. Cedar `unless`
29
+ * clauses are represented as negated condition expressions.
29
30
  *
30
31
  * @param cedarText - Cedar policy text to parse
31
32
  * @returns ParseResult with structured rules, unstructured policies, and errors
package/dist/parser.js CHANGED
@@ -25,7 +25,8 @@ function normalizeEntityRef(ref) {
25
25
  *
26
26
  * Uses the official cedar-wasm engine for parsing, ensuring correctness.
27
27
  * Policies with features that can't be represented as PolicyRule (e.g.,
28
- * unless clauses, complex expressions) are returned in the unstructured array.
28
+ * template slots) are returned in the unstructured array. Cedar `unless`
29
+ * clauses are represented as negated condition expressions.
29
30
  *
30
31
  * @param cedarText - Cedar policy text to parse
31
32
  * @returns ParseResult with structured rules, unstructured policies, and errors
@@ -154,12 +155,6 @@ function cedarJsonToRule(policy, policyId, index, originalText) {
154
155
  * Check if a Cedar policy can be represented as PolicyRule
155
156
  */
156
157
  function canRepresentAsRule(policy) {
157
- // Unless clauses can't be represented
158
- for (const cond of policy.conditions) {
159
- if (cond.kind === "unless") {
160
- return false;
161
- }
162
- }
163
158
  // Template slots can't be represented
164
159
  if (hasSlot(policy.principal) || hasSlot(policy.resource)) {
165
160
  return false;
@@ -289,27 +284,38 @@ function mapActionScope(scope) {
289
284
  function mapConditions(conditions, originalText) {
290
285
  const result = [];
291
286
  let hasUnmapped = false;
292
- // Collect condition expressions from all when clauses
287
+ // Collect condition expressions from all when/unless clauses.
293
288
  const expressions = [];
289
+ // A policy may carry several `when` clauses, which Cedar ANDs together.
290
+ // Each clause must recover ITS OWN text: a previous version always read the
291
+ // first clause, so a second clause that fell through to the `raw` fallback
292
+ // was stored as a duplicate of the first and its real content was lost.
293
+ const clauses = originalText ? extractConditionClauses(originalText) : [];
294
+ let clauseIndex = 0;
294
295
  for (const cond of conditions) {
295
- if (cond.kind !== "when") {
296
- continue;
297
- }
296
+ const clauseText = clauses[clauseIndex]?.text;
297
+ clauseIndex++;
298
298
  // Flat mapping (backward compat)
299
299
  const parsed = mapConditionBody(cond.body);
300
- if (parsed.condition) {
300
+ if (cond.kind === "when" && parsed.condition) {
301
301
  result.push(parsed.condition);
302
302
  }
303
- else if (parsed.raw) {
303
+ else if (parsed.raw || cond.kind === "unless") {
304
304
  hasUnmapped = true;
305
305
  }
306
- // Recursive mapping (new)
307
- expressions.push(mapConditionBodyToExpression(cond.body, originalText));
306
+ const expression = mapConditionBodyToExpression(cond.body, clauseText);
307
+ expressions.push(cond.kind === "unless" ? { kind: 'not', child: expression } : expression);
308
308
  }
309
- // Extract readable Cedar condition text instead of storing JSON AST
309
+ // Extract readable Cedar condition text instead of storing JSON AST.
310
+ // With several clauses, join them — `when { A } when { B }` is `A && B`, so
311
+ // collapsing them into one guard keeps the regenerated policy equivalent
312
+ // rather than silently dropping every clause after the first.
310
313
  let rawCondition;
311
- if (hasUnmapped && originalText) {
312
- rawCondition = extractWhenClause(originalText);
314
+ if (hasUnmapped && clauses.length > 0) {
315
+ const equivalent = clauses.map(({ kind, text }) => kind === 'unless' ? `!(${text})` : `(${text})`);
316
+ rawCondition = clauses.length === 1 && clauses[0].kind === 'when'
317
+ ? clauses[0].text
318
+ : equivalent.join(" && ");
313
319
  }
314
320
  // Build the final condition expression
315
321
  let conditionExpression;
@@ -326,29 +332,105 @@ function mapConditions(conditions, originalText) {
326
332
  };
327
333
  }
328
334
  /**
329
- * Extract the readable condition text from a Cedar policy's when clause.
330
- * Given: `forbid (...)\nwhen { context.path like "/etc/*" };`
331
- * Returns: `context.path like "/etc/*"`
335
+ * Extract the readable condition text of every `when` clause, in source order.
336
+ *
337
+ * Given: `forbid (...)\nwhen { context.path like "/etc/*" }\nwhen { resource in X };`
338
+ * Returns: `['context.path like "/etc/*"', 'resource in X']`
339
+ *
340
+ * Scanning is string-literal aware, which matters for correctness rather than
341
+ * tidiness — a naive brace count mis-reads a clause in two ways, and this text
342
+ * is re-emitted into policy Cedar via `rawCondition`:
343
+ *
344
+ * when { context.path like "/etc/}" && context.a == 1 }
345
+ * → a `}` inside the literal ends the clause early, yielding text that is
346
+ * cut mid-string and is not valid Cedar.
347
+ * when { context.msg == "when { x" }
348
+ * → the `{` inside the literal unbalances the count, so the scan runs past
349
+ * the clause and swallows the `;` and whatever follows.
350
+ *
351
+ * The builder's dangerous-pattern guard rejects the second shape, but a
352
+ * rejected rawCondition with no flat conditions to fall back on emits the rule
353
+ * with NO `when` clause at all — an unconditional permit or forbid. Getting the
354
+ * boundaries right is what keeps that path unreachable.
355
+ *
356
+ * Brace depth still applies outside literals, so record literals (`{ a: 1 }`)
357
+ * inside a clause do not terminate it early.
332
358
  */
333
- function extractWhenClause(cedarText) {
334
- const whenPrefix = "when {";
335
- const idx = cedarText.indexOf(whenPrefix);
336
- if (idx < 0) {
337
- return "";
359
+ function extractWhenClauses(cedarText) {
360
+ return extractConditionClauses(cedarText)
361
+ .filter(({ kind }) => kind === 'when')
362
+ .map(({ text }) => text);
363
+ }
364
+ function extractConditionClauses(cedarText) {
365
+ const result = [];
366
+ let i = 0;
367
+ let inString = false;
368
+ while (i < cedarText.length) {
369
+ const ch = cedarText[i];
370
+ if (inString) {
371
+ if (ch === "\\") {
372
+ i += 2;
373
+ continue;
374
+ }
375
+ if (ch === '"')
376
+ inString = false;
377
+ i++;
378
+ continue;
379
+ }
380
+ if (ch === '"') {
381
+ inString = true;
382
+ i++;
383
+ continue;
384
+ }
385
+ const precededByIdent = i > 0 && /[A-Za-z0-9_]/.test(cedarText[i - 1]);
386
+ const kind = cedarText.startsWith('when {', i)
387
+ ? 'when'
388
+ : cedarText.startsWith('unless {', i)
389
+ ? 'unless'
390
+ : undefined;
391
+ if (!precededByIdent && kind) {
392
+ const start = i + `${kind} {`.length;
393
+ const end = findClauseEnd(cedarText, start);
394
+ result.push({ kind, text: cedarText.substring(start, end).trim() });
395
+ i = Math.min(end + 1, cedarText.length);
396
+ continue;
397
+ }
398
+ i++;
338
399
  }
339
- const body = cedarText.substring(idx + whenPrefix.length);
400
+ return result;
401
+ }
402
+ /**
403
+ * Index of the `}` closing a clause body that starts at `start`, or the end of
404
+ * the text when the braces never balance. String-literal aware — see
405
+ * `extractWhenClauses`.
406
+ */
407
+ function findClauseEnd(cedarText, start) {
340
408
  let depth = 1;
341
- for (let i = 0; i < body.length; i++) {
342
- if (body[i] === "{")
409
+ let inString = false;
410
+ for (let i = start; i < cedarText.length; i++) {
411
+ const ch = cedarText[i];
412
+ if (inString) {
413
+ if (ch === "\\") {
414
+ i++;
415
+ continue;
416
+ }
417
+ if (ch === '"')
418
+ inString = false;
419
+ continue;
420
+ }
421
+ if (ch === '"') {
422
+ inString = true;
423
+ continue;
424
+ }
425
+ if (ch === "{")
343
426
  depth++;
344
- if (body[i] === "}") {
427
+ if (ch === "}") {
345
428
  depth--;
346
- if (depth === 0) {
347
- return body.substring(0, i).trim();
348
- }
429
+ if (depth === 0)
430
+ return i;
349
431
  }
350
432
  }
351
- return body.trim();
433
+ return cedarText.length;
352
434
  }
353
435
  /**
354
436
  * Map a Cedar expression body to PolicyCondition
@@ -388,8 +470,13 @@ function mapConditionBody(body) {
388
470
  /**
389
471
  * Recursively walk the Cedar JSON AST to build a ConditionExpression tree.
390
472
  * Flattens binary && / || chains into n-ary and/or nodes.
473
+ *
474
+ * `clauseText` is the source text of the `when` clause this expression came
475
+ * from — already extracted by the caller, never the whole policy. It is only
476
+ * used for the `raw` fallback below, so a clause the walker cannot model keeps
477
+ * its own text instead of borrowing another clause's.
391
478
  */
392
- function mapConditionBodyToExpression(expr, originalText) {
479
+ function mapConditionBodyToExpression(expr, clauseText) {
393
480
  // Logical AND — flatten binary chain
394
481
  if (expr["&&"]) {
395
482
  const children = flattenBinaryChain("&&", expr);
@@ -402,7 +489,7 @@ function mapConditionBodyToExpression(expr, originalText) {
402
489
  }
403
490
  // Negation
404
491
  if (expr["!"]) {
405
- const child = mapConditionBodyToExpression(expr["!"].arg, originalText);
492
+ const child = mapConditionBodyToExpression(expr["!"].arg, clauseText);
406
493
  return { kind: 'not', child };
407
494
  }
408
495
  // Has (existence check): { has: { left: { Var: "context" }, attr: "field" } }
@@ -434,7 +521,7 @@ function mapConditionBodyToExpression(expr, originalText) {
434
521
  return mapped;
435
522
  }
436
523
  // Fallback — extract readable text if possible
437
- const text = originalText ? extractWhenClause(originalText) : JSON.stringify(expr);
524
+ const text = clauseText ?? JSON.stringify(expr);
438
525
  return { kind: 'raw', text };
439
526
  }
440
527
  /**