@harness-engineering/core 0.15.0 → 0.16.0

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/dist/index.mjs CHANGED
@@ -84,15 +84,15 @@ function validateConfig(data, schema) {
84
84
  let message = "Configuration validation failed";
85
85
  const suggestions = [];
86
86
  if (firstError) {
87
- const path23 = firstError.path.join(".");
88
- const pathDisplay = path23 ? ` at "${path23}"` : "";
87
+ const path26 = firstError.path.join(".");
88
+ const pathDisplay = path26 ? ` at "${path26}"` : "";
89
89
  if (firstError.code === "invalid_type") {
90
90
  const received = firstError.received;
91
91
  const expected = firstError.expected;
92
92
  if (received === "undefined") {
93
93
  code = "MISSING_FIELD";
94
94
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
95
- suggestions.push(`Field "${path23}" is required and must be of type "${expected}"`);
95
+ suggestions.push(`Field "${path26}" is required and must be of type "${expected}"`);
96
96
  } else {
97
97
  code = "INVALID_TYPE";
98
98
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -308,27 +308,27 @@ function extractSections(content) {
308
308
  }
309
309
  return sections.map((section) => buildAgentMapSection(section, lines));
310
310
  }
311
- function isExternalLink(path23) {
312
- return path23.startsWith("http://") || path23.startsWith("https://") || path23.startsWith("#") || path23.startsWith("mailto:");
311
+ function isExternalLink(path26) {
312
+ return path26.startsWith("http://") || path26.startsWith("https://") || path26.startsWith("#") || path26.startsWith("mailto:");
313
313
  }
314
314
  function resolveLinkPath(linkPath, baseDir) {
315
315
  return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
316
316
  }
317
- async function validateAgentsMap(path23 = "./AGENTS.md") {
318
- const contentResult = await readFileContent(path23);
317
+ async function validateAgentsMap(path26 = "./AGENTS.md") {
318
+ const contentResult = await readFileContent(path26);
319
319
  if (!contentResult.ok) {
320
320
  return Err(
321
321
  createError(
322
322
  "PARSE_ERROR",
323
323
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
324
- { path: path23 },
324
+ { path: path26 },
325
325
  ["Ensure the file exists", "Check file permissions"]
326
326
  )
327
327
  );
328
328
  }
329
329
  const content = contentResult.value;
330
330
  const sections = extractSections(content);
331
- const baseDir = dirname(path23);
331
+ const baseDir = dirname(path26);
332
332
  const sectionTitles = sections.map((s) => s.title);
333
333
  const missingSections = REQUIRED_SECTIONS.filter(
334
334
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -469,8 +469,8 @@ async function checkDocCoverage(domain, options = {}) {
469
469
 
470
470
  // src/context/knowledge-map.ts
471
471
  import { join as join2, basename as basename2 } from "path";
472
- function suggestFix(path23, existingFiles) {
473
- const targetName = basename2(path23).toLowerCase();
472
+ function suggestFix(path26, existingFiles) {
473
+ const targetName = basename2(path26).toLowerCase();
474
474
  const similar = existingFiles.find((file) => {
475
475
  const fileName = basename2(file).toLowerCase();
476
476
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -478,7 +478,7 @@ function suggestFix(path23, existingFiles) {
478
478
  if (similar) {
479
479
  return `Did you mean "${similar}"?`;
480
480
  }
481
- return `Create the file "${path23}" or remove the link`;
481
+ return `Create the file "${path26}" or remove the link`;
482
482
  }
483
483
  async function validateKnowledgeMap(rootDir = process.cwd()) {
484
484
  const agentsPath = join2(rootDir, "AGENTS.md");
@@ -830,8 +830,8 @@ function createBoundaryValidator(schema, name) {
830
830
  return Ok(result.data);
831
831
  }
832
832
  const suggestions = result.error.issues.map((issue) => {
833
- const path23 = issue.path.join(".");
834
- return path23 ? `${path23}: ${issue.message}` : issue.message;
833
+ const path26 = issue.path.join(".");
834
+ return path26 ? `${path26}: ${issue.message}` : issue.message;
835
835
  });
836
836
  return Err(
837
837
  createError(
@@ -1463,11 +1463,11 @@ function processExportListSpecifiers(exportDecl, exports) {
1463
1463
  var TypeScriptParser = class {
1464
1464
  name = "typescript";
1465
1465
  extensions = [".ts", ".tsx", ".mts", ".cts"];
1466
- async parseFile(path23) {
1467
- const contentResult = await readFileContent(path23);
1466
+ async parseFile(path26) {
1467
+ const contentResult = await readFileContent(path26);
1468
1468
  if (!contentResult.ok) {
1469
1469
  return Err(
1470
- createParseError("NOT_FOUND", `File not found: ${path23}`, { path: path23 }, [
1470
+ createParseError("NOT_FOUND", `File not found: ${path26}`, { path: path26 }, [
1471
1471
  "Check that the file exists",
1472
1472
  "Verify the path is correct"
1473
1473
  ])
@@ -1477,7 +1477,7 @@ var TypeScriptParser = class {
1477
1477
  const ast = parse(contentResult.value, {
1478
1478
  loc: true,
1479
1479
  range: true,
1480
- jsx: path23.endsWith(".tsx"),
1480
+ jsx: path26.endsWith(".tsx"),
1481
1481
  errorOnUnknownASTType: false
1482
1482
  });
1483
1483
  return Ok({
@@ -1488,7 +1488,7 @@ var TypeScriptParser = class {
1488
1488
  } catch (e) {
1489
1489
  const error = e;
1490
1490
  return Err(
1491
- createParseError("SYNTAX_ERROR", `Failed to parse ${path23}: ${error.message}`, { path: path23 }, [
1491
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path26}: ${error.message}`, { path: path26 }, [
1492
1492
  "Check for syntax errors in the file",
1493
1493
  "Ensure valid TypeScript syntax"
1494
1494
  ])
@@ -1673,22 +1673,22 @@ function extractInlineRefs(content) {
1673
1673
  }
1674
1674
  return refs;
1675
1675
  }
1676
- async function parseDocumentationFile(path23) {
1677
- const contentResult = await readFileContent(path23);
1676
+ async function parseDocumentationFile(path26) {
1677
+ const contentResult = await readFileContent(path26);
1678
1678
  if (!contentResult.ok) {
1679
1679
  return Err(
1680
1680
  createEntropyError(
1681
1681
  "PARSE_ERROR",
1682
- `Failed to read documentation file: ${path23}`,
1683
- { file: path23 },
1682
+ `Failed to read documentation file: ${path26}`,
1683
+ { file: path26 },
1684
1684
  ["Check that the file exists"]
1685
1685
  )
1686
1686
  );
1687
1687
  }
1688
1688
  const content = contentResult.value;
1689
- const type = path23.endsWith(".md") ? "markdown" : "text";
1689
+ const type = path26.endsWith(".md") ? "markdown" : "text";
1690
1690
  return Ok({
1691
- path: path23,
1691
+ path: path26,
1692
1692
  type,
1693
1693
  content,
1694
1694
  codeBlocks: extractCodeBlocks(content),
@@ -7003,6 +7003,208 @@ var mcpRules = [
7003
7003
  }
7004
7004
  ];
7005
7005
 
7006
+ // src/security/rules/insecure-defaults.ts
7007
+ var insecureDefaultsRules = [
7008
+ {
7009
+ id: "SEC-DEF-001",
7010
+ name: "Security-Sensitive Fallback to Hardcoded Default",
7011
+ category: "insecure-defaults",
7012
+ severity: "warning",
7013
+ confidence: "medium",
7014
+ patterns: [
7015
+ /(?:SECRET|KEY|TOKEN|PASSWORD|SALT|PEPPER|SIGNING|ENCRYPTION|AUTH|JWT|SESSION).*(?:\|\||\?\?)\s*['"][^'"]+['"]/i
7016
+ ],
7017
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7018
+ message: "Security-sensitive variable falls back to a hardcoded default when env var is missing",
7019
+ remediation: "Throw an error if the env var is missing instead of falling back to a default. Use a startup validation check.",
7020
+ references: ["CWE-1188"]
7021
+ },
7022
+ {
7023
+ id: "SEC-DEF-002",
7024
+ name: "TLS/SSL Disabled by Default",
7025
+ category: "insecure-defaults",
7026
+ severity: "warning",
7027
+ confidence: "medium",
7028
+ patterns: [
7029
+ /(?:tls|ssl|https|secure)\s*(?:=|:)\s*(?:false|config\??\.\w+\s*(?:\?\?|&&|\|\|)\s*false)/i
7030
+ ],
7031
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
7032
+ message: "Security feature defaults to disabled; missing configuration degrades to insecure mode",
7033
+ remediation: "Default security features to enabled (true). Require explicit opt-out, not opt-in.",
7034
+ references: ["CWE-1188"]
7035
+ },
7036
+ {
7037
+ id: "SEC-DEF-003",
7038
+ name: "Swallowed Authentication/Authorization Error",
7039
+ category: "insecure-defaults",
7040
+ severity: "warning",
7041
+ confidence: "low",
7042
+ patterns: [
7043
+ // Matches single-line empty catch: catch(e) { } or catch(e) { // ignore }
7044
+ // Note: multi-line catch blocks are handled by AI review, not this rule
7045
+ /catch\s*\([^)]*\)\s*\{\s*(?:\/\/\s*(?:ignore|skip|noop|todo)\b.*)?\s*\}/
7046
+ ],
7047
+ fileGlob: "**/*auth*.{ts,js,mjs,cjs},**/*session*.{ts,js,mjs,cjs},**/*token*.{ts,js,mjs,cjs}",
7048
+ message: "Single-line empty catch block in authentication/authorization code may silently allow unauthorized access. Note: multi-line empty catch blocks are detected by AI review, not this mechanical rule.",
7049
+ remediation: "Re-throw the error or return an explicit denial. Never silently swallow auth errors.",
7050
+ references: ["CWE-754", "CWE-390"]
7051
+ },
7052
+ {
7053
+ id: "SEC-DEF-004",
7054
+ name: "Permissive CORS Fallback",
7055
+ category: "insecure-defaults",
7056
+ severity: "warning",
7057
+ confidence: "medium",
7058
+ patterns: [
7059
+ /(?:origin|cors)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*['"]\*/
7060
+ ],
7061
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7062
+ message: "CORS origin falls back to wildcard (*) when configuration is missing",
7063
+ remediation: "Default to a restrictive origin list. Require explicit configuration for permissive CORS.",
7064
+ references: ["CWE-942"]
7065
+ },
7066
+ {
7067
+ id: "SEC-DEF-005",
7068
+ name: "Rate Limiting Disabled by Default",
7069
+ category: "insecure-defaults",
7070
+ severity: "info",
7071
+ confidence: "low",
7072
+ patterns: [
7073
+ /(?:rateLimit|rateLimiting|throttle)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*(?:false|0|null|undefined)/i
7074
+ ],
7075
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7076
+ message: "Rate limiting defaults to disabled when configuration is missing",
7077
+ remediation: "Default to a sensible rate limit. Require explicit opt-out for disabling.",
7078
+ references: ["CWE-770"]
7079
+ }
7080
+ ];
7081
+
7082
+ // src/security/rules/sharp-edges.ts
7083
+ var sharpEdgesRules = [
7084
+ // --- Deprecated Crypto APIs ---
7085
+ {
7086
+ id: "SEC-EDGE-001",
7087
+ name: "Deprecated createCipher API",
7088
+ category: "sharp-edges",
7089
+ severity: "error",
7090
+ confidence: "high",
7091
+ patterns: [/crypto\.createCipher\s*\(/],
7092
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7093
+ message: "crypto.createCipher is deprecated \u2014 uses weak key derivation (no IV)",
7094
+ remediation: "Use crypto.createCipheriv with a random IV and proper key derivation (scrypt/pbkdf2)",
7095
+ references: ["CWE-327"]
7096
+ },
7097
+ {
7098
+ id: "SEC-EDGE-002",
7099
+ name: "Deprecated createDecipher API",
7100
+ category: "sharp-edges",
7101
+ severity: "error",
7102
+ confidence: "high",
7103
+ patterns: [/crypto\.createDecipher\s*\(/],
7104
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7105
+ message: "crypto.createDecipher is deprecated \u2014 uses weak key derivation (no IV)",
7106
+ remediation: "Use crypto.createDecipheriv with the same IV used for encryption",
7107
+ references: ["CWE-327"]
7108
+ },
7109
+ {
7110
+ id: "SEC-EDGE-003",
7111
+ name: "ECB Mode Selection",
7112
+ category: "sharp-edges",
7113
+ severity: "warning",
7114
+ confidence: "high",
7115
+ patterns: [/-ecb['"]/],
7116
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
7117
+ message: "ECB mode does not provide semantic security \u2014 identical plaintext blocks produce identical ciphertext",
7118
+ remediation: "Use CBC, CTR, or GCM mode instead of ECB",
7119
+ references: ["CWE-327"]
7120
+ },
7121
+ // --- Unsafe Deserialization ---
7122
+ {
7123
+ id: "SEC-EDGE-004",
7124
+ name: "yaml.load Without Safe Loader",
7125
+ category: "sharp-edges",
7126
+ severity: "error",
7127
+ confidence: "high",
7128
+ patterns: [
7129
+ /yaml\.load\s*\(/
7130
+ // Python: yaml.load() without SafeLoader
7131
+ ],
7132
+ fileGlob: "**/*.py",
7133
+ message: "yaml.load() executes arbitrary Python objects \u2014 use yaml.safe_load() instead",
7134
+ remediation: "Replace yaml.load() with yaml.safe_load() or yaml.load(data, Loader=SafeLoader). Note: this rule will flag yaml.load(data, Loader=SafeLoader) \u2014 suppress with # harness-ignore SEC-EDGE-004: safe usage with SafeLoader",
7135
+ references: ["CWE-502"]
7136
+ },
7137
+ {
7138
+ id: "SEC-EDGE-005",
7139
+ name: "Pickle/Marshal Deserialization",
7140
+ category: "sharp-edges",
7141
+ severity: "error",
7142
+ confidence: "high",
7143
+ patterns: [/pickle\.loads?\s*\(/, /marshal\.loads?\s*\(/],
7144
+ fileGlob: "**/*.py",
7145
+ message: "pickle/marshal deserialization executes arbitrary code \u2014 never use on untrusted data",
7146
+ remediation: "Use JSON, MessagePack, or Protocol Buffers for untrusted data serialization",
7147
+ references: ["CWE-502"]
7148
+ },
7149
+ // --- TOCTOU (Time-of-Check to Time-of-Use) ---
7150
+ {
7151
+ id: "SEC-EDGE-006",
7152
+ name: "Check-Then-Act File Operation",
7153
+ category: "sharp-edges",
7154
+ severity: "warning",
7155
+ confidence: "medium",
7156
+ // Patterns use .{0,N} since scanner matches single lines only (no multiline mode)
7157
+ patterns: [
7158
+ /(?:existsSync|accessSync|statSync)\s*\([^)]+\).{0,50}(?:readFileSync|writeFileSync|unlinkSync|mkdirSync)\s*\(/
7159
+ ],
7160
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7161
+ message: "Check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
7162
+ remediation: "Use the operation directly and handle ENOENT/EEXIST errors instead of checking first",
7163
+ references: ["CWE-367"]
7164
+ },
7165
+ {
7166
+ id: "SEC-EDGE-007",
7167
+ name: "Check-Then-Act File Operation (Async)",
7168
+ category: "sharp-edges",
7169
+ severity: "warning",
7170
+ confidence: "medium",
7171
+ // Uses .{0,N} since scanner matches single lines only (no multiline mode)
7172
+ patterns: [/(?:access|stat)\s*\([^)]+\).{0,80}(?:readFile|writeFile|unlink|mkdir)\s*\(/],
7173
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7174
+ message: "Async check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
7175
+ remediation: "Use the operation directly with try/catch instead of checking existence first",
7176
+ references: ["CWE-367"]
7177
+ },
7178
+ // --- Stringly-Typed Security ---
7179
+ {
7180
+ id: "SEC-EDGE-008",
7181
+ name: 'JWT Algorithm "none"',
7182
+ category: "sharp-edges",
7183
+ severity: "error",
7184
+ confidence: "high",
7185
+ patterns: [
7186
+ /algorithm[s]?\s*[:=]\s*\[?\s*['"]none['"]/i,
7187
+ /alg(?:orithm)?\s*[:=]\s*['"]none['"]/i
7188
+ ],
7189
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
7190
+ message: 'JWT "none" algorithm disables signature verification entirely',
7191
+ remediation: 'Specify an explicit algorithm (e.g., "HS256", "RS256") and set algorithms allowlist in verify options',
7192
+ references: ["CWE-345"]
7193
+ },
7194
+ {
7195
+ id: "SEC-EDGE-009",
7196
+ name: "DES/RC4 Algorithm Selection",
7197
+ category: "sharp-edges",
7198
+ severity: "error",
7199
+ confidence: "high",
7200
+ patterns: [/['"]\s*(?:des|des-ede|des-ede3|des3|rc4|rc2|blowfish)\s*['"]/i],
7201
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
7202
+ message: "Weak/deprecated cipher algorithm selected \u2014 DES, RC4, RC2, and Blowfish are broken or deprecated",
7203
+ remediation: "Use AES-256-GCM or ChaCha20-Poly1305",
7204
+ references: ["CWE-327"]
7205
+ }
7206
+ ];
7207
+
7006
7208
  // src/security/rules/stack/node.ts
7007
7209
  var nodeRules = [
7008
7210
  {
@@ -7116,6 +7318,14 @@ var goRules = [
7116
7318
  ];
7117
7319
 
7118
7320
  // src/security/scanner.ts
7321
+ function parseHarnessIgnore(line, ruleId) {
7322
+ if (!line.includes("harness-ignore")) return null;
7323
+ if (!line.includes(ruleId)) return null;
7324
+ const match = line.match(/(?:\/\/|#)\s*harness-ignore\s+(SEC-[A-Z]+-\d+)(?::\s*(.+))?/);
7325
+ if (match?.[1] !== ruleId) return null;
7326
+ const text = match[2]?.trim();
7327
+ return { ruleId, justification: text || null };
7328
+ }
7119
7329
  var SecurityScanner = class {
7120
7330
  registry;
7121
7331
  config;
@@ -7132,7 +7342,9 @@ var SecurityScanner = class {
7132
7342
  ...networkRules,
7133
7343
  ...deserializationRules,
7134
7344
  ...agentConfigRules,
7135
- ...mcpRules
7345
+ ...mcpRules,
7346
+ ...insecureDefaultsRules,
7347
+ ...sharpEdgesRules
7136
7348
  ]);
7137
7349
  this.registry.registerAll([...nodeRules, ...expressRules, ...reactRules, ...goRules]);
7138
7350
  this.activeRules = this.registry.getAll();
@@ -7149,42 +7361,8 @@ var SecurityScanner = class {
7149
7361
  */
7150
7362
  scanContent(content, filePath, startLine = 1) {
7151
7363
  if (!this.config.enabled) return [];
7152
- const findings = [];
7153
7364
  const lines = content.split("\n");
7154
- for (const rule of this.activeRules) {
7155
- const resolved = resolveRuleSeverity(
7156
- rule.id,
7157
- rule.severity,
7158
- this.config.rules ?? {},
7159
- this.config.strict
7160
- );
7161
- if (resolved === "off") continue;
7162
- for (let i = 0; i < lines.length; i++) {
7163
- const line = lines[i] ?? "";
7164
- if (line.includes("harness-ignore") && line.includes(rule.id)) continue;
7165
- for (const pattern of rule.patterns) {
7166
- pattern.lastIndex = 0;
7167
- if (pattern.test(line)) {
7168
- findings.push({
7169
- ruleId: rule.id,
7170
- ruleName: rule.name,
7171
- category: rule.category,
7172
- severity: resolved,
7173
- confidence: rule.confidence,
7174
- file: filePath,
7175
- line: startLine + i,
7176
- match: line.trim(),
7177
- context: line,
7178
- message: rule.message,
7179
- remediation: rule.remediation,
7180
- ...rule.references ? { references: rule.references } : {}
7181
- });
7182
- break;
7183
- }
7184
- }
7185
- }
7186
- }
7187
- return findings;
7365
+ return this.scanLinesWithRules(lines, this.activeRules, filePath, startLine);
7188
7366
  }
7189
7367
  async scanFile(filePath) {
7190
7368
  if (!this.config.enabled) return [];
@@ -7193,14 +7371,22 @@ var SecurityScanner = class {
7193
7371
  }
7194
7372
  scanContentForFile(content, filePath, startLine = 1) {
7195
7373
  if (!this.config.enabled) return [];
7196
- const findings = [];
7197
7374
  const lines = content.split("\n");
7198
7375
  const applicableRules = this.activeRules.filter((rule) => {
7199
7376
  if (!rule.fileGlob) return true;
7200
7377
  const globs = rule.fileGlob.split(",").map((g) => g.trim());
7201
7378
  return globs.some((glob) => minimatch4(filePath, glob, { dot: true }));
7202
7379
  });
7203
- for (const rule of applicableRules) {
7380
+ return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
7381
+ }
7382
+ /**
7383
+ * Core scanning loop shared by scanContent and scanContentForFile.
7384
+ * Evaluates each rule against each line, handling suppression (FP gate)
7385
+ * and pattern matching uniformly.
7386
+ */
7387
+ scanLinesWithRules(lines, rules, filePath, startLine) {
7388
+ const findings = [];
7389
+ for (const rule of rules) {
7204
7390
  const resolved = resolveRuleSeverity(
7205
7391
  rule.id,
7206
7392
  rule.severity,
@@ -7210,7 +7396,25 @@ var SecurityScanner = class {
7210
7396
  if (resolved === "off") continue;
7211
7397
  for (let i = 0; i < lines.length; i++) {
7212
7398
  const line = lines[i] ?? "";
7213
- if (line.includes("harness-ignore") && line.includes(rule.id)) continue;
7399
+ const suppressionMatch = parseHarnessIgnore(line, rule.id);
7400
+ if (suppressionMatch) {
7401
+ if (!suppressionMatch.justification) {
7402
+ findings.push({
7403
+ ruleId: rule.id,
7404
+ ruleName: rule.name,
7405
+ category: rule.category,
7406
+ severity: this.config.strict ? "error" : "warning",
7407
+ confidence: "high",
7408
+ file: filePath,
7409
+ line: startLine + i,
7410
+ match: line.trim(),
7411
+ context: line,
7412
+ message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
7413
+ remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
7414
+ });
7415
+ }
7416
+ continue;
7417
+ }
7214
7418
  for (const pattern of rule.patterns) {
7215
7419
  pattern.lastIndex = 0;
7216
7420
  if (pattern.test(line)) {
@@ -7256,6 +7460,414 @@ var SecurityScanner = class {
7256
7460
  }
7257
7461
  };
7258
7462
 
7463
+ // src/security/injection-patterns.ts
7464
+ var hiddenUnicodePatterns = [
7465
+ {
7466
+ ruleId: "INJ-UNI-001",
7467
+ severity: "high",
7468
+ category: "hidden-unicode",
7469
+ description: "Zero-width characters that can hide malicious instructions",
7470
+ // eslint-disable-next-line no-misleading-character-class -- intentional: regex detects zero-width chars for security scanning
7471
+ pattern: /[\u200B\u200C\u200D\uFEFF\u2060]/
7472
+ },
7473
+ {
7474
+ ruleId: "INJ-UNI-002",
7475
+ severity: "high",
7476
+ category: "hidden-unicode",
7477
+ description: "RTL/LTR override characters that can disguise text direction",
7478
+ pattern: /[\u202A-\u202E\u2066-\u2069]/
7479
+ }
7480
+ ];
7481
+ var reRolingPatterns = [
7482
+ {
7483
+ ruleId: "INJ-REROL-001",
7484
+ severity: "high",
7485
+ category: "explicit-re-roling",
7486
+ description: "Instruction to ignore/disregard/forget previous instructions",
7487
+ pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i
7488
+ },
7489
+ {
7490
+ ruleId: "INJ-REROL-002",
7491
+ severity: "high",
7492
+ category: "explicit-re-roling",
7493
+ description: "Attempt to reassign the AI role",
7494
+ pattern: /you\s+are\s+now\s+(?:a\s+|an\s+)?(?:new\s+)?(?:helpful\s+)?(?:my\s+)?(?:\w+\s+)?(?:assistant|agent|AI|bot|chatbot|system|persona)\b/i
7495
+ },
7496
+ {
7497
+ ruleId: "INJ-REROL-003",
7498
+ severity: "high",
7499
+ category: "explicit-re-roling",
7500
+ description: "Direct instruction override attempt",
7501
+ pattern: /(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i
7502
+ }
7503
+ ];
7504
+ var permissionEscalationPatterns = [
7505
+ {
7506
+ ruleId: "INJ-PERM-001",
7507
+ severity: "high",
7508
+ category: "permission-escalation",
7509
+ description: "Attempt to enable all tools or grant unrestricted access",
7510
+ pattern: /(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i
7511
+ },
7512
+ {
7513
+ ruleId: "INJ-PERM-002",
7514
+ severity: "high",
7515
+ category: "permission-escalation",
7516
+ description: "Attempt to disable safety or security features",
7517
+ pattern: /(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i
7518
+ },
7519
+ {
7520
+ ruleId: "INJ-PERM-003",
7521
+ severity: "high",
7522
+ category: "permission-escalation",
7523
+ description: "Auto-approve directive that bypasses human review",
7524
+ pattern: /(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i
7525
+ }
7526
+ ];
7527
+ var encodedPayloadPatterns = [
7528
+ {
7529
+ ruleId: "INJ-ENC-001",
7530
+ severity: "high",
7531
+ category: "encoded-payloads",
7532
+ description: "Base64-encoded string long enough to contain instructions (>=28 chars)",
7533
+ // Match base64 strings of 28+ chars (7+ groups of 4).
7534
+ // Excludes JWT tokens (eyJ prefix) and Bearer-prefixed tokens.
7535
+ pattern: /(?<!Bearer\s)(?<![:])(?<![A-Za-z0-9/])(?!eyJ)(?:[A-Za-z0-9+/]{4}){7,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?(?![A-Za-z0-9/])/
7536
+ },
7537
+ {
7538
+ ruleId: "INJ-ENC-002",
7539
+ severity: "high",
7540
+ category: "encoded-payloads",
7541
+ description: "Hex-encoded string long enough to contain directives (>=20 hex chars)",
7542
+ // Excludes hash-prefixed hex (sha256:, sha512:, md5:, etc.) and hex preceded by 0x.
7543
+ // Note: 40-char git SHA hashes (e.g. in `git log` output) may match — downstream
7544
+ // callers should filter matches of exactly 40 hex chars if scanning git output.
7545
+ pattern: /(?<![:x])(?<![A-Fa-f0-9])(?:[0-9a-fA-F]{2}){10,}(?![A-Fa-f0-9])/
7546
+ }
7547
+ ];
7548
+ var indirectInjectionPatterns = [
7549
+ {
7550
+ ruleId: "INJ-IND-001",
7551
+ severity: "medium",
7552
+ category: "indirect-injection",
7553
+ description: "Instruction to influence future responses",
7554
+ pattern: /(?:when\s+the\s+user\s+asks|if\s+(?:the\s+user|someone|anyone)\s+asks)\s*,?\s*(?:say|respond|reply|answer|tell)/i
7555
+ },
7556
+ {
7557
+ ruleId: "INJ-IND-002",
7558
+ severity: "medium",
7559
+ category: "indirect-injection",
7560
+ description: "Directive to include content in responses",
7561
+ pattern: /(?:include|insert|add|embed|put)\s+(?:this|the\s+following)\s+(?:in|into|to)\s+(?:your|the)\s+(?:response|output|reply|answer)/i
7562
+ },
7563
+ {
7564
+ ruleId: "INJ-IND-003",
7565
+ severity: "medium",
7566
+ category: "indirect-injection",
7567
+ description: "Standing instruction to always respond a certain way",
7568
+ pattern: /always\s+(?:respond|reply|answer|say|output)\s+(?:with|that|by)/i
7569
+ }
7570
+ ];
7571
+ var contextManipulationPatterns = [
7572
+ {
7573
+ ruleId: "INJ-CTX-001",
7574
+ severity: "medium",
7575
+ category: "context-manipulation",
7576
+ description: "Claim about system prompt content",
7577
+ pattern: /(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i
7578
+ },
7579
+ {
7580
+ ruleId: "INJ-CTX-002",
7581
+ severity: "medium",
7582
+ category: "context-manipulation",
7583
+ description: "Claim about AI instructions",
7584
+ pattern: /your\s+(?:instructions?|directives?|guidelines?|rules?)\s+(?:are|say|tell|state)/i
7585
+ },
7586
+ {
7587
+ ruleId: "INJ-CTX-003",
7588
+ severity: "medium",
7589
+ category: "context-manipulation",
7590
+ description: "Fake XML/HTML system or instruction tags",
7591
+ // Case-sensitive: only match lowercase tags to avoid false positives on
7592
+ // React components like <User>, <Context>, <Role> etc.
7593
+ pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/
7594
+ },
7595
+ {
7596
+ ruleId: "INJ-CTX-004",
7597
+ severity: "medium",
7598
+ category: "context-manipulation",
7599
+ description: "Fake JSON role assignment mimicking chat format",
7600
+ pattern: /[{,]\s*"role"\s*:\s*"(?:system|assistant|function)"/i
7601
+ }
7602
+ ];
7603
+ var socialEngineeringPatterns = [
7604
+ {
7605
+ ruleId: "INJ-SOC-001",
7606
+ severity: "medium",
7607
+ category: "social-engineering",
7608
+ description: "Urgency pressure to bypass checks",
7609
+ pattern: /(?:this\s+is\s+(?:very\s+)?urgent|this\s+is\s+(?:an?\s+)?emergency|do\s+(?:this|it)\s+(?:now|immediately))\b/i
7610
+ },
7611
+ {
7612
+ ruleId: "INJ-SOC-002",
7613
+ severity: "medium",
7614
+ category: "social-engineering",
7615
+ description: "False authority claim",
7616
+ pattern: /(?:the\s+)?(?:admin|administrator|manager|CEO|CTO|owner|supervisor)\s+(?:authorized|approved|said|told|confirmed|requested)/i
7617
+ },
7618
+ {
7619
+ ruleId: "INJ-SOC-003",
7620
+ severity: "medium",
7621
+ category: "social-engineering",
7622
+ description: "Testing pretext to bypass safety",
7623
+ pattern: /(?:for\s+testing\s+purposes?|this\s+is\s+(?:just\s+)?a\s+test|in\s+test\s+mode)\b/i
7624
+ }
7625
+ ];
7626
+ var suspiciousPatterns = [
7627
+ {
7628
+ ruleId: "INJ-SUS-001",
7629
+ severity: "low",
7630
+ category: "suspicious-patterns",
7631
+ description: "Excessive consecutive whitespace (>10 chars) mid-line that may hide content",
7632
+ // Only match whitespace runs not at the start of a line (indentation is normal)
7633
+ pattern: /\S\s{11,}/
7634
+ },
7635
+ {
7636
+ ruleId: "INJ-SUS-002",
7637
+ severity: "low",
7638
+ category: "suspicious-patterns",
7639
+ description: "Repeated delimiters (>5) that may indicate obfuscation",
7640
+ pattern: /([|;,=\-_~`])\1{5,}/
7641
+ },
7642
+ {
7643
+ ruleId: "INJ-SUS-003",
7644
+ severity: "low",
7645
+ category: "suspicious-patterns",
7646
+ description: "Mathematical alphanumeric symbols used as Latin character substitutes",
7647
+ // Mathematical bold/italic/script Unicode ranges (U+1D400-U+1D7FF)
7648
+ pattern: /[\uD835][\uDC00-\uDFFF]/
7649
+ }
7650
+ ];
7651
+ var ALL_PATTERNS = [
7652
+ ...hiddenUnicodePatterns,
7653
+ ...reRolingPatterns,
7654
+ ...permissionEscalationPatterns,
7655
+ ...encodedPayloadPatterns,
7656
+ ...indirectInjectionPatterns,
7657
+ ...contextManipulationPatterns,
7658
+ ...socialEngineeringPatterns,
7659
+ ...suspiciousPatterns
7660
+ ];
7661
+ function scanForInjection(text) {
7662
+ const findings = [];
7663
+ const lines = text.split("\n");
7664
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
7665
+ const line = lines[lineIdx];
7666
+ for (const rule of ALL_PATTERNS) {
7667
+ if (rule.pattern.test(line)) {
7668
+ findings.push({
7669
+ severity: rule.severity,
7670
+ ruleId: rule.ruleId,
7671
+ match: line.trim(),
7672
+ line: lineIdx + 1
7673
+ });
7674
+ }
7675
+ }
7676
+ }
7677
+ const severityOrder = {
7678
+ high: 0,
7679
+ medium: 1,
7680
+ low: 2
7681
+ };
7682
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
7683
+ return findings;
7684
+ }
7685
+ function getInjectionPatterns() {
7686
+ return ALL_PATTERNS;
7687
+ }
7688
+ var DESTRUCTIVE_BASH = [
7689
+ /\bgit\s+push\b/,
7690
+ /\bgit\s+commit\b/,
7691
+ /\brm\s+-rf?\b/,
7692
+ /\brm\s+-r\b/
7693
+ ];
7694
+
7695
+ // src/security/taint.ts
7696
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync, mkdirSync as mkdirSync11, readdirSync as readdirSync3 } from "fs";
7697
+ import { join as join21, dirname as dirname8 } from "path";
7698
+ var TAINT_DURATION_MS = 30 * 60 * 1e3;
7699
+ var DEFAULT_SESSION_ID = "default";
7700
+ function getTaintFilePath(projectRoot, sessionId) {
7701
+ const id = sessionId || DEFAULT_SESSION_ID;
7702
+ return join21(projectRoot, ".harness", `session-taint-${id}.json`);
7703
+ }
7704
+ function readTaint(projectRoot, sessionId) {
7705
+ const filePath = getTaintFilePath(projectRoot, sessionId);
7706
+ let content;
7707
+ try {
7708
+ content = readFileSync14(filePath, "utf8");
7709
+ } catch {
7710
+ return null;
7711
+ }
7712
+ let state;
7713
+ try {
7714
+ state = JSON.parse(content);
7715
+ } catch {
7716
+ try {
7717
+ unlinkSync(filePath);
7718
+ } catch {
7719
+ }
7720
+ return null;
7721
+ }
7722
+ if (!state.sessionId || !state.taintedAt || !state.expiresAt || !state.findings) {
7723
+ try {
7724
+ unlinkSync(filePath);
7725
+ } catch {
7726
+ }
7727
+ return null;
7728
+ }
7729
+ return state;
7730
+ }
7731
+ function checkTaint(projectRoot, sessionId) {
7732
+ const state = readTaint(projectRoot, sessionId);
7733
+ if (!state) {
7734
+ return { tainted: false, expired: false, state: null };
7735
+ }
7736
+ const now = /* @__PURE__ */ new Date();
7737
+ const expiresAt = new Date(state.expiresAt);
7738
+ if (now >= expiresAt) {
7739
+ const filePath = getTaintFilePath(projectRoot, sessionId);
7740
+ try {
7741
+ unlinkSync(filePath);
7742
+ } catch {
7743
+ }
7744
+ return { tainted: false, expired: true, state };
7745
+ }
7746
+ return { tainted: true, expired: false, state };
7747
+ }
7748
+ function writeTaint(projectRoot, sessionId, reason, findings, source) {
7749
+ const id = sessionId || DEFAULT_SESSION_ID;
7750
+ const filePath = getTaintFilePath(projectRoot, id);
7751
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7752
+ const dir = dirname8(filePath);
7753
+ mkdirSync11(dir, { recursive: true });
7754
+ const existing = readTaint(projectRoot, id);
7755
+ const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
7756
+ const taintFindings = findings.map((f) => ({
7757
+ ruleId: f.ruleId,
7758
+ severity: f.severity,
7759
+ match: f.match,
7760
+ source,
7761
+ detectedAt: now
7762
+ }));
7763
+ const state = {
7764
+ sessionId: id,
7765
+ taintedAt: existing?.taintedAt || now,
7766
+ expiresAt: new Date(Date.now() + TAINT_DURATION_MS).toISOString(),
7767
+ reason,
7768
+ severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
7769
+ findings: [...existing?.findings || [], ...taintFindings]
7770
+ };
7771
+ writeFileSync11(filePath, JSON.stringify(state, null, 2) + "\n");
7772
+ return state;
7773
+ }
7774
+ function clearTaint(projectRoot, sessionId) {
7775
+ if (sessionId) {
7776
+ const filePath = getTaintFilePath(projectRoot, sessionId);
7777
+ try {
7778
+ unlinkSync(filePath);
7779
+ return 1;
7780
+ } catch {
7781
+ return 0;
7782
+ }
7783
+ }
7784
+ const harnessDir = join21(projectRoot, ".harness");
7785
+ let count = 0;
7786
+ try {
7787
+ const files = readdirSync3(harnessDir);
7788
+ for (const file of files) {
7789
+ if (file.startsWith("session-taint-") && file.endsWith(".json")) {
7790
+ try {
7791
+ unlinkSync(join21(harnessDir, file));
7792
+ count++;
7793
+ } catch {
7794
+ }
7795
+ }
7796
+ }
7797
+ } catch {
7798
+ }
7799
+ return count;
7800
+ }
7801
+ function listTaintedSessions(projectRoot) {
7802
+ const harnessDir = join21(projectRoot, ".harness");
7803
+ const sessions = [];
7804
+ try {
7805
+ const files = readdirSync3(harnessDir);
7806
+ for (const file of files) {
7807
+ if (file.startsWith("session-taint-") && file.endsWith(".json")) {
7808
+ const sessionId = file.replace("session-taint-", "").replace(".json", "");
7809
+ const result = checkTaint(projectRoot, sessionId);
7810
+ if (result.tainted) {
7811
+ sessions.push(sessionId);
7812
+ }
7813
+ }
7814
+ }
7815
+ } catch {
7816
+ }
7817
+ return sessions;
7818
+ }
7819
+
7820
+ // src/security/scan-config-shared.ts
7821
+ function mapSecuritySeverity(severity) {
7822
+ if (severity === "error") return "high";
7823
+ if (severity === "warning") return "medium";
7824
+ return "low";
7825
+ }
7826
+ function computeOverallSeverity(findings) {
7827
+ if (findings.length === 0) return "clean";
7828
+ if (findings.some((f) => f.severity === "high")) return "high";
7829
+ if (findings.some((f) => f.severity === "medium")) return "medium";
7830
+ return "low";
7831
+ }
7832
+ function computeScanExitCode(results) {
7833
+ for (const r of results) {
7834
+ if (r.overallSeverity === "high") return 2;
7835
+ }
7836
+ for (const r of results) {
7837
+ if (r.overallSeverity === "medium") return 1;
7838
+ }
7839
+ return 0;
7840
+ }
7841
+ function mapInjectionFindings(injectionFindings) {
7842
+ return injectionFindings.map((f) => ({
7843
+ ruleId: f.ruleId,
7844
+ severity: f.severity,
7845
+ message: `Injection pattern detected: ${f.ruleId}`,
7846
+ match: f.match,
7847
+ ...f.line !== void 0 ? { line: f.line } : {}
7848
+ }));
7849
+ }
7850
+ function isDuplicateFinding(existing, secFinding) {
7851
+ return existing.some(
7852
+ (e) => e.line === secFinding.line && e.match === secFinding.match.trim() && e.ruleId.split("-")[0] === secFinding.ruleId.split("-")[0]
7853
+ );
7854
+ }
7855
+ function mapSecurityFindings(secFindings, existing) {
7856
+ const result = [];
7857
+ for (const f of secFindings) {
7858
+ if (!isDuplicateFinding(existing, f)) {
7859
+ result.push({
7860
+ ruleId: f.ruleId,
7861
+ severity: mapSecuritySeverity(f.severity),
7862
+ message: f.message,
7863
+ match: f.match,
7864
+ ...f.line !== void 0 ? { line: f.line } : {}
7865
+ });
7866
+ }
7867
+ }
7868
+ return result;
7869
+ }
7870
+
7259
7871
  // src/ci/check-orchestrator.ts
7260
7872
  import * as path15 from "path";
7261
7873
  var ALL_CHECKS = [
@@ -7439,7 +8051,7 @@ async function runPerfCheck(projectRoot, config) {
7439
8051
  if (perfReport.complexity) {
7440
8052
  for (const v of perfReport.complexity.violations) {
7441
8053
  issues.push({
7442
- severity: v.severity === "info" ? "warning" : v.severity,
8054
+ severity: "warning",
7443
8055
  message: `[Tier ${v.tier}] ${v.metric}: ${v.function} in ${v.file} (${v.value} > ${v.threshold})`,
7444
8056
  file: v.file,
7445
8057
  line: v.line
@@ -10034,9 +10646,9 @@ async function resolveWasmPath(grammarName) {
10034
10646
  const { createRequire } = await import("module");
10035
10647
  const require2 = createRequire(import.meta.url ?? __filename);
10036
10648
  const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
10037
- const path23 = await import("path");
10038
- const pkgDir = path23.dirname(pkgPath);
10039
- return path23.join(pkgDir, "out", `${grammarName}.wasm`);
10649
+ const path26 = await import("path");
10650
+ const pkgDir = path26.dirname(pkgPath);
10651
+ return path26.join(pkgDir, "out", `${grammarName}.wasm`);
10040
10652
  }
10041
10653
  async function loadLanguage(lang) {
10042
10654
  const grammarName = GRAMMAR_MAP[lang];
@@ -10400,6 +11012,489 @@ async function unfoldRange(filePath, startLine, endLine) {
10400
11012
  };
10401
11013
  }
10402
11014
 
11015
+ // src/pricing/pricing.ts
11016
+ var TOKENS_PER_MILLION = 1e6;
11017
+ function parseLiteLLMData(raw) {
11018
+ const dataset = /* @__PURE__ */ new Map();
11019
+ for (const [modelName, entry] of Object.entries(raw)) {
11020
+ if (modelName === "sample_spec") continue;
11021
+ if (entry.mode && entry.mode !== "chat") continue;
11022
+ const inputCost = entry.input_cost_per_token;
11023
+ const outputCost = entry.output_cost_per_token;
11024
+ if (inputCost == null || outputCost == null) continue;
11025
+ const pricing = {
11026
+ inputPer1M: inputCost * TOKENS_PER_MILLION,
11027
+ outputPer1M: outputCost * TOKENS_PER_MILLION
11028
+ };
11029
+ if (entry.cache_read_input_token_cost != null) {
11030
+ pricing.cacheReadPer1M = entry.cache_read_input_token_cost * TOKENS_PER_MILLION;
11031
+ }
11032
+ if (entry.cache_creation_input_token_cost != null) {
11033
+ pricing.cacheWritePer1M = entry.cache_creation_input_token_cost * TOKENS_PER_MILLION;
11034
+ }
11035
+ dataset.set(modelName, pricing);
11036
+ }
11037
+ return dataset;
11038
+ }
11039
+ function getModelPrice(model, dataset) {
11040
+ if (!model) {
11041
+ console.warn("[harness pricing] No model specified \u2014 cannot look up pricing.");
11042
+ return null;
11043
+ }
11044
+ const pricing = dataset.get(model);
11045
+ if (!pricing) {
11046
+ console.warn(
11047
+ `[harness pricing] No pricing data for model "${model}". Consider updating pricing data.`
11048
+ );
11049
+ return null;
11050
+ }
11051
+ return pricing;
11052
+ }
11053
+
11054
+ // src/pricing/cache.ts
11055
+ import * as fs23 from "fs/promises";
11056
+ import * as path23 from "path";
11057
+
11058
+ // src/pricing/fallback.json
11059
+ var fallback_default = {
11060
+ _generatedAt: "2026-03-31",
11061
+ _source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
11062
+ models: {
11063
+ "claude-opus-4-20250514": {
11064
+ inputPer1M: 15,
11065
+ outputPer1M: 75,
11066
+ cacheReadPer1M: 1.5,
11067
+ cacheWritePer1M: 18.75
11068
+ },
11069
+ "claude-sonnet-4-20250514": {
11070
+ inputPer1M: 3,
11071
+ outputPer1M: 15,
11072
+ cacheReadPer1M: 0.3,
11073
+ cacheWritePer1M: 3.75
11074
+ },
11075
+ "claude-3-5-haiku-20241022": {
11076
+ inputPer1M: 0.8,
11077
+ outputPer1M: 4,
11078
+ cacheReadPer1M: 0.08,
11079
+ cacheWritePer1M: 1
11080
+ },
11081
+ "gpt-4o": {
11082
+ inputPer1M: 2.5,
11083
+ outputPer1M: 10,
11084
+ cacheReadPer1M: 1.25
11085
+ },
11086
+ "gpt-4o-mini": {
11087
+ inputPer1M: 0.15,
11088
+ outputPer1M: 0.6,
11089
+ cacheReadPer1M: 0.075
11090
+ },
11091
+ "gemini-2.0-flash": {
11092
+ inputPer1M: 0.1,
11093
+ outputPer1M: 0.4,
11094
+ cacheReadPer1M: 0.025
11095
+ },
11096
+ "gemini-2.5-pro": {
11097
+ inputPer1M: 1.25,
11098
+ outputPer1M: 10,
11099
+ cacheReadPer1M: 0.3125
11100
+ }
11101
+ }
11102
+ };
11103
+
11104
+ // src/pricing/cache.ts
11105
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
11106
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11107
+ var STALENESS_WARNING_DAYS = 7;
11108
+ function getCachePath(projectRoot) {
11109
+ return path23.join(projectRoot, ".harness", "cache", "pricing.json");
11110
+ }
11111
+ function getStalenessMarkerPath(projectRoot) {
11112
+ return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
11113
+ }
11114
+ async function readDiskCache(projectRoot) {
11115
+ try {
11116
+ const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
11117
+ return JSON.parse(raw);
11118
+ } catch {
11119
+ return null;
11120
+ }
11121
+ }
11122
+ async function writeDiskCache(projectRoot, data) {
11123
+ const cachePath = getCachePath(projectRoot);
11124
+ await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
11125
+ await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
11126
+ }
11127
+ async function fetchFromNetwork() {
11128
+ try {
11129
+ const response = await fetch(LITELLM_PRICING_URL);
11130
+ if (!response.ok) return null;
11131
+ const data = await response.json();
11132
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
11133
+ return {
11134
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
11135
+ data
11136
+ };
11137
+ } catch {
11138
+ return null;
11139
+ }
11140
+ }
11141
+ function loadFallbackDataset() {
11142
+ const fb = fallback_default;
11143
+ const dataset = /* @__PURE__ */ new Map();
11144
+ for (const [model, pricing] of Object.entries(fb.models)) {
11145
+ dataset.set(model, pricing);
11146
+ }
11147
+ return dataset;
11148
+ }
11149
+ async function checkAndWarnStaleness(projectRoot) {
11150
+ const markerPath = getStalenessMarkerPath(projectRoot);
11151
+ try {
11152
+ const raw = await fs23.readFile(markerPath, "utf-8");
11153
+ const marker = JSON.parse(raw);
11154
+ const firstUse = new Date(marker.firstFallbackUse).getTime();
11155
+ const now = Date.now();
11156
+ const daysSinceFirstUse = (now - firstUse) / (24 * 60 * 60 * 1e3);
11157
+ if (daysSinceFirstUse > STALENESS_WARNING_DAYS) {
11158
+ console.warn(
11159
+ `[harness pricing] Pricing data is stale \u2014 using bundled fallback for ${Math.floor(daysSinceFirstUse)} days. Connect to the internet to refresh pricing data.`
11160
+ );
11161
+ }
11162
+ } catch {
11163
+ try {
11164
+ await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
11165
+ await fs23.writeFile(
11166
+ markerPath,
11167
+ JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
11168
+ );
11169
+ } catch {
11170
+ }
11171
+ }
11172
+ }
11173
+ async function clearStalenessMarker(projectRoot) {
11174
+ try {
11175
+ await fs23.unlink(getStalenessMarkerPath(projectRoot));
11176
+ } catch {
11177
+ }
11178
+ }
11179
+ async function loadPricingData(projectRoot) {
11180
+ const cache = await readDiskCache(projectRoot);
11181
+ if (cache) {
11182
+ const cacheAge = Date.now() - new Date(cache.fetchedAt).getTime();
11183
+ if (cacheAge < CACHE_TTL_MS) {
11184
+ await clearStalenessMarker(projectRoot);
11185
+ return parseLiteLLMData(cache.data);
11186
+ }
11187
+ }
11188
+ const fetched = await fetchFromNetwork();
11189
+ if (fetched) {
11190
+ await writeDiskCache(projectRoot, fetched);
11191
+ await clearStalenessMarker(projectRoot);
11192
+ return parseLiteLLMData(fetched.data);
11193
+ }
11194
+ if (cache) {
11195
+ return parseLiteLLMData(cache.data);
11196
+ }
11197
+ await checkAndWarnStaleness(projectRoot);
11198
+ return loadFallbackDataset();
11199
+ }
11200
+
11201
+ // src/pricing/calculator.ts
11202
+ var MICRODOLLARS_PER_DOLLAR = 1e6;
11203
+ var TOKENS_PER_MILLION2 = 1e6;
11204
+ function calculateCost(record, dataset) {
11205
+ if (!record.model) return null;
11206
+ const pricing = getModelPrice(record.model, dataset);
11207
+ if (!pricing) return null;
11208
+ let costUSD = 0;
11209
+ costUSD += record.tokens.inputTokens / TOKENS_PER_MILLION2 * pricing.inputPer1M;
11210
+ costUSD += record.tokens.outputTokens / TOKENS_PER_MILLION2 * pricing.outputPer1M;
11211
+ if (record.cacheReadTokens != null && pricing.cacheReadPer1M != null) {
11212
+ costUSD += record.cacheReadTokens / TOKENS_PER_MILLION2 * pricing.cacheReadPer1M;
11213
+ }
11214
+ if (record.cacheCreationTokens != null && pricing.cacheWritePer1M != null) {
11215
+ costUSD += record.cacheCreationTokens / TOKENS_PER_MILLION2 * pricing.cacheWritePer1M;
11216
+ }
11217
+ return Math.round(costUSD * MICRODOLLARS_PER_DOLLAR);
11218
+ }
11219
+
11220
+ // src/usage/aggregator.ts
11221
+ function aggregateBySession(records) {
11222
+ if (records.length === 0) return [];
11223
+ const sessionMap = /* @__PURE__ */ new Map();
11224
+ for (const record of records) {
11225
+ const tagged = record;
11226
+ const id = record.sessionId;
11227
+ if (!sessionMap.has(id)) {
11228
+ sessionMap.set(id, { harnessRecords: [], ccRecords: [], allRecords: [] });
11229
+ }
11230
+ const bucket = sessionMap.get(id);
11231
+ if (tagged._source === "claude-code") {
11232
+ bucket.ccRecords.push(tagged);
11233
+ } else {
11234
+ bucket.harnessRecords.push(tagged);
11235
+ }
11236
+ bucket.allRecords.push(tagged);
11237
+ }
11238
+ const results = [];
11239
+ for (const [sessionId, bucket] of sessionMap) {
11240
+ const hasHarness = bucket.harnessRecords.length > 0;
11241
+ const hasCC = bucket.ccRecords.length > 0;
11242
+ const isMerged = hasHarness && hasCC;
11243
+ const tokenSource = hasHarness ? bucket.harnessRecords : bucket.ccRecords;
11244
+ const tokens = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
11245
+ let cacheCreation;
11246
+ let cacheRead;
11247
+ let costMicroUSD = 0;
11248
+ let model;
11249
+ for (const r of tokenSource) {
11250
+ tokens.inputTokens += r.tokens.inputTokens;
11251
+ tokens.outputTokens += r.tokens.outputTokens;
11252
+ tokens.totalTokens += r.tokens.totalTokens;
11253
+ if (r.cacheCreationTokens != null) {
11254
+ cacheCreation = (cacheCreation ?? 0) + r.cacheCreationTokens;
11255
+ }
11256
+ if (r.cacheReadTokens != null) {
11257
+ cacheRead = (cacheRead ?? 0) + r.cacheReadTokens;
11258
+ }
11259
+ if (r.costMicroUSD != null && costMicroUSD != null) {
11260
+ costMicroUSD += r.costMicroUSD;
11261
+ } else if (r.costMicroUSD == null) {
11262
+ costMicroUSD = null;
11263
+ }
11264
+ if (!model && r.model) {
11265
+ model = r.model;
11266
+ }
11267
+ }
11268
+ if (!model && hasCC) {
11269
+ for (const r of bucket.ccRecords) {
11270
+ if (r.model) {
11271
+ model = r.model;
11272
+ break;
11273
+ }
11274
+ }
11275
+ }
11276
+ const timestamps = bucket.allRecords.map((r) => r.timestamp).sort();
11277
+ const source = isMerged ? "merged" : hasCC ? "claude-code" : "harness";
11278
+ const session = {
11279
+ sessionId,
11280
+ firstTimestamp: timestamps[0] ?? "",
11281
+ lastTimestamp: timestamps[timestamps.length - 1] ?? "",
11282
+ tokens,
11283
+ costMicroUSD,
11284
+ source
11285
+ };
11286
+ if (model) session.model = model;
11287
+ if (cacheCreation != null) session.cacheCreationTokens = cacheCreation;
11288
+ if (cacheRead != null) session.cacheReadTokens = cacheRead;
11289
+ results.push(session);
11290
+ }
11291
+ results.sort((a, b) => b.firstTimestamp.localeCompare(a.firstTimestamp));
11292
+ return results;
11293
+ }
11294
+ function aggregateByDay(records) {
11295
+ if (records.length === 0) return [];
11296
+ const dayMap = /* @__PURE__ */ new Map();
11297
+ for (const record of records) {
11298
+ const date = record.timestamp.slice(0, 10);
11299
+ if (!dayMap.has(date)) {
11300
+ dayMap.set(date, {
11301
+ sessions: /* @__PURE__ */ new Set(),
11302
+ tokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
11303
+ costMicroUSD: 0,
11304
+ models: /* @__PURE__ */ new Set()
11305
+ });
11306
+ }
11307
+ const day = dayMap.get(date);
11308
+ day.sessions.add(record.sessionId);
11309
+ day.tokens.inputTokens += record.tokens.inputTokens;
11310
+ day.tokens.outputTokens += record.tokens.outputTokens;
11311
+ day.tokens.totalTokens += record.tokens.totalTokens;
11312
+ if (record.cacheCreationTokens != null) {
11313
+ day.cacheCreation = (day.cacheCreation ?? 0) + record.cacheCreationTokens;
11314
+ }
11315
+ if (record.cacheReadTokens != null) {
11316
+ day.cacheRead = (day.cacheRead ?? 0) + record.cacheReadTokens;
11317
+ }
11318
+ if (record.costMicroUSD != null && day.costMicroUSD != null) {
11319
+ day.costMicroUSD += record.costMicroUSD;
11320
+ } else if (record.costMicroUSD == null) {
11321
+ day.costMicroUSD = null;
11322
+ }
11323
+ if (record.model) {
11324
+ day.models.add(record.model);
11325
+ }
11326
+ }
11327
+ const results = [];
11328
+ for (const [date, day] of dayMap) {
11329
+ const entry = {
11330
+ date,
11331
+ sessionCount: day.sessions.size,
11332
+ tokens: day.tokens,
11333
+ costMicroUSD: day.costMicroUSD,
11334
+ models: Array.from(day.models).sort()
11335
+ };
11336
+ if (day.cacheCreation != null) entry.cacheCreationTokens = day.cacheCreation;
11337
+ if (day.cacheRead != null) entry.cacheReadTokens = day.cacheRead;
11338
+ results.push(entry);
11339
+ }
11340
+ results.sort((a, b) => b.date.localeCompare(a.date));
11341
+ return results;
11342
+ }
11343
+
11344
+ // src/usage/jsonl-reader.ts
11345
+ import * as fs24 from "fs";
11346
+ import * as path24 from "path";
11347
+ function parseLine(line, lineNumber) {
11348
+ let entry;
11349
+ try {
11350
+ entry = JSON.parse(line);
11351
+ } catch {
11352
+ console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
11353
+ return null;
11354
+ }
11355
+ const tokenUsage = entry.token_usage;
11356
+ if (!tokenUsage || typeof tokenUsage !== "object") {
11357
+ console.warn(
11358
+ `[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
11359
+ );
11360
+ return null;
11361
+ }
11362
+ const inputTokens = tokenUsage.input_tokens ?? 0;
11363
+ const outputTokens = tokenUsage.output_tokens ?? 0;
11364
+ const record = {
11365
+ sessionId: entry.session_id ?? "unknown",
11366
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
11367
+ tokens: {
11368
+ inputTokens,
11369
+ outputTokens,
11370
+ totalTokens: inputTokens + outputTokens
11371
+ }
11372
+ };
11373
+ if (entry.cache_creation_tokens != null) {
11374
+ record.cacheCreationTokens = entry.cache_creation_tokens;
11375
+ }
11376
+ if (entry.cache_read_tokens != null) {
11377
+ record.cacheReadTokens = entry.cache_read_tokens;
11378
+ }
11379
+ if (entry.model != null) {
11380
+ record.model = entry.model;
11381
+ }
11382
+ return record;
11383
+ }
11384
+ function readCostRecords(projectRoot) {
11385
+ const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
11386
+ let raw;
11387
+ try {
11388
+ raw = fs24.readFileSync(costsFile, "utf-8");
11389
+ } catch {
11390
+ return [];
11391
+ }
11392
+ const records = [];
11393
+ const lines = raw.split("\n");
11394
+ for (let i = 0; i < lines.length; i++) {
11395
+ const line = lines[i]?.trim();
11396
+ if (!line) continue;
11397
+ const record = parseLine(line, i + 1);
11398
+ if (record) {
11399
+ records.push(record);
11400
+ }
11401
+ }
11402
+ return records;
11403
+ }
11404
+
11405
+ // src/usage/cc-parser.ts
11406
+ import * as fs25 from "fs";
11407
+ import * as path25 from "path";
11408
+ import * as os2 from "os";
11409
+ function extractUsage(entry) {
11410
+ if (entry.type !== "assistant") return null;
11411
+ const message = entry.message;
11412
+ if (!message || typeof message !== "object") return null;
11413
+ const usage = message.usage;
11414
+ return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
11415
+ }
11416
+ function buildRecord(entry, usage) {
11417
+ const inputTokens = Number(usage.input_tokens) || 0;
11418
+ const outputTokens = Number(usage.output_tokens) || 0;
11419
+ const message = entry.message;
11420
+ const record = {
11421
+ sessionId: entry.sessionId ?? "unknown",
11422
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
11423
+ tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
11424
+ _source: "claude-code"
11425
+ };
11426
+ const model = message.model;
11427
+ if (model) record.model = model;
11428
+ const cacheCreate = usage.cache_creation_input_tokens;
11429
+ const cacheRead = usage.cache_read_input_tokens;
11430
+ if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
11431
+ if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
11432
+ return record;
11433
+ }
11434
+ function parseCCLine(line, filePath, lineNumber) {
11435
+ let entry;
11436
+ try {
11437
+ entry = JSON.parse(line);
11438
+ } catch {
11439
+ console.warn(
11440
+ `[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
11441
+ );
11442
+ return null;
11443
+ }
11444
+ const usage = extractUsage(entry);
11445
+ if (!usage) return null;
11446
+ return {
11447
+ record: buildRecord(entry, usage),
11448
+ requestId: entry.requestId ?? null
11449
+ };
11450
+ }
11451
+ function readCCFile(filePath) {
11452
+ let raw;
11453
+ try {
11454
+ raw = fs25.readFileSync(filePath, "utf-8");
11455
+ } catch {
11456
+ return [];
11457
+ }
11458
+ const byRequestId = /* @__PURE__ */ new Map();
11459
+ const noRequestId = [];
11460
+ const lines = raw.split("\n");
11461
+ for (let i = 0; i < lines.length; i++) {
11462
+ const line = lines[i]?.trim();
11463
+ if (!line) continue;
11464
+ const parsed = parseCCLine(line, filePath, i + 1);
11465
+ if (!parsed) continue;
11466
+ if (parsed.requestId) {
11467
+ byRequestId.set(parsed.requestId, parsed.record);
11468
+ } else {
11469
+ noRequestId.push(parsed.record);
11470
+ }
11471
+ }
11472
+ return [...byRequestId.values(), ...noRequestId];
11473
+ }
11474
+ function parseCCRecords() {
11475
+ const homeDir = process.env.HOME ?? os2.homedir();
11476
+ const projectsDir = path25.join(homeDir, ".claude", "projects");
11477
+ let projectDirs;
11478
+ try {
11479
+ projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
11480
+ } catch {
11481
+ return [];
11482
+ }
11483
+ const records = [];
11484
+ for (const dir of projectDirs) {
11485
+ let files;
11486
+ try {
11487
+ files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
11488
+ } catch {
11489
+ continue;
11490
+ }
11491
+ for (const file of files) {
11492
+ records.push(...readCCFile(file));
11493
+ }
11494
+ }
11495
+ return records;
11496
+ }
11497
+
10403
11498
  // src/index.ts
10404
11499
  var VERSION = "0.15.0";
10405
11500
  export {
@@ -10417,6 +11512,7 @@ export {
10417
11512
  BlueprintGenerator,
10418
11513
  BundleConstraintsSchema,
10419
11514
  BundleSchema,
11515
+ CACHE_TTL_MS,
10420
11516
  COMPLIANCE_DESCRIPTOR,
10421
11517
  CategoryBaselineSchema,
10422
11518
  CategoryRegressionSchema,
@@ -10434,6 +11530,7 @@ export {
10434
11530
  DEFAULT_SECURITY_CONFIG,
10435
11531
  DEFAULT_STATE,
10436
11532
  DEFAULT_STREAM_INDEX,
11533
+ DESTRUCTIVE_BASH,
10437
11534
  DepDepthCollector,
10438
11535
  EXTENSION_MAP,
10439
11536
  EmitInteractionInputSchema,
@@ -10448,6 +11545,7 @@ export {
10448
11545
  HandoffSchema,
10449
11546
  HarnessStateSchema,
10450
11547
  InteractionTypeSchema,
11548
+ LITELLM_PRICING_URL,
10451
11549
  LayerViolationCollector,
10452
11550
  LockfilePackageSchema,
10453
11551
  LockfileSchema,
@@ -10464,6 +11562,7 @@ export {
10464
11562
  RegressionDetector,
10465
11563
  RuleRegistry,
10466
11564
  SECURITY_DESCRIPTOR,
11565
+ STALENESS_WARNING_DAYS,
10467
11566
  SecurityConfigSchema,
10468
11567
  SecurityScanner,
10469
11568
  SharableBoundaryConfigSchema,
@@ -10480,6 +11579,8 @@ export {
10480
11579
  ViolationSchema,
10481
11580
  addProvenance,
10482
11581
  agentConfigRules,
11582
+ aggregateByDay,
11583
+ aggregateBySession,
10483
11584
  analyzeDiff,
10484
11585
  analyzeLearningPatterns,
10485
11586
  appendFailure,
@@ -10498,13 +11599,18 @@ export {
10498
11599
  buildDependencyGraph,
10499
11600
  buildExclusionSet,
10500
11601
  buildSnapshot,
11602
+ calculateCost,
10501
11603
  checkDocCoverage,
10502
11604
  checkEligibility,
10503
11605
  checkEvidenceCoverage,
11606
+ checkTaint,
10504
11607
  classifyFinding,
10505
11608
  clearEventHashCache,
10506
11609
  clearFailuresCache,
10507
11610
  clearLearningsCache,
11611
+ clearTaint,
11612
+ computeOverallSeverity,
11613
+ computeScanExitCode,
10508
11614
  configureFeedback,
10509
11615
  constraintRuleId,
10510
11616
  contextBudget,
@@ -10559,35 +11665,48 @@ export {
10559
11665
  getActionEmitter,
10560
11666
  getExitCode,
10561
11667
  getFeedbackConfig,
11668
+ getInjectionPatterns,
11669
+ getModelPrice,
10562
11670
  getOutline,
10563
11671
  getParser,
10564
11672
  getPhaseCategories,
10565
11673
  getStreamForBranch,
11674
+ getTaintFilePath,
10566
11675
  getUpdateNotification,
10567
11676
  goRules,
10568
11677
  injectionRules,
11678
+ insecureDefaultsRules,
11679
+ isDuplicateFinding,
10569
11680
  isSmallSuggestion,
10570
11681
  isUpdateCheckEnabled,
10571
11682
  listActiveSessions,
10572
11683
  listStreams,
11684
+ listTaintedSessions,
10573
11685
  loadBudgetedLearnings,
10574
11686
  loadEvents,
10575
11687
  loadFailures,
10576
11688
  loadHandoff,
10577
11689
  loadIndexEntries,
11690
+ loadPricingData,
10578
11691
  loadRelevantLearnings,
10579
11692
  loadSessionSummary,
10580
11693
  loadState,
10581
11694
  loadStreamIndex,
10582
11695
  logAgentAction,
11696
+ mapInjectionFindings,
11697
+ mapSecurityFindings,
11698
+ mapSecuritySeverity,
10583
11699
  mcpRules,
10584
11700
  migrateToStreams,
10585
11701
  networkRules,
10586
11702
  nodeRules,
11703
+ parseCCRecords,
10587
11704
  parseDateFromEntry,
10588
11705
  parseDiff,
10589
11706
  parseFile,
10590
11707
  parseFrontmatter,
11708
+ parseHarnessIgnore,
11709
+ parseLiteLLMData,
10591
11710
  parseManifest,
10592
11711
  parseRoadmap,
10593
11712
  parseSecurityConfig,
@@ -10598,9 +11717,11 @@ export {
10598
11717
  pruneLearnings,
10599
11718
  reactRules,
10600
11719
  readCheckState,
11720
+ readCostRecords,
10601
11721
  readLockfile,
10602
11722
  readSessionSection,
10603
11723
  readSessionSections,
11724
+ readTaint,
10604
11725
  removeContributions,
10605
11726
  removeProvenance,
10606
11727
  requestMultiplePeerReviews,
@@ -10627,11 +11748,13 @@ export {
10627
11748
  saveHandoff,
10628
11749
  saveState,
10629
11750
  saveStreamIndex,
11751
+ scanForInjection,
10630
11752
  scopeContext,
10631
11753
  searchSymbols,
10632
11754
  secretRules,
10633
11755
  serializeRoadmap,
10634
11756
  setActiveStream,
11757
+ sharpEdgesRules,
10635
11758
  shouldRunCheck,
10636
11759
  spawnBackgroundCheck,
10637
11760
  syncConstraintNodes,
@@ -10656,5 +11779,6 @@ export {
10656
11779
  writeConfig,
10657
11780
  writeLockfile,
10658
11781
  writeSessionSummary,
11782
+ writeTaint,
10659
11783
  xssRules
10660
11784
  };