@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.js CHANGED
@@ -45,6 +45,7 @@ __export(index_exports, {
45
45
  BlueprintGenerator: () => BlueprintGenerator,
46
46
  BundleConstraintsSchema: () => BundleConstraintsSchema,
47
47
  BundleSchema: () => BundleSchema,
48
+ CACHE_TTL_MS: () => CACHE_TTL_MS,
48
49
  COMPLIANCE_DESCRIPTOR: () => COMPLIANCE_DESCRIPTOR,
49
50
  CategoryBaselineSchema: () => CategoryBaselineSchema,
50
51
  CategoryRegressionSchema: () => CategoryRegressionSchema,
@@ -62,6 +63,7 @@ __export(index_exports, {
62
63
  DEFAULT_SECURITY_CONFIG: () => DEFAULT_SECURITY_CONFIG,
63
64
  DEFAULT_STATE: () => DEFAULT_STATE,
64
65
  DEFAULT_STREAM_INDEX: () => DEFAULT_STREAM_INDEX,
66
+ DESTRUCTIVE_BASH: () => DESTRUCTIVE_BASH,
65
67
  DepDepthCollector: () => DepDepthCollector,
66
68
  EXTENSION_MAP: () => EXTENSION_MAP,
67
69
  EmitInteractionInputSchema: () => EmitInteractionInputSchema,
@@ -76,6 +78,7 @@ __export(index_exports, {
76
78
  HandoffSchema: () => HandoffSchema,
77
79
  HarnessStateSchema: () => HarnessStateSchema,
78
80
  InteractionTypeSchema: () => InteractionTypeSchema,
81
+ LITELLM_PRICING_URL: () => LITELLM_PRICING_URL,
79
82
  LayerViolationCollector: () => LayerViolationCollector,
80
83
  LockfilePackageSchema: () => LockfilePackageSchema,
81
84
  LockfileSchema: () => LockfileSchema,
@@ -92,6 +95,7 @@ __export(index_exports, {
92
95
  RegressionDetector: () => RegressionDetector,
93
96
  RuleRegistry: () => RuleRegistry,
94
97
  SECURITY_DESCRIPTOR: () => SECURITY_DESCRIPTOR,
98
+ STALENESS_WARNING_DAYS: () => STALENESS_WARNING_DAYS,
95
99
  SecurityConfigSchema: () => SecurityConfigSchema,
96
100
  SecurityScanner: () => SecurityScanner,
97
101
  SharableBoundaryConfigSchema: () => SharableBoundaryConfigSchema,
@@ -108,6 +112,8 @@ __export(index_exports, {
108
112
  ViolationSchema: () => ViolationSchema,
109
113
  addProvenance: () => addProvenance,
110
114
  agentConfigRules: () => agentConfigRules,
115
+ aggregateByDay: () => aggregateByDay,
116
+ aggregateBySession: () => aggregateBySession,
111
117
  analyzeDiff: () => analyzeDiff,
112
118
  analyzeLearningPatterns: () => analyzeLearningPatterns,
113
119
  appendFailure: () => appendFailure,
@@ -126,13 +132,18 @@ __export(index_exports, {
126
132
  buildDependencyGraph: () => buildDependencyGraph,
127
133
  buildExclusionSet: () => buildExclusionSet,
128
134
  buildSnapshot: () => buildSnapshot,
135
+ calculateCost: () => calculateCost,
129
136
  checkDocCoverage: () => checkDocCoverage,
130
137
  checkEligibility: () => checkEligibility,
131
138
  checkEvidenceCoverage: () => checkEvidenceCoverage,
139
+ checkTaint: () => checkTaint,
132
140
  classifyFinding: () => classifyFinding,
133
141
  clearEventHashCache: () => clearEventHashCache,
134
142
  clearFailuresCache: () => clearFailuresCache,
135
143
  clearLearningsCache: () => clearLearningsCache,
144
+ clearTaint: () => clearTaint,
145
+ computeOverallSeverity: () => computeOverallSeverity,
146
+ computeScanExitCode: () => computeScanExitCode,
136
147
  configureFeedback: () => configureFeedback,
137
148
  constraintRuleId: () => constraintRuleId,
138
149
  contextBudget: () => contextBudget,
@@ -187,35 +198,48 @@ __export(index_exports, {
187
198
  getActionEmitter: () => getActionEmitter,
188
199
  getExitCode: () => getExitCode,
189
200
  getFeedbackConfig: () => getFeedbackConfig,
201
+ getInjectionPatterns: () => getInjectionPatterns,
202
+ getModelPrice: () => getModelPrice,
190
203
  getOutline: () => getOutline,
191
204
  getParser: () => getParser,
192
205
  getPhaseCategories: () => getPhaseCategories,
193
206
  getStreamForBranch: () => getStreamForBranch,
207
+ getTaintFilePath: () => getTaintFilePath,
194
208
  getUpdateNotification: () => getUpdateNotification,
195
209
  goRules: () => goRules,
196
210
  injectionRules: () => injectionRules,
211
+ insecureDefaultsRules: () => insecureDefaultsRules,
212
+ isDuplicateFinding: () => isDuplicateFinding,
197
213
  isSmallSuggestion: () => isSmallSuggestion,
198
214
  isUpdateCheckEnabled: () => isUpdateCheckEnabled,
199
215
  listActiveSessions: () => listActiveSessions,
200
216
  listStreams: () => listStreams,
217
+ listTaintedSessions: () => listTaintedSessions,
201
218
  loadBudgetedLearnings: () => loadBudgetedLearnings,
202
219
  loadEvents: () => loadEvents,
203
220
  loadFailures: () => loadFailures,
204
221
  loadHandoff: () => loadHandoff,
205
222
  loadIndexEntries: () => loadIndexEntries,
223
+ loadPricingData: () => loadPricingData,
206
224
  loadRelevantLearnings: () => loadRelevantLearnings,
207
225
  loadSessionSummary: () => loadSessionSummary,
208
226
  loadState: () => loadState,
209
227
  loadStreamIndex: () => loadStreamIndex,
210
228
  logAgentAction: () => logAgentAction,
229
+ mapInjectionFindings: () => mapInjectionFindings,
230
+ mapSecurityFindings: () => mapSecurityFindings,
231
+ mapSecuritySeverity: () => mapSecuritySeverity,
211
232
  mcpRules: () => mcpRules,
212
233
  migrateToStreams: () => migrateToStreams,
213
234
  networkRules: () => networkRules,
214
235
  nodeRules: () => nodeRules,
236
+ parseCCRecords: () => parseCCRecords,
215
237
  parseDateFromEntry: () => parseDateFromEntry,
216
238
  parseDiff: () => parseDiff,
217
239
  parseFile: () => parseFile,
218
240
  parseFrontmatter: () => parseFrontmatter,
241
+ parseHarnessIgnore: () => parseHarnessIgnore,
242
+ parseLiteLLMData: () => parseLiteLLMData,
219
243
  parseManifest: () => parseManifest,
220
244
  parseRoadmap: () => parseRoadmap,
221
245
  parseSecurityConfig: () => parseSecurityConfig,
@@ -226,9 +250,11 @@ __export(index_exports, {
226
250
  pruneLearnings: () => pruneLearnings,
227
251
  reactRules: () => reactRules,
228
252
  readCheckState: () => readCheckState,
253
+ readCostRecords: () => readCostRecords,
229
254
  readLockfile: () => readLockfile,
230
255
  readSessionSection: () => readSessionSection,
231
256
  readSessionSections: () => readSessionSections,
257
+ readTaint: () => readTaint,
232
258
  removeContributions: () => removeContributions,
233
259
  removeProvenance: () => removeProvenance,
234
260
  requestMultiplePeerReviews: () => requestMultiplePeerReviews,
@@ -255,11 +281,13 @@ __export(index_exports, {
255
281
  saveHandoff: () => saveHandoff,
256
282
  saveState: () => saveState,
257
283
  saveStreamIndex: () => saveStreamIndex,
284
+ scanForInjection: () => scanForInjection,
258
285
  scopeContext: () => scopeContext,
259
286
  searchSymbols: () => searchSymbols,
260
287
  secretRules: () => secretRules,
261
288
  serializeRoadmap: () => serializeRoadmap,
262
289
  setActiveStream: () => setActiveStream,
290
+ sharpEdgesRules: () => sharpEdgesRules,
263
291
  shouldRunCheck: () => shouldRunCheck,
264
292
  spawnBackgroundCheck: () => spawnBackgroundCheck,
265
293
  syncConstraintNodes: () => syncConstraintNodes,
@@ -284,6 +312,7 @@ __export(index_exports, {
284
312
  writeConfig: () => writeConfig,
285
313
  writeLockfile: () => writeLockfile,
286
314
  writeSessionSummary: () => writeSessionSummary,
315
+ writeTaint: () => writeTaint,
287
316
  xssRules: () => xssRules
288
317
  });
289
318
  module.exports = __toCommonJS(index_exports);
@@ -307,17 +336,17 @@ var import_node_path = require("path");
307
336
  var import_glob = require("glob");
308
337
  var accessAsync = (0, import_util.promisify)(import_fs.access);
309
338
  var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
310
- async function fileExists(path23) {
339
+ async function fileExists(path26) {
311
340
  try {
312
- await accessAsync(path23, import_fs.constants.F_OK);
341
+ await accessAsync(path26, import_fs.constants.F_OK);
313
342
  return true;
314
343
  } catch {
315
344
  return false;
316
345
  }
317
346
  }
318
- async function readFileContent(path23) {
347
+ async function readFileContent(path26) {
319
348
  try {
320
- const content = await readFileAsync(path23, "utf-8");
349
+ const content = await readFileAsync(path26, "utf-8");
321
350
  return (0, import_types.Ok)(content);
322
351
  } catch (error) {
323
352
  return (0, import_types.Err)(error);
@@ -368,15 +397,15 @@ function validateConfig(data, schema) {
368
397
  let message = "Configuration validation failed";
369
398
  const suggestions = [];
370
399
  if (firstError) {
371
- const path23 = firstError.path.join(".");
372
- const pathDisplay = path23 ? ` at "${path23}"` : "";
400
+ const path26 = firstError.path.join(".");
401
+ const pathDisplay = path26 ? ` at "${path26}"` : "";
373
402
  if (firstError.code === "invalid_type") {
374
403
  const received = firstError.received;
375
404
  const expected = firstError.expected;
376
405
  if (received === "undefined") {
377
406
  code = "MISSING_FIELD";
378
407
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
379
- suggestions.push(`Field "${path23}" is required and must be of type "${expected}"`);
408
+ suggestions.push(`Field "${path26}" is required and must be of type "${expected}"`);
380
409
  } else {
381
410
  code = "INVALID_TYPE";
382
411
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -592,27 +621,27 @@ function extractSections(content) {
592
621
  }
593
622
  return sections.map((section) => buildAgentMapSection(section, lines));
594
623
  }
595
- function isExternalLink(path23) {
596
- return path23.startsWith("http://") || path23.startsWith("https://") || path23.startsWith("#") || path23.startsWith("mailto:");
624
+ function isExternalLink(path26) {
625
+ return path26.startsWith("http://") || path26.startsWith("https://") || path26.startsWith("#") || path26.startsWith("mailto:");
597
626
  }
598
627
  function resolveLinkPath(linkPath, baseDir) {
599
628
  return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
600
629
  }
601
- async function validateAgentsMap(path23 = "./AGENTS.md") {
602
- const contentResult = await readFileContent(path23);
630
+ async function validateAgentsMap(path26 = "./AGENTS.md") {
631
+ const contentResult = await readFileContent(path26);
603
632
  if (!contentResult.ok) {
604
633
  return (0, import_types.Err)(
605
634
  createError(
606
635
  "PARSE_ERROR",
607
636
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
608
- { path: path23 },
637
+ { path: path26 },
609
638
  ["Ensure the file exists", "Check file permissions"]
610
639
  )
611
640
  );
612
641
  }
613
642
  const content = contentResult.value;
614
643
  const sections = extractSections(content);
615
- const baseDir = (0, import_path.dirname)(path23);
644
+ const baseDir = (0, import_path.dirname)(path26);
616
645
  const sectionTitles = sections.map((s) => s.title);
617
646
  const missingSections = REQUIRED_SECTIONS.filter(
618
647
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -753,8 +782,8 @@ async function checkDocCoverage(domain, options = {}) {
753
782
 
754
783
  // src/context/knowledge-map.ts
755
784
  var import_path3 = require("path");
756
- function suggestFix(path23, existingFiles) {
757
- const targetName = (0, import_path3.basename)(path23).toLowerCase();
785
+ function suggestFix(path26, existingFiles) {
786
+ const targetName = (0, import_path3.basename)(path26).toLowerCase();
758
787
  const similar = existingFiles.find((file) => {
759
788
  const fileName = (0, import_path3.basename)(file).toLowerCase();
760
789
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -762,7 +791,7 @@ function suggestFix(path23, existingFiles) {
762
791
  if (similar) {
763
792
  return `Did you mean "${similar}"?`;
764
793
  }
765
- return `Create the file "${path23}" or remove the link`;
794
+ return `Create the file "${path26}" or remove the link`;
766
795
  }
767
796
  async function validateKnowledgeMap(rootDir = process.cwd()) {
768
797
  const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
@@ -1368,8 +1397,8 @@ function createBoundaryValidator(schema, name) {
1368
1397
  return (0, import_types.Ok)(result.data);
1369
1398
  }
1370
1399
  const suggestions = result.error.issues.map((issue) => {
1371
- const path23 = issue.path.join(".");
1372
- return path23 ? `${path23}: ${issue.message}` : issue.message;
1400
+ const path26 = issue.path.join(".");
1401
+ return path26 ? `${path26}: ${issue.message}` : issue.message;
1373
1402
  });
1374
1403
  return (0, import_types.Err)(
1375
1404
  createError(
@@ -2001,11 +2030,11 @@ function processExportListSpecifiers(exportDecl, exports2) {
2001
2030
  var TypeScriptParser = class {
2002
2031
  name = "typescript";
2003
2032
  extensions = [".ts", ".tsx", ".mts", ".cts"];
2004
- async parseFile(path23) {
2005
- const contentResult = await readFileContent(path23);
2033
+ async parseFile(path26) {
2034
+ const contentResult = await readFileContent(path26);
2006
2035
  if (!contentResult.ok) {
2007
2036
  return (0, import_types.Err)(
2008
- createParseError("NOT_FOUND", `File not found: ${path23}`, { path: path23 }, [
2037
+ createParseError("NOT_FOUND", `File not found: ${path26}`, { path: path26 }, [
2009
2038
  "Check that the file exists",
2010
2039
  "Verify the path is correct"
2011
2040
  ])
@@ -2015,7 +2044,7 @@ var TypeScriptParser = class {
2015
2044
  const ast = (0, import_typescript_estree.parse)(contentResult.value, {
2016
2045
  loc: true,
2017
2046
  range: true,
2018
- jsx: path23.endsWith(".tsx"),
2047
+ jsx: path26.endsWith(".tsx"),
2019
2048
  errorOnUnknownASTType: false
2020
2049
  });
2021
2050
  return (0, import_types.Ok)({
@@ -2026,7 +2055,7 @@ var TypeScriptParser = class {
2026
2055
  } catch (e) {
2027
2056
  const error = e;
2028
2057
  return (0, import_types.Err)(
2029
- createParseError("SYNTAX_ERROR", `Failed to parse ${path23}: ${error.message}`, { path: path23 }, [
2058
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path26}: ${error.message}`, { path: path26 }, [
2030
2059
  "Check for syntax errors in the file",
2031
2060
  "Ensure valid TypeScript syntax"
2032
2061
  ])
@@ -2211,22 +2240,22 @@ function extractInlineRefs(content) {
2211
2240
  }
2212
2241
  return refs;
2213
2242
  }
2214
- async function parseDocumentationFile(path23) {
2215
- const contentResult = await readFileContent(path23);
2243
+ async function parseDocumentationFile(path26) {
2244
+ const contentResult = await readFileContent(path26);
2216
2245
  if (!contentResult.ok) {
2217
2246
  return (0, import_types.Err)(
2218
2247
  createEntropyError(
2219
2248
  "PARSE_ERROR",
2220
- `Failed to read documentation file: ${path23}`,
2221
- { file: path23 },
2249
+ `Failed to read documentation file: ${path26}`,
2250
+ { file: path26 },
2222
2251
  ["Check that the file exists"]
2223
2252
  )
2224
2253
  );
2225
2254
  }
2226
2255
  const content = contentResult.value;
2227
- const type = path23.endsWith(".md") ? "markdown" : "text";
2256
+ const type = path26.endsWith(".md") ? "markdown" : "text";
2228
2257
  return (0, import_types.Ok)({
2229
- path: path23,
2258
+ path: path26,
2230
2259
  type,
2231
2260
  content,
2232
2261
  codeBlocks: extractCodeBlocks(content),
@@ -9074,6 +9103,208 @@ var mcpRules = [
9074
9103
  }
9075
9104
  ];
9076
9105
 
9106
+ // src/security/rules/insecure-defaults.ts
9107
+ var insecureDefaultsRules = [
9108
+ {
9109
+ id: "SEC-DEF-001",
9110
+ name: "Security-Sensitive Fallback to Hardcoded Default",
9111
+ category: "insecure-defaults",
9112
+ severity: "warning",
9113
+ confidence: "medium",
9114
+ patterns: [
9115
+ /(?:SECRET|KEY|TOKEN|PASSWORD|SALT|PEPPER|SIGNING|ENCRYPTION|AUTH|JWT|SESSION).*(?:\|\||\?\?)\s*['"][^'"]+['"]/i
9116
+ ],
9117
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9118
+ message: "Security-sensitive variable falls back to a hardcoded default when env var is missing",
9119
+ remediation: "Throw an error if the env var is missing instead of falling back to a default. Use a startup validation check.",
9120
+ references: ["CWE-1188"]
9121
+ },
9122
+ {
9123
+ id: "SEC-DEF-002",
9124
+ name: "TLS/SSL Disabled by Default",
9125
+ category: "insecure-defaults",
9126
+ severity: "warning",
9127
+ confidence: "medium",
9128
+ patterns: [
9129
+ /(?:tls|ssl|https|secure)\s*(?:=|:)\s*(?:false|config\??\.\w+\s*(?:\?\?|&&|\|\|)\s*false)/i
9130
+ ],
9131
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
9132
+ message: "Security feature defaults to disabled; missing configuration degrades to insecure mode",
9133
+ remediation: "Default security features to enabled (true). Require explicit opt-out, not opt-in.",
9134
+ references: ["CWE-1188"]
9135
+ },
9136
+ {
9137
+ id: "SEC-DEF-003",
9138
+ name: "Swallowed Authentication/Authorization Error",
9139
+ category: "insecure-defaults",
9140
+ severity: "warning",
9141
+ confidence: "low",
9142
+ patterns: [
9143
+ // Matches single-line empty catch: catch(e) { } or catch(e) { // ignore }
9144
+ // Note: multi-line catch blocks are handled by AI review, not this rule
9145
+ /catch\s*\([^)]*\)\s*\{\s*(?:\/\/\s*(?:ignore|skip|noop|todo)\b.*)?\s*\}/
9146
+ ],
9147
+ fileGlob: "**/*auth*.{ts,js,mjs,cjs},**/*session*.{ts,js,mjs,cjs},**/*token*.{ts,js,mjs,cjs}",
9148
+ 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.",
9149
+ remediation: "Re-throw the error or return an explicit denial. Never silently swallow auth errors.",
9150
+ references: ["CWE-754", "CWE-390"]
9151
+ },
9152
+ {
9153
+ id: "SEC-DEF-004",
9154
+ name: "Permissive CORS Fallback",
9155
+ category: "insecure-defaults",
9156
+ severity: "warning",
9157
+ confidence: "medium",
9158
+ patterns: [
9159
+ /(?:origin|cors)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*['"]\*/
9160
+ ],
9161
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9162
+ message: "CORS origin falls back to wildcard (*) when configuration is missing",
9163
+ remediation: "Default to a restrictive origin list. Require explicit configuration for permissive CORS.",
9164
+ references: ["CWE-942"]
9165
+ },
9166
+ {
9167
+ id: "SEC-DEF-005",
9168
+ name: "Rate Limiting Disabled by Default",
9169
+ category: "insecure-defaults",
9170
+ severity: "info",
9171
+ confidence: "low",
9172
+ patterns: [
9173
+ /(?:rateLimit|rateLimiting|throttle)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*(?:false|0|null|undefined)/i
9174
+ ],
9175
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9176
+ message: "Rate limiting defaults to disabled when configuration is missing",
9177
+ remediation: "Default to a sensible rate limit. Require explicit opt-out for disabling.",
9178
+ references: ["CWE-770"]
9179
+ }
9180
+ ];
9181
+
9182
+ // src/security/rules/sharp-edges.ts
9183
+ var sharpEdgesRules = [
9184
+ // --- Deprecated Crypto APIs ---
9185
+ {
9186
+ id: "SEC-EDGE-001",
9187
+ name: "Deprecated createCipher API",
9188
+ category: "sharp-edges",
9189
+ severity: "error",
9190
+ confidence: "high",
9191
+ patterns: [/crypto\.createCipher\s*\(/],
9192
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9193
+ message: "crypto.createCipher is deprecated \u2014 uses weak key derivation (no IV)",
9194
+ remediation: "Use crypto.createCipheriv with a random IV and proper key derivation (scrypt/pbkdf2)",
9195
+ references: ["CWE-327"]
9196
+ },
9197
+ {
9198
+ id: "SEC-EDGE-002",
9199
+ name: "Deprecated createDecipher API",
9200
+ category: "sharp-edges",
9201
+ severity: "error",
9202
+ confidence: "high",
9203
+ patterns: [/crypto\.createDecipher\s*\(/],
9204
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9205
+ message: "crypto.createDecipher is deprecated \u2014 uses weak key derivation (no IV)",
9206
+ remediation: "Use crypto.createDecipheriv with the same IV used for encryption",
9207
+ references: ["CWE-327"]
9208
+ },
9209
+ {
9210
+ id: "SEC-EDGE-003",
9211
+ name: "ECB Mode Selection",
9212
+ category: "sharp-edges",
9213
+ severity: "warning",
9214
+ confidence: "high",
9215
+ patterns: [/-ecb['"]/],
9216
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
9217
+ message: "ECB mode does not provide semantic security \u2014 identical plaintext blocks produce identical ciphertext",
9218
+ remediation: "Use CBC, CTR, or GCM mode instead of ECB",
9219
+ references: ["CWE-327"]
9220
+ },
9221
+ // --- Unsafe Deserialization ---
9222
+ {
9223
+ id: "SEC-EDGE-004",
9224
+ name: "yaml.load Without Safe Loader",
9225
+ category: "sharp-edges",
9226
+ severity: "error",
9227
+ confidence: "high",
9228
+ patterns: [
9229
+ /yaml\.load\s*\(/
9230
+ // Python: yaml.load() without SafeLoader
9231
+ ],
9232
+ fileGlob: "**/*.py",
9233
+ message: "yaml.load() executes arbitrary Python objects \u2014 use yaml.safe_load() instead",
9234
+ 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",
9235
+ references: ["CWE-502"]
9236
+ },
9237
+ {
9238
+ id: "SEC-EDGE-005",
9239
+ name: "Pickle/Marshal Deserialization",
9240
+ category: "sharp-edges",
9241
+ severity: "error",
9242
+ confidence: "high",
9243
+ patterns: [/pickle\.loads?\s*\(/, /marshal\.loads?\s*\(/],
9244
+ fileGlob: "**/*.py",
9245
+ message: "pickle/marshal deserialization executes arbitrary code \u2014 never use on untrusted data",
9246
+ remediation: "Use JSON, MessagePack, or Protocol Buffers for untrusted data serialization",
9247
+ references: ["CWE-502"]
9248
+ },
9249
+ // --- TOCTOU (Time-of-Check to Time-of-Use) ---
9250
+ {
9251
+ id: "SEC-EDGE-006",
9252
+ name: "Check-Then-Act File Operation",
9253
+ category: "sharp-edges",
9254
+ severity: "warning",
9255
+ confidence: "medium",
9256
+ // Patterns use .{0,N} since scanner matches single lines only (no multiline mode)
9257
+ patterns: [
9258
+ /(?:existsSync|accessSync|statSync)\s*\([^)]+\).{0,50}(?:readFileSync|writeFileSync|unlinkSync|mkdirSync)\s*\(/
9259
+ ],
9260
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9261
+ message: "Check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
9262
+ remediation: "Use the operation directly and handle ENOENT/EEXIST errors instead of checking first",
9263
+ references: ["CWE-367"]
9264
+ },
9265
+ {
9266
+ id: "SEC-EDGE-007",
9267
+ name: "Check-Then-Act File Operation (Async)",
9268
+ category: "sharp-edges",
9269
+ severity: "warning",
9270
+ confidence: "medium",
9271
+ // Uses .{0,N} since scanner matches single lines only (no multiline mode)
9272
+ patterns: [/(?:access|stat)\s*\([^)]+\).{0,80}(?:readFile|writeFile|unlink|mkdir)\s*\(/],
9273
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9274
+ message: "Async check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
9275
+ remediation: "Use the operation directly with try/catch instead of checking existence first",
9276
+ references: ["CWE-367"]
9277
+ },
9278
+ // --- Stringly-Typed Security ---
9279
+ {
9280
+ id: "SEC-EDGE-008",
9281
+ name: 'JWT Algorithm "none"',
9282
+ category: "sharp-edges",
9283
+ severity: "error",
9284
+ confidence: "high",
9285
+ patterns: [
9286
+ /algorithm[s]?\s*[:=]\s*\[?\s*['"]none['"]/i,
9287
+ /alg(?:orithm)?\s*[:=]\s*['"]none['"]/i
9288
+ ],
9289
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9290
+ message: 'JWT "none" algorithm disables signature verification entirely',
9291
+ remediation: 'Specify an explicit algorithm (e.g., "HS256", "RS256") and set algorithms allowlist in verify options',
9292
+ references: ["CWE-345"]
9293
+ },
9294
+ {
9295
+ id: "SEC-EDGE-009",
9296
+ name: "DES/RC4 Algorithm Selection",
9297
+ category: "sharp-edges",
9298
+ severity: "error",
9299
+ confidence: "high",
9300
+ patterns: [/['"]\s*(?:des|des-ede|des-ede3|des3|rc4|rc2|blowfish)\s*['"]/i],
9301
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
9302
+ message: "Weak/deprecated cipher algorithm selected \u2014 DES, RC4, RC2, and Blowfish are broken or deprecated",
9303
+ remediation: "Use AES-256-GCM or ChaCha20-Poly1305",
9304
+ references: ["CWE-327"]
9305
+ }
9306
+ ];
9307
+
9077
9308
  // src/security/rules/stack/node.ts
9078
9309
  var nodeRules = [
9079
9310
  {
@@ -9187,6 +9418,14 @@ var goRules = [
9187
9418
  ];
9188
9419
 
9189
9420
  // src/security/scanner.ts
9421
+ function parseHarnessIgnore(line, ruleId) {
9422
+ if (!line.includes("harness-ignore")) return null;
9423
+ if (!line.includes(ruleId)) return null;
9424
+ const match = line.match(/(?:\/\/|#)\s*harness-ignore\s+(SEC-[A-Z]+-\d+)(?::\s*(.+))?/);
9425
+ if (match?.[1] !== ruleId) return null;
9426
+ const text = match[2]?.trim();
9427
+ return { ruleId, justification: text || null };
9428
+ }
9190
9429
  var SecurityScanner = class {
9191
9430
  registry;
9192
9431
  config;
@@ -9203,7 +9442,9 @@ var SecurityScanner = class {
9203
9442
  ...networkRules,
9204
9443
  ...deserializationRules,
9205
9444
  ...agentConfigRules,
9206
- ...mcpRules
9445
+ ...mcpRules,
9446
+ ...insecureDefaultsRules,
9447
+ ...sharpEdgesRules
9207
9448
  ]);
9208
9449
  this.registry.registerAll([...nodeRules, ...expressRules, ...reactRules, ...goRules]);
9209
9450
  this.activeRules = this.registry.getAll();
@@ -9220,42 +9461,8 @@ var SecurityScanner = class {
9220
9461
  */
9221
9462
  scanContent(content, filePath, startLine = 1) {
9222
9463
  if (!this.config.enabled) return [];
9223
- const findings = [];
9224
9464
  const lines = content.split("\n");
9225
- for (const rule of this.activeRules) {
9226
- const resolved = resolveRuleSeverity(
9227
- rule.id,
9228
- rule.severity,
9229
- this.config.rules ?? {},
9230
- this.config.strict
9231
- );
9232
- if (resolved === "off") continue;
9233
- for (let i = 0; i < lines.length; i++) {
9234
- const line = lines[i] ?? "";
9235
- if (line.includes("harness-ignore") && line.includes(rule.id)) continue;
9236
- for (const pattern of rule.patterns) {
9237
- pattern.lastIndex = 0;
9238
- if (pattern.test(line)) {
9239
- findings.push({
9240
- ruleId: rule.id,
9241
- ruleName: rule.name,
9242
- category: rule.category,
9243
- severity: resolved,
9244
- confidence: rule.confidence,
9245
- file: filePath,
9246
- line: startLine + i,
9247
- match: line.trim(),
9248
- context: line,
9249
- message: rule.message,
9250
- remediation: rule.remediation,
9251
- ...rule.references ? { references: rule.references } : {}
9252
- });
9253
- break;
9254
- }
9255
- }
9256
- }
9257
- }
9258
- return findings;
9465
+ return this.scanLinesWithRules(lines, this.activeRules, filePath, startLine);
9259
9466
  }
9260
9467
  async scanFile(filePath) {
9261
9468
  if (!this.config.enabled) return [];
@@ -9264,14 +9471,22 @@ var SecurityScanner = class {
9264
9471
  }
9265
9472
  scanContentForFile(content, filePath, startLine = 1) {
9266
9473
  if (!this.config.enabled) return [];
9267
- const findings = [];
9268
9474
  const lines = content.split("\n");
9269
9475
  const applicableRules = this.activeRules.filter((rule) => {
9270
9476
  if (!rule.fileGlob) return true;
9271
9477
  const globs = rule.fileGlob.split(",").map((g) => g.trim());
9272
9478
  return globs.some((glob2) => (0, import_minimatch5.minimatch)(filePath, glob2, { dot: true }));
9273
9479
  });
9274
- for (const rule of applicableRules) {
9480
+ return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
9481
+ }
9482
+ /**
9483
+ * Core scanning loop shared by scanContent and scanContentForFile.
9484
+ * Evaluates each rule against each line, handling suppression (FP gate)
9485
+ * and pattern matching uniformly.
9486
+ */
9487
+ scanLinesWithRules(lines, rules, filePath, startLine) {
9488
+ const findings = [];
9489
+ for (const rule of rules) {
9275
9490
  const resolved = resolveRuleSeverity(
9276
9491
  rule.id,
9277
9492
  rule.severity,
@@ -9281,7 +9496,25 @@ var SecurityScanner = class {
9281
9496
  if (resolved === "off") continue;
9282
9497
  for (let i = 0; i < lines.length; i++) {
9283
9498
  const line = lines[i] ?? "";
9284
- if (line.includes("harness-ignore") && line.includes(rule.id)) continue;
9499
+ const suppressionMatch = parseHarnessIgnore(line, rule.id);
9500
+ if (suppressionMatch) {
9501
+ if (!suppressionMatch.justification) {
9502
+ findings.push({
9503
+ ruleId: rule.id,
9504
+ ruleName: rule.name,
9505
+ category: rule.category,
9506
+ severity: this.config.strict ? "error" : "warning",
9507
+ confidence: "high",
9508
+ file: filePath,
9509
+ line: startLine + i,
9510
+ match: line.trim(),
9511
+ context: line,
9512
+ message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
9513
+ remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
9514
+ });
9515
+ }
9516
+ continue;
9517
+ }
9285
9518
  for (const pattern of rule.patterns) {
9286
9519
  pattern.lastIndex = 0;
9287
9520
  if (pattern.test(line)) {
@@ -9327,6 +9560,414 @@ var SecurityScanner = class {
9327
9560
  }
9328
9561
  };
9329
9562
 
9563
+ // src/security/injection-patterns.ts
9564
+ var hiddenUnicodePatterns = [
9565
+ {
9566
+ ruleId: "INJ-UNI-001",
9567
+ severity: "high",
9568
+ category: "hidden-unicode",
9569
+ description: "Zero-width characters that can hide malicious instructions",
9570
+ // eslint-disable-next-line no-misleading-character-class -- intentional: regex detects zero-width chars for security scanning
9571
+ pattern: /[\u200B\u200C\u200D\uFEFF\u2060]/
9572
+ },
9573
+ {
9574
+ ruleId: "INJ-UNI-002",
9575
+ severity: "high",
9576
+ category: "hidden-unicode",
9577
+ description: "RTL/LTR override characters that can disguise text direction",
9578
+ pattern: /[\u202A-\u202E\u2066-\u2069]/
9579
+ }
9580
+ ];
9581
+ var reRolingPatterns = [
9582
+ {
9583
+ ruleId: "INJ-REROL-001",
9584
+ severity: "high",
9585
+ category: "explicit-re-roling",
9586
+ description: "Instruction to ignore/disregard/forget previous instructions",
9587
+ pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i
9588
+ },
9589
+ {
9590
+ ruleId: "INJ-REROL-002",
9591
+ severity: "high",
9592
+ category: "explicit-re-roling",
9593
+ description: "Attempt to reassign the AI role",
9594
+ 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
9595
+ },
9596
+ {
9597
+ ruleId: "INJ-REROL-003",
9598
+ severity: "high",
9599
+ category: "explicit-re-roling",
9600
+ description: "Direct instruction override attempt",
9601
+ pattern: /(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i
9602
+ }
9603
+ ];
9604
+ var permissionEscalationPatterns = [
9605
+ {
9606
+ ruleId: "INJ-PERM-001",
9607
+ severity: "high",
9608
+ category: "permission-escalation",
9609
+ description: "Attempt to enable all tools or grant unrestricted access",
9610
+ pattern: /(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i
9611
+ },
9612
+ {
9613
+ ruleId: "INJ-PERM-002",
9614
+ severity: "high",
9615
+ category: "permission-escalation",
9616
+ description: "Attempt to disable safety or security features",
9617
+ pattern: /(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i
9618
+ },
9619
+ {
9620
+ ruleId: "INJ-PERM-003",
9621
+ severity: "high",
9622
+ category: "permission-escalation",
9623
+ description: "Auto-approve directive that bypasses human review",
9624
+ pattern: /(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i
9625
+ }
9626
+ ];
9627
+ var encodedPayloadPatterns = [
9628
+ {
9629
+ ruleId: "INJ-ENC-001",
9630
+ severity: "high",
9631
+ category: "encoded-payloads",
9632
+ description: "Base64-encoded string long enough to contain instructions (>=28 chars)",
9633
+ // Match base64 strings of 28+ chars (7+ groups of 4).
9634
+ // Excludes JWT tokens (eyJ prefix) and Bearer-prefixed tokens.
9635
+ 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/])/
9636
+ },
9637
+ {
9638
+ ruleId: "INJ-ENC-002",
9639
+ severity: "high",
9640
+ category: "encoded-payloads",
9641
+ description: "Hex-encoded string long enough to contain directives (>=20 hex chars)",
9642
+ // Excludes hash-prefixed hex (sha256:, sha512:, md5:, etc.) and hex preceded by 0x.
9643
+ // Note: 40-char git SHA hashes (e.g. in `git log` output) may match — downstream
9644
+ // callers should filter matches of exactly 40 hex chars if scanning git output.
9645
+ pattern: /(?<![:x])(?<![A-Fa-f0-9])(?:[0-9a-fA-F]{2}){10,}(?![A-Fa-f0-9])/
9646
+ }
9647
+ ];
9648
+ var indirectInjectionPatterns = [
9649
+ {
9650
+ ruleId: "INJ-IND-001",
9651
+ severity: "medium",
9652
+ category: "indirect-injection",
9653
+ description: "Instruction to influence future responses",
9654
+ 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
9655
+ },
9656
+ {
9657
+ ruleId: "INJ-IND-002",
9658
+ severity: "medium",
9659
+ category: "indirect-injection",
9660
+ description: "Directive to include content in responses",
9661
+ pattern: /(?:include|insert|add|embed|put)\s+(?:this|the\s+following)\s+(?:in|into|to)\s+(?:your|the)\s+(?:response|output|reply|answer)/i
9662
+ },
9663
+ {
9664
+ ruleId: "INJ-IND-003",
9665
+ severity: "medium",
9666
+ category: "indirect-injection",
9667
+ description: "Standing instruction to always respond a certain way",
9668
+ pattern: /always\s+(?:respond|reply|answer|say|output)\s+(?:with|that|by)/i
9669
+ }
9670
+ ];
9671
+ var contextManipulationPatterns = [
9672
+ {
9673
+ ruleId: "INJ-CTX-001",
9674
+ severity: "medium",
9675
+ category: "context-manipulation",
9676
+ description: "Claim about system prompt content",
9677
+ pattern: /(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i
9678
+ },
9679
+ {
9680
+ ruleId: "INJ-CTX-002",
9681
+ severity: "medium",
9682
+ category: "context-manipulation",
9683
+ description: "Claim about AI instructions",
9684
+ pattern: /your\s+(?:instructions?|directives?|guidelines?|rules?)\s+(?:are|say|tell|state)/i
9685
+ },
9686
+ {
9687
+ ruleId: "INJ-CTX-003",
9688
+ severity: "medium",
9689
+ category: "context-manipulation",
9690
+ description: "Fake XML/HTML system or instruction tags",
9691
+ // Case-sensitive: only match lowercase tags to avoid false positives on
9692
+ // React components like <User>, <Context>, <Role> etc.
9693
+ pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/
9694
+ },
9695
+ {
9696
+ ruleId: "INJ-CTX-004",
9697
+ severity: "medium",
9698
+ category: "context-manipulation",
9699
+ description: "Fake JSON role assignment mimicking chat format",
9700
+ pattern: /[{,]\s*"role"\s*:\s*"(?:system|assistant|function)"/i
9701
+ }
9702
+ ];
9703
+ var socialEngineeringPatterns = [
9704
+ {
9705
+ ruleId: "INJ-SOC-001",
9706
+ severity: "medium",
9707
+ category: "social-engineering",
9708
+ description: "Urgency pressure to bypass checks",
9709
+ pattern: /(?:this\s+is\s+(?:very\s+)?urgent|this\s+is\s+(?:an?\s+)?emergency|do\s+(?:this|it)\s+(?:now|immediately))\b/i
9710
+ },
9711
+ {
9712
+ ruleId: "INJ-SOC-002",
9713
+ severity: "medium",
9714
+ category: "social-engineering",
9715
+ description: "False authority claim",
9716
+ pattern: /(?:the\s+)?(?:admin|administrator|manager|CEO|CTO|owner|supervisor)\s+(?:authorized|approved|said|told|confirmed|requested)/i
9717
+ },
9718
+ {
9719
+ ruleId: "INJ-SOC-003",
9720
+ severity: "medium",
9721
+ category: "social-engineering",
9722
+ description: "Testing pretext to bypass safety",
9723
+ pattern: /(?:for\s+testing\s+purposes?|this\s+is\s+(?:just\s+)?a\s+test|in\s+test\s+mode)\b/i
9724
+ }
9725
+ ];
9726
+ var suspiciousPatterns = [
9727
+ {
9728
+ ruleId: "INJ-SUS-001",
9729
+ severity: "low",
9730
+ category: "suspicious-patterns",
9731
+ description: "Excessive consecutive whitespace (>10 chars) mid-line that may hide content",
9732
+ // Only match whitespace runs not at the start of a line (indentation is normal)
9733
+ pattern: /\S\s{11,}/
9734
+ },
9735
+ {
9736
+ ruleId: "INJ-SUS-002",
9737
+ severity: "low",
9738
+ category: "suspicious-patterns",
9739
+ description: "Repeated delimiters (>5) that may indicate obfuscation",
9740
+ pattern: /([|;,=\-_~`])\1{5,}/
9741
+ },
9742
+ {
9743
+ ruleId: "INJ-SUS-003",
9744
+ severity: "low",
9745
+ category: "suspicious-patterns",
9746
+ description: "Mathematical alphanumeric symbols used as Latin character substitutes",
9747
+ // Mathematical bold/italic/script Unicode ranges (U+1D400-U+1D7FF)
9748
+ pattern: /[\uD835][\uDC00-\uDFFF]/
9749
+ }
9750
+ ];
9751
+ var ALL_PATTERNS = [
9752
+ ...hiddenUnicodePatterns,
9753
+ ...reRolingPatterns,
9754
+ ...permissionEscalationPatterns,
9755
+ ...encodedPayloadPatterns,
9756
+ ...indirectInjectionPatterns,
9757
+ ...contextManipulationPatterns,
9758
+ ...socialEngineeringPatterns,
9759
+ ...suspiciousPatterns
9760
+ ];
9761
+ function scanForInjection(text) {
9762
+ const findings = [];
9763
+ const lines = text.split("\n");
9764
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
9765
+ const line = lines[lineIdx];
9766
+ for (const rule of ALL_PATTERNS) {
9767
+ if (rule.pattern.test(line)) {
9768
+ findings.push({
9769
+ severity: rule.severity,
9770
+ ruleId: rule.ruleId,
9771
+ match: line.trim(),
9772
+ line: lineIdx + 1
9773
+ });
9774
+ }
9775
+ }
9776
+ }
9777
+ const severityOrder = {
9778
+ high: 0,
9779
+ medium: 1,
9780
+ low: 2
9781
+ };
9782
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
9783
+ return findings;
9784
+ }
9785
+ function getInjectionPatterns() {
9786
+ return ALL_PATTERNS;
9787
+ }
9788
+ var DESTRUCTIVE_BASH = [
9789
+ /\bgit\s+push\b/,
9790
+ /\bgit\s+commit\b/,
9791
+ /\brm\s+-rf?\b/,
9792
+ /\brm\s+-r\b/
9793
+ ];
9794
+
9795
+ // src/security/taint.ts
9796
+ var import_node_fs4 = require("fs");
9797
+ var import_node_path7 = require("path");
9798
+ var TAINT_DURATION_MS = 30 * 60 * 1e3;
9799
+ var DEFAULT_SESSION_ID = "default";
9800
+ function getTaintFilePath(projectRoot, sessionId) {
9801
+ const id = sessionId || DEFAULT_SESSION_ID;
9802
+ return (0, import_node_path7.join)(projectRoot, ".harness", `session-taint-${id}.json`);
9803
+ }
9804
+ function readTaint(projectRoot, sessionId) {
9805
+ const filePath = getTaintFilePath(projectRoot, sessionId);
9806
+ let content;
9807
+ try {
9808
+ content = (0, import_node_fs4.readFileSync)(filePath, "utf8");
9809
+ } catch {
9810
+ return null;
9811
+ }
9812
+ let state;
9813
+ try {
9814
+ state = JSON.parse(content);
9815
+ } catch {
9816
+ try {
9817
+ (0, import_node_fs4.unlinkSync)(filePath);
9818
+ } catch {
9819
+ }
9820
+ return null;
9821
+ }
9822
+ if (!state.sessionId || !state.taintedAt || !state.expiresAt || !state.findings) {
9823
+ try {
9824
+ (0, import_node_fs4.unlinkSync)(filePath);
9825
+ } catch {
9826
+ }
9827
+ return null;
9828
+ }
9829
+ return state;
9830
+ }
9831
+ function checkTaint(projectRoot, sessionId) {
9832
+ const state = readTaint(projectRoot, sessionId);
9833
+ if (!state) {
9834
+ return { tainted: false, expired: false, state: null };
9835
+ }
9836
+ const now = /* @__PURE__ */ new Date();
9837
+ const expiresAt = new Date(state.expiresAt);
9838
+ if (now >= expiresAt) {
9839
+ const filePath = getTaintFilePath(projectRoot, sessionId);
9840
+ try {
9841
+ (0, import_node_fs4.unlinkSync)(filePath);
9842
+ } catch {
9843
+ }
9844
+ return { tainted: false, expired: true, state };
9845
+ }
9846
+ return { tainted: true, expired: false, state };
9847
+ }
9848
+ function writeTaint(projectRoot, sessionId, reason, findings, source) {
9849
+ const id = sessionId || DEFAULT_SESSION_ID;
9850
+ const filePath = getTaintFilePath(projectRoot, id);
9851
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9852
+ const dir = (0, import_node_path7.dirname)(filePath);
9853
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
9854
+ const existing = readTaint(projectRoot, id);
9855
+ const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
9856
+ const taintFindings = findings.map((f) => ({
9857
+ ruleId: f.ruleId,
9858
+ severity: f.severity,
9859
+ match: f.match,
9860
+ source,
9861
+ detectedAt: now
9862
+ }));
9863
+ const state = {
9864
+ sessionId: id,
9865
+ taintedAt: existing?.taintedAt || now,
9866
+ expiresAt: new Date(Date.now() + TAINT_DURATION_MS).toISOString(),
9867
+ reason,
9868
+ severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
9869
+ findings: [...existing?.findings || [], ...taintFindings]
9870
+ };
9871
+ (0, import_node_fs4.writeFileSync)(filePath, JSON.stringify(state, null, 2) + "\n");
9872
+ return state;
9873
+ }
9874
+ function clearTaint(projectRoot, sessionId) {
9875
+ if (sessionId) {
9876
+ const filePath = getTaintFilePath(projectRoot, sessionId);
9877
+ try {
9878
+ (0, import_node_fs4.unlinkSync)(filePath);
9879
+ return 1;
9880
+ } catch {
9881
+ return 0;
9882
+ }
9883
+ }
9884
+ const harnessDir = (0, import_node_path7.join)(projectRoot, ".harness");
9885
+ let count = 0;
9886
+ try {
9887
+ const files = (0, import_node_fs4.readdirSync)(harnessDir);
9888
+ for (const file of files) {
9889
+ if (file.startsWith("session-taint-") && file.endsWith(".json")) {
9890
+ try {
9891
+ (0, import_node_fs4.unlinkSync)((0, import_node_path7.join)(harnessDir, file));
9892
+ count++;
9893
+ } catch {
9894
+ }
9895
+ }
9896
+ }
9897
+ } catch {
9898
+ }
9899
+ return count;
9900
+ }
9901
+ function listTaintedSessions(projectRoot) {
9902
+ const harnessDir = (0, import_node_path7.join)(projectRoot, ".harness");
9903
+ const sessions = [];
9904
+ try {
9905
+ const files = (0, import_node_fs4.readdirSync)(harnessDir);
9906
+ for (const file of files) {
9907
+ if (file.startsWith("session-taint-") && file.endsWith(".json")) {
9908
+ const sessionId = file.replace("session-taint-", "").replace(".json", "");
9909
+ const result = checkTaint(projectRoot, sessionId);
9910
+ if (result.tainted) {
9911
+ sessions.push(sessionId);
9912
+ }
9913
+ }
9914
+ }
9915
+ } catch {
9916
+ }
9917
+ return sessions;
9918
+ }
9919
+
9920
+ // src/security/scan-config-shared.ts
9921
+ function mapSecuritySeverity(severity) {
9922
+ if (severity === "error") return "high";
9923
+ if (severity === "warning") return "medium";
9924
+ return "low";
9925
+ }
9926
+ function computeOverallSeverity(findings) {
9927
+ if (findings.length === 0) return "clean";
9928
+ if (findings.some((f) => f.severity === "high")) return "high";
9929
+ if (findings.some((f) => f.severity === "medium")) return "medium";
9930
+ return "low";
9931
+ }
9932
+ function computeScanExitCode(results) {
9933
+ for (const r of results) {
9934
+ if (r.overallSeverity === "high") return 2;
9935
+ }
9936
+ for (const r of results) {
9937
+ if (r.overallSeverity === "medium") return 1;
9938
+ }
9939
+ return 0;
9940
+ }
9941
+ function mapInjectionFindings(injectionFindings) {
9942
+ return injectionFindings.map((f) => ({
9943
+ ruleId: f.ruleId,
9944
+ severity: f.severity,
9945
+ message: `Injection pattern detected: ${f.ruleId}`,
9946
+ match: f.match,
9947
+ ...f.line !== void 0 ? { line: f.line } : {}
9948
+ }));
9949
+ }
9950
+ function isDuplicateFinding(existing, secFinding) {
9951
+ return existing.some(
9952
+ (e) => e.line === secFinding.line && e.match === secFinding.match.trim() && e.ruleId.split("-")[0] === secFinding.ruleId.split("-")[0]
9953
+ );
9954
+ }
9955
+ function mapSecurityFindings(secFindings, existing) {
9956
+ const result = [];
9957
+ for (const f of secFindings) {
9958
+ if (!isDuplicateFinding(existing, f)) {
9959
+ result.push({
9960
+ ruleId: f.ruleId,
9961
+ severity: mapSecuritySeverity(f.severity),
9962
+ message: f.message,
9963
+ match: f.match,
9964
+ ...f.line !== void 0 ? { line: f.line } : {}
9965
+ });
9966
+ }
9967
+ }
9968
+ return result;
9969
+ }
9970
+
9330
9971
  // src/ci/check-orchestrator.ts
9331
9972
  var path15 = __toESM(require("path"));
9332
9973
  var ALL_CHECKS = [
@@ -9510,7 +10151,7 @@ async function runPerfCheck(projectRoot, config) {
9510
10151
  if (perfReport.complexity) {
9511
10152
  for (const v of perfReport.complexity.violations) {
9512
10153
  issues.push({
9513
- severity: v.severity === "info" ? "warning" : v.severity,
10154
+ severity: "warning",
9514
10155
  message: `[Tier ${v.tier}] ${v.metric}: ${v.function} in ${v.file} (${v.value} > ${v.threshold})`,
9515
10156
  file: v.file,
9516
10157
  line: v.line
@@ -12106,9 +12747,9 @@ async function resolveWasmPath(grammarName) {
12106
12747
  const { createRequire } = await import("module");
12107
12748
  const require2 = createRequire(import_meta.url ?? __filename);
12108
12749
  const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
12109
- const path23 = await import("path");
12110
- const pkgDir = path23.dirname(pkgPath);
12111
- return path23.join(pkgDir, "out", `${grammarName}.wasm`);
12750
+ const path26 = await import("path");
12751
+ const pkgDir = path26.dirname(pkgPath);
12752
+ return path26.join(pkgDir, "out", `${grammarName}.wasm`);
12112
12753
  }
12113
12754
  async function loadLanguage(lang) {
12114
12755
  const grammarName = GRAMMAR_MAP[lang];
@@ -12472,6 +13113,489 @@ async function unfoldRange(filePath, startLine, endLine) {
12472
13113
  };
12473
13114
  }
12474
13115
 
13116
+ // src/pricing/pricing.ts
13117
+ var TOKENS_PER_MILLION = 1e6;
13118
+ function parseLiteLLMData(raw) {
13119
+ const dataset = /* @__PURE__ */ new Map();
13120
+ for (const [modelName, entry] of Object.entries(raw)) {
13121
+ if (modelName === "sample_spec") continue;
13122
+ if (entry.mode && entry.mode !== "chat") continue;
13123
+ const inputCost = entry.input_cost_per_token;
13124
+ const outputCost = entry.output_cost_per_token;
13125
+ if (inputCost == null || outputCost == null) continue;
13126
+ const pricing = {
13127
+ inputPer1M: inputCost * TOKENS_PER_MILLION,
13128
+ outputPer1M: outputCost * TOKENS_PER_MILLION
13129
+ };
13130
+ if (entry.cache_read_input_token_cost != null) {
13131
+ pricing.cacheReadPer1M = entry.cache_read_input_token_cost * TOKENS_PER_MILLION;
13132
+ }
13133
+ if (entry.cache_creation_input_token_cost != null) {
13134
+ pricing.cacheWritePer1M = entry.cache_creation_input_token_cost * TOKENS_PER_MILLION;
13135
+ }
13136
+ dataset.set(modelName, pricing);
13137
+ }
13138
+ return dataset;
13139
+ }
13140
+ function getModelPrice(model, dataset) {
13141
+ if (!model) {
13142
+ console.warn("[harness pricing] No model specified \u2014 cannot look up pricing.");
13143
+ return null;
13144
+ }
13145
+ const pricing = dataset.get(model);
13146
+ if (!pricing) {
13147
+ console.warn(
13148
+ `[harness pricing] No pricing data for model "${model}". Consider updating pricing data.`
13149
+ );
13150
+ return null;
13151
+ }
13152
+ return pricing;
13153
+ }
13154
+
13155
+ // src/pricing/cache.ts
13156
+ var fs23 = __toESM(require("fs/promises"));
13157
+ var path23 = __toESM(require("path"));
13158
+
13159
+ // src/pricing/fallback.json
13160
+ var fallback_default = {
13161
+ _generatedAt: "2026-03-31",
13162
+ _source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
13163
+ models: {
13164
+ "claude-opus-4-20250514": {
13165
+ inputPer1M: 15,
13166
+ outputPer1M: 75,
13167
+ cacheReadPer1M: 1.5,
13168
+ cacheWritePer1M: 18.75
13169
+ },
13170
+ "claude-sonnet-4-20250514": {
13171
+ inputPer1M: 3,
13172
+ outputPer1M: 15,
13173
+ cacheReadPer1M: 0.3,
13174
+ cacheWritePer1M: 3.75
13175
+ },
13176
+ "claude-3-5-haiku-20241022": {
13177
+ inputPer1M: 0.8,
13178
+ outputPer1M: 4,
13179
+ cacheReadPer1M: 0.08,
13180
+ cacheWritePer1M: 1
13181
+ },
13182
+ "gpt-4o": {
13183
+ inputPer1M: 2.5,
13184
+ outputPer1M: 10,
13185
+ cacheReadPer1M: 1.25
13186
+ },
13187
+ "gpt-4o-mini": {
13188
+ inputPer1M: 0.15,
13189
+ outputPer1M: 0.6,
13190
+ cacheReadPer1M: 0.075
13191
+ },
13192
+ "gemini-2.0-flash": {
13193
+ inputPer1M: 0.1,
13194
+ outputPer1M: 0.4,
13195
+ cacheReadPer1M: 0.025
13196
+ },
13197
+ "gemini-2.5-pro": {
13198
+ inputPer1M: 1.25,
13199
+ outputPer1M: 10,
13200
+ cacheReadPer1M: 0.3125
13201
+ }
13202
+ }
13203
+ };
13204
+
13205
+ // src/pricing/cache.ts
13206
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
13207
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
13208
+ var STALENESS_WARNING_DAYS = 7;
13209
+ function getCachePath(projectRoot) {
13210
+ return path23.join(projectRoot, ".harness", "cache", "pricing.json");
13211
+ }
13212
+ function getStalenessMarkerPath(projectRoot) {
13213
+ return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
13214
+ }
13215
+ async function readDiskCache(projectRoot) {
13216
+ try {
13217
+ const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
13218
+ return JSON.parse(raw);
13219
+ } catch {
13220
+ return null;
13221
+ }
13222
+ }
13223
+ async function writeDiskCache(projectRoot, data) {
13224
+ const cachePath = getCachePath(projectRoot);
13225
+ await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
13226
+ await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
13227
+ }
13228
+ async function fetchFromNetwork() {
13229
+ try {
13230
+ const response = await fetch(LITELLM_PRICING_URL);
13231
+ if (!response.ok) return null;
13232
+ const data = await response.json();
13233
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
13234
+ return {
13235
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
13236
+ data
13237
+ };
13238
+ } catch {
13239
+ return null;
13240
+ }
13241
+ }
13242
+ function loadFallbackDataset() {
13243
+ const fb = fallback_default;
13244
+ const dataset = /* @__PURE__ */ new Map();
13245
+ for (const [model, pricing] of Object.entries(fb.models)) {
13246
+ dataset.set(model, pricing);
13247
+ }
13248
+ return dataset;
13249
+ }
13250
+ async function checkAndWarnStaleness(projectRoot) {
13251
+ const markerPath = getStalenessMarkerPath(projectRoot);
13252
+ try {
13253
+ const raw = await fs23.readFile(markerPath, "utf-8");
13254
+ const marker = JSON.parse(raw);
13255
+ const firstUse = new Date(marker.firstFallbackUse).getTime();
13256
+ const now = Date.now();
13257
+ const daysSinceFirstUse = (now - firstUse) / (24 * 60 * 60 * 1e3);
13258
+ if (daysSinceFirstUse > STALENESS_WARNING_DAYS) {
13259
+ console.warn(
13260
+ `[harness pricing] Pricing data is stale \u2014 using bundled fallback for ${Math.floor(daysSinceFirstUse)} days. Connect to the internet to refresh pricing data.`
13261
+ );
13262
+ }
13263
+ } catch {
13264
+ try {
13265
+ await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
13266
+ await fs23.writeFile(
13267
+ markerPath,
13268
+ JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
13269
+ );
13270
+ } catch {
13271
+ }
13272
+ }
13273
+ }
13274
+ async function clearStalenessMarker(projectRoot) {
13275
+ try {
13276
+ await fs23.unlink(getStalenessMarkerPath(projectRoot));
13277
+ } catch {
13278
+ }
13279
+ }
13280
+ async function loadPricingData(projectRoot) {
13281
+ const cache = await readDiskCache(projectRoot);
13282
+ if (cache) {
13283
+ const cacheAge = Date.now() - new Date(cache.fetchedAt).getTime();
13284
+ if (cacheAge < CACHE_TTL_MS) {
13285
+ await clearStalenessMarker(projectRoot);
13286
+ return parseLiteLLMData(cache.data);
13287
+ }
13288
+ }
13289
+ const fetched = await fetchFromNetwork();
13290
+ if (fetched) {
13291
+ await writeDiskCache(projectRoot, fetched);
13292
+ await clearStalenessMarker(projectRoot);
13293
+ return parseLiteLLMData(fetched.data);
13294
+ }
13295
+ if (cache) {
13296
+ return parseLiteLLMData(cache.data);
13297
+ }
13298
+ await checkAndWarnStaleness(projectRoot);
13299
+ return loadFallbackDataset();
13300
+ }
13301
+
13302
+ // src/pricing/calculator.ts
13303
+ var MICRODOLLARS_PER_DOLLAR = 1e6;
13304
+ var TOKENS_PER_MILLION2 = 1e6;
13305
+ function calculateCost(record, dataset) {
13306
+ if (!record.model) return null;
13307
+ const pricing = getModelPrice(record.model, dataset);
13308
+ if (!pricing) return null;
13309
+ let costUSD = 0;
13310
+ costUSD += record.tokens.inputTokens / TOKENS_PER_MILLION2 * pricing.inputPer1M;
13311
+ costUSD += record.tokens.outputTokens / TOKENS_PER_MILLION2 * pricing.outputPer1M;
13312
+ if (record.cacheReadTokens != null && pricing.cacheReadPer1M != null) {
13313
+ costUSD += record.cacheReadTokens / TOKENS_PER_MILLION2 * pricing.cacheReadPer1M;
13314
+ }
13315
+ if (record.cacheCreationTokens != null && pricing.cacheWritePer1M != null) {
13316
+ costUSD += record.cacheCreationTokens / TOKENS_PER_MILLION2 * pricing.cacheWritePer1M;
13317
+ }
13318
+ return Math.round(costUSD * MICRODOLLARS_PER_DOLLAR);
13319
+ }
13320
+
13321
+ // src/usage/aggregator.ts
13322
+ function aggregateBySession(records) {
13323
+ if (records.length === 0) return [];
13324
+ const sessionMap = /* @__PURE__ */ new Map();
13325
+ for (const record of records) {
13326
+ const tagged = record;
13327
+ const id = record.sessionId;
13328
+ if (!sessionMap.has(id)) {
13329
+ sessionMap.set(id, { harnessRecords: [], ccRecords: [], allRecords: [] });
13330
+ }
13331
+ const bucket = sessionMap.get(id);
13332
+ if (tagged._source === "claude-code") {
13333
+ bucket.ccRecords.push(tagged);
13334
+ } else {
13335
+ bucket.harnessRecords.push(tagged);
13336
+ }
13337
+ bucket.allRecords.push(tagged);
13338
+ }
13339
+ const results = [];
13340
+ for (const [sessionId, bucket] of sessionMap) {
13341
+ const hasHarness = bucket.harnessRecords.length > 0;
13342
+ const hasCC = bucket.ccRecords.length > 0;
13343
+ const isMerged = hasHarness && hasCC;
13344
+ const tokenSource = hasHarness ? bucket.harnessRecords : bucket.ccRecords;
13345
+ const tokens = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
13346
+ let cacheCreation;
13347
+ let cacheRead;
13348
+ let costMicroUSD = 0;
13349
+ let model;
13350
+ for (const r of tokenSource) {
13351
+ tokens.inputTokens += r.tokens.inputTokens;
13352
+ tokens.outputTokens += r.tokens.outputTokens;
13353
+ tokens.totalTokens += r.tokens.totalTokens;
13354
+ if (r.cacheCreationTokens != null) {
13355
+ cacheCreation = (cacheCreation ?? 0) + r.cacheCreationTokens;
13356
+ }
13357
+ if (r.cacheReadTokens != null) {
13358
+ cacheRead = (cacheRead ?? 0) + r.cacheReadTokens;
13359
+ }
13360
+ if (r.costMicroUSD != null && costMicroUSD != null) {
13361
+ costMicroUSD += r.costMicroUSD;
13362
+ } else if (r.costMicroUSD == null) {
13363
+ costMicroUSD = null;
13364
+ }
13365
+ if (!model && r.model) {
13366
+ model = r.model;
13367
+ }
13368
+ }
13369
+ if (!model && hasCC) {
13370
+ for (const r of bucket.ccRecords) {
13371
+ if (r.model) {
13372
+ model = r.model;
13373
+ break;
13374
+ }
13375
+ }
13376
+ }
13377
+ const timestamps = bucket.allRecords.map((r) => r.timestamp).sort();
13378
+ const source = isMerged ? "merged" : hasCC ? "claude-code" : "harness";
13379
+ const session = {
13380
+ sessionId,
13381
+ firstTimestamp: timestamps[0] ?? "",
13382
+ lastTimestamp: timestamps[timestamps.length - 1] ?? "",
13383
+ tokens,
13384
+ costMicroUSD,
13385
+ source
13386
+ };
13387
+ if (model) session.model = model;
13388
+ if (cacheCreation != null) session.cacheCreationTokens = cacheCreation;
13389
+ if (cacheRead != null) session.cacheReadTokens = cacheRead;
13390
+ results.push(session);
13391
+ }
13392
+ results.sort((a, b) => b.firstTimestamp.localeCompare(a.firstTimestamp));
13393
+ return results;
13394
+ }
13395
+ function aggregateByDay(records) {
13396
+ if (records.length === 0) return [];
13397
+ const dayMap = /* @__PURE__ */ new Map();
13398
+ for (const record of records) {
13399
+ const date = record.timestamp.slice(0, 10);
13400
+ if (!dayMap.has(date)) {
13401
+ dayMap.set(date, {
13402
+ sessions: /* @__PURE__ */ new Set(),
13403
+ tokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
13404
+ costMicroUSD: 0,
13405
+ models: /* @__PURE__ */ new Set()
13406
+ });
13407
+ }
13408
+ const day = dayMap.get(date);
13409
+ day.sessions.add(record.sessionId);
13410
+ day.tokens.inputTokens += record.tokens.inputTokens;
13411
+ day.tokens.outputTokens += record.tokens.outputTokens;
13412
+ day.tokens.totalTokens += record.tokens.totalTokens;
13413
+ if (record.cacheCreationTokens != null) {
13414
+ day.cacheCreation = (day.cacheCreation ?? 0) + record.cacheCreationTokens;
13415
+ }
13416
+ if (record.cacheReadTokens != null) {
13417
+ day.cacheRead = (day.cacheRead ?? 0) + record.cacheReadTokens;
13418
+ }
13419
+ if (record.costMicroUSD != null && day.costMicroUSD != null) {
13420
+ day.costMicroUSD += record.costMicroUSD;
13421
+ } else if (record.costMicroUSD == null) {
13422
+ day.costMicroUSD = null;
13423
+ }
13424
+ if (record.model) {
13425
+ day.models.add(record.model);
13426
+ }
13427
+ }
13428
+ const results = [];
13429
+ for (const [date, day] of dayMap) {
13430
+ const entry = {
13431
+ date,
13432
+ sessionCount: day.sessions.size,
13433
+ tokens: day.tokens,
13434
+ costMicroUSD: day.costMicroUSD,
13435
+ models: Array.from(day.models).sort()
13436
+ };
13437
+ if (day.cacheCreation != null) entry.cacheCreationTokens = day.cacheCreation;
13438
+ if (day.cacheRead != null) entry.cacheReadTokens = day.cacheRead;
13439
+ results.push(entry);
13440
+ }
13441
+ results.sort((a, b) => b.date.localeCompare(a.date));
13442
+ return results;
13443
+ }
13444
+
13445
+ // src/usage/jsonl-reader.ts
13446
+ var fs24 = __toESM(require("fs"));
13447
+ var path24 = __toESM(require("path"));
13448
+ function parseLine(line, lineNumber) {
13449
+ let entry;
13450
+ try {
13451
+ entry = JSON.parse(line);
13452
+ } catch {
13453
+ console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
13454
+ return null;
13455
+ }
13456
+ const tokenUsage = entry.token_usage;
13457
+ if (!tokenUsage || typeof tokenUsage !== "object") {
13458
+ console.warn(
13459
+ `[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
13460
+ );
13461
+ return null;
13462
+ }
13463
+ const inputTokens = tokenUsage.input_tokens ?? 0;
13464
+ const outputTokens = tokenUsage.output_tokens ?? 0;
13465
+ const record = {
13466
+ sessionId: entry.session_id ?? "unknown",
13467
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
13468
+ tokens: {
13469
+ inputTokens,
13470
+ outputTokens,
13471
+ totalTokens: inputTokens + outputTokens
13472
+ }
13473
+ };
13474
+ if (entry.cache_creation_tokens != null) {
13475
+ record.cacheCreationTokens = entry.cache_creation_tokens;
13476
+ }
13477
+ if (entry.cache_read_tokens != null) {
13478
+ record.cacheReadTokens = entry.cache_read_tokens;
13479
+ }
13480
+ if (entry.model != null) {
13481
+ record.model = entry.model;
13482
+ }
13483
+ return record;
13484
+ }
13485
+ function readCostRecords(projectRoot) {
13486
+ const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
13487
+ let raw;
13488
+ try {
13489
+ raw = fs24.readFileSync(costsFile, "utf-8");
13490
+ } catch {
13491
+ return [];
13492
+ }
13493
+ const records = [];
13494
+ const lines = raw.split("\n");
13495
+ for (let i = 0; i < lines.length; i++) {
13496
+ const line = lines[i]?.trim();
13497
+ if (!line) continue;
13498
+ const record = parseLine(line, i + 1);
13499
+ if (record) {
13500
+ records.push(record);
13501
+ }
13502
+ }
13503
+ return records;
13504
+ }
13505
+
13506
+ // src/usage/cc-parser.ts
13507
+ var fs25 = __toESM(require("fs"));
13508
+ var path25 = __toESM(require("path"));
13509
+ var os2 = __toESM(require("os"));
13510
+ function extractUsage(entry) {
13511
+ if (entry.type !== "assistant") return null;
13512
+ const message = entry.message;
13513
+ if (!message || typeof message !== "object") return null;
13514
+ const usage = message.usage;
13515
+ return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
13516
+ }
13517
+ function buildRecord(entry, usage) {
13518
+ const inputTokens = Number(usage.input_tokens) || 0;
13519
+ const outputTokens = Number(usage.output_tokens) || 0;
13520
+ const message = entry.message;
13521
+ const record = {
13522
+ sessionId: entry.sessionId ?? "unknown",
13523
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
13524
+ tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
13525
+ _source: "claude-code"
13526
+ };
13527
+ const model = message.model;
13528
+ if (model) record.model = model;
13529
+ const cacheCreate = usage.cache_creation_input_tokens;
13530
+ const cacheRead = usage.cache_read_input_tokens;
13531
+ if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
13532
+ if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
13533
+ return record;
13534
+ }
13535
+ function parseCCLine(line, filePath, lineNumber) {
13536
+ let entry;
13537
+ try {
13538
+ entry = JSON.parse(line);
13539
+ } catch {
13540
+ console.warn(
13541
+ `[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
13542
+ );
13543
+ return null;
13544
+ }
13545
+ const usage = extractUsage(entry);
13546
+ if (!usage) return null;
13547
+ return {
13548
+ record: buildRecord(entry, usage),
13549
+ requestId: entry.requestId ?? null
13550
+ };
13551
+ }
13552
+ function readCCFile(filePath) {
13553
+ let raw;
13554
+ try {
13555
+ raw = fs25.readFileSync(filePath, "utf-8");
13556
+ } catch {
13557
+ return [];
13558
+ }
13559
+ const byRequestId = /* @__PURE__ */ new Map();
13560
+ const noRequestId = [];
13561
+ const lines = raw.split("\n");
13562
+ for (let i = 0; i < lines.length; i++) {
13563
+ const line = lines[i]?.trim();
13564
+ if (!line) continue;
13565
+ const parsed = parseCCLine(line, filePath, i + 1);
13566
+ if (!parsed) continue;
13567
+ if (parsed.requestId) {
13568
+ byRequestId.set(parsed.requestId, parsed.record);
13569
+ } else {
13570
+ noRequestId.push(parsed.record);
13571
+ }
13572
+ }
13573
+ return [...byRequestId.values(), ...noRequestId];
13574
+ }
13575
+ function parseCCRecords() {
13576
+ const homeDir = process.env.HOME ?? os2.homedir();
13577
+ const projectsDir = path25.join(homeDir, ".claude", "projects");
13578
+ let projectDirs;
13579
+ try {
13580
+ projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
13581
+ } catch {
13582
+ return [];
13583
+ }
13584
+ const records = [];
13585
+ for (const dir of projectDirs) {
13586
+ let files;
13587
+ try {
13588
+ files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
13589
+ } catch {
13590
+ continue;
13591
+ }
13592
+ for (const file of files) {
13593
+ records.push(...readCCFile(file));
13594
+ }
13595
+ }
13596
+ return records;
13597
+ }
13598
+
12475
13599
  // src/index.ts
12476
13600
  var VERSION = "0.15.0";
12477
13601
  // Annotate the CommonJS export names for ESM import in node:
@@ -12490,6 +13614,7 @@ var VERSION = "0.15.0";
12490
13614
  BlueprintGenerator,
12491
13615
  BundleConstraintsSchema,
12492
13616
  BundleSchema,
13617
+ CACHE_TTL_MS,
12493
13618
  COMPLIANCE_DESCRIPTOR,
12494
13619
  CategoryBaselineSchema,
12495
13620
  CategoryRegressionSchema,
@@ -12507,6 +13632,7 @@ var VERSION = "0.15.0";
12507
13632
  DEFAULT_SECURITY_CONFIG,
12508
13633
  DEFAULT_STATE,
12509
13634
  DEFAULT_STREAM_INDEX,
13635
+ DESTRUCTIVE_BASH,
12510
13636
  DepDepthCollector,
12511
13637
  EXTENSION_MAP,
12512
13638
  EmitInteractionInputSchema,
@@ -12521,6 +13647,7 @@ var VERSION = "0.15.0";
12521
13647
  HandoffSchema,
12522
13648
  HarnessStateSchema,
12523
13649
  InteractionTypeSchema,
13650
+ LITELLM_PRICING_URL,
12524
13651
  LayerViolationCollector,
12525
13652
  LockfilePackageSchema,
12526
13653
  LockfileSchema,
@@ -12537,6 +13664,7 @@ var VERSION = "0.15.0";
12537
13664
  RegressionDetector,
12538
13665
  RuleRegistry,
12539
13666
  SECURITY_DESCRIPTOR,
13667
+ STALENESS_WARNING_DAYS,
12540
13668
  SecurityConfigSchema,
12541
13669
  SecurityScanner,
12542
13670
  SharableBoundaryConfigSchema,
@@ -12553,6 +13681,8 @@ var VERSION = "0.15.0";
12553
13681
  ViolationSchema,
12554
13682
  addProvenance,
12555
13683
  agentConfigRules,
13684
+ aggregateByDay,
13685
+ aggregateBySession,
12556
13686
  analyzeDiff,
12557
13687
  analyzeLearningPatterns,
12558
13688
  appendFailure,
@@ -12571,13 +13701,18 @@ var VERSION = "0.15.0";
12571
13701
  buildDependencyGraph,
12572
13702
  buildExclusionSet,
12573
13703
  buildSnapshot,
13704
+ calculateCost,
12574
13705
  checkDocCoverage,
12575
13706
  checkEligibility,
12576
13707
  checkEvidenceCoverage,
13708
+ checkTaint,
12577
13709
  classifyFinding,
12578
13710
  clearEventHashCache,
12579
13711
  clearFailuresCache,
12580
13712
  clearLearningsCache,
13713
+ clearTaint,
13714
+ computeOverallSeverity,
13715
+ computeScanExitCode,
12581
13716
  configureFeedback,
12582
13717
  constraintRuleId,
12583
13718
  contextBudget,
@@ -12632,35 +13767,48 @@ var VERSION = "0.15.0";
12632
13767
  getActionEmitter,
12633
13768
  getExitCode,
12634
13769
  getFeedbackConfig,
13770
+ getInjectionPatterns,
13771
+ getModelPrice,
12635
13772
  getOutline,
12636
13773
  getParser,
12637
13774
  getPhaseCategories,
12638
13775
  getStreamForBranch,
13776
+ getTaintFilePath,
12639
13777
  getUpdateNotification,
12640
13778
  goRules,
12641
13779
  injectionRules,
13780
+ insecureDefaultsRules,
13781
+ isDuplicateFinding,
12642
13782
  isSmallSuggestion,
12643
13783
  isUpdateCheckEnabled,
12644
13784
  listActiveSessions,
12645
13785
  listStreams,
13786
+ listTaintedSessions,
12646
13787
  loadBudgetedLearnings,
12647
13788
  loadEvents,
12648
13789
  loadFailures,
12649
13790
  loadHandoff,
12650
13791
  loadIndexEntries,
13792
+ loadPricingData,
12651
13793
  loadRelevantLearnings,
12652
13794
  loadSessionSummary,
12653
13795
  loadState,
12654
13796
  loadStreamIndex,
12655
13797
  logAgentAction,
13798
+ mapInjectionFindings,
13799
+ mapSecurityFindings,
13800
+ mapSecuritySeverity,
12656
13801
  mcpRules,
12657
13802
  migrateToStreams,
12658
13803
  networkRules,
12659
13804
  nodeRules,
13805
+ parseCCRecords,
12660
13806
  parseDateFromEntry,
12661
13807
  parseDiff,
12662
13808
  parseFile,
12663
13809
  parseFrontmatter,
13810
+ parseHarnessIgnore,
13811
+ parseLiteLLMData,
12664
13812
  parseManifest,
12665
13813
  parseRoadmap,
12666
13814
  parseSecurityConfig,
@@ -12671,9 +13819,11 @@ var VERSION = "0.15.0";
12671
13819
  pruneLearnings,
12672
13820
  reactRules,
12673
13821
  readCheckState,
13822
+ readCostRecords,
12674
13823
  readLockfile,
12675
13824
  readSessionSection,
12676
13825
  readSessionSections,
13826
+ readTaint,
12677
13827
  removeContributions,
12678
13828
  removeProvenance,
12679
13829
  requestMultiplePeerReviews,
@@ -12700,11 +13850,13 @@ var VERSION = "0.15.0";
12700
13850
  saveHandoff,
12701
13851
  saveState,
12702
13852
  saveStreamIndex,
13853
+ scanForInjection,
12703
13854
  scopeContext,
12704
13855
  searchSymbols,
12705
13856
  secretRules,
12706
13857
  serializeRoadmap,
12707
13858
  setActiveStream,
13859
+ sharpEdgesRules,
12708
13860
  shouldRunCheck,
12709
13861
  spawnBackgroundCheck,
12710
13862
  syncConstraintNodes,
@@ -12729,6 +13881,7 @@ var VERSION = "0.15.0";
12729
13881
  writeConfig,
12730
13882
  writeLockfile,
12731
13883
  writeSessionSummary,
13884
+ writeTaint,
12732
13885
  xssRules,
12733
13886
  ...require("@harness-engineering/types")
12734
13887
  });