scripts/inventory-logging.mjs
scripts/inventory-logging.mjsBrowse 5 files
10,192 tokens
46,777 bytes
Token encoding: o200k_base
Snapshot 506f736
← Back to SKILL.md
1#!/usr/bin/env node2 3import { createHash } from 'node:crypto';4import { readFileSync, readdirSync, statSync } from 'node:fs';5import { dirname, relative, resolve } from 'node:path';6import process from 'node:process';7import ts from 'typescript';8 9const LOGGER_METHODS = new Set(['debug', 'error', 'info', 'log', 'warn']);10const COMPUTED_METHOD = 'computed';11const LOGGER_TYPE_PROPERTIES = [12 'debug',13 'dontLogModelData',14 'dontLogToolData',15 'error',16 'namespace',17 'warn',18];19const STANDARD_GLOBALS = new Set(['global', 'globalThis', 'window']);20const SENSITIVE_HELPERS = new Map([21 ['logModelAndToolActionDebug', 'model+tool-helper'],22 ['logModelAndToolActionError', 'model+tool-helper'],23 ['logModelAndToolActionWarning', 'model+tool-helper'],24 ['logModelActionError', 'model-helper'],25 ['logToolActionDebug', 'tool-helper'],26 ['logToolActionError', 'tool-helper'],27 ['logToolActionWarning', 'tool-helper'],28]);29const SENSITIVE_HELPER_MODULES = new Set([30 '@openai/agents-core/utils/internal',31]);32const SOURCE_EXTENSIONS = new Set(['.cts', '.mts', '.ts', '.tsx']);33 34function parseArguments(argv) {35 const options = {36 format: 'markdown',37 roots: [],38 summaryOnly: false,39 };40 41 for (let index = 0; index < argv.length; index += 1) {42 const argument = argv[index];43 if (argument === '--format') {44 const format = argv[index + 1];45 if (format !== 'json' && format !== 'markdown') {46 throw new Error('--format must be either json or markdown.');47 }48 options.format = format;49 index += 1;50 } else if (argument === '--summary-only') {51 options.summaryOnly = true;52 } else if (argument === '--help' || argument === '-h') {53 options.help = true;54 } else if (argument.startsWith('-')) {55 throw new Error(`Unknown option: ${argument}`);56 } else {57 options.roots.push(argument);58 }59 }60 61 return options;62}63 64function extension(path) {65 const match = path.match(/(\.[^.]+)$/);66 return match?.[1] ?? '';67}68 69function collectSourceFiles(path) {70 const absolutePath = resolve(path);71 const stats = statSync(absolutePath);72 if (stats.isFile()) {73 return SOURCE_EXTENSIONS.has(extension(absolutePath)) ? [absolutePath] : [];74 }75 76 return readdirSync(absolutePath, { withFileTypes: true })77 .filter((entry) => !entry.name.startsWith('.') && entry.name !== 'dist')78 .flatMap((entry) => collectSourceFiles(resolve(absolutePath, entry.name)));79}80 81function unwrapExpression(expression) {82 let current = expression;83 while (84 ts.isAsExpression(current) ||85 ts.isNonNullExpression(current) ||86 ts.isParenthesizedExpression(current) ||87 ts.isSatisfiesExpression(current) ||88 ts.isTypeAssertionExpression(current)89 ) {90 current = current.expression;91 }92 return current;93}94 95function propertyAccessParts(expression) {96 const unwrapped = unwrapExpression(expression);97 if (ts.isPropertyAccessExpression(unwrapped)) {98 return {99 computed: false,100 receiver: unwrapped.expression,101 method: unwrapped.name.text,102 };103 }104 if (ts.isElementAccessExpression(unwrapped) && unwrapped.argumentExpression) {105 const isStatic =106 ts.isStringLiteral(unwrapped.argumentExpression) ||107 ts.isNoSubstitutionTemplateLiteral(unwrapped.argumentExpression);108 return {109 computed: !isStatic,110 receiver: unwrapped.expression,111 method: isStatic ? unwrapped.argumentExpression.text : COMPUTED_METHOD,112 };113 }114 return null;115}116 117function isGloballyQualifiedConsole(expression) {118 const access = propertyAccessParts(expression);119 if (!access || access.method !== 'console') {120 return false;121 }122 const receiver = unwrapExpression(access.receiver);123 return ts.isIdentifier(receiver) && STANDARD_GLOBALS.has(receiver.text);124}125 126function checkerRecognizesLogger(expression, checker) {127 if (!checker) {128 return false;129 }130 try {131 const type = checker.getApparentType(132 checker.getNonNullableType(checker.getTypeAtLocation(expression)),133 );134 return LOGGER_TYPE_PROPERTIES.every((property) =>135 checker.getPropertyOfType(type, property),136 );137 } catch {138 return false;139 }140}141 142function normalizeFilePath(path) {143 return path.replace(/\\/g, '/');144}145 146function importName(specifier) {147 return specifier.propertyName?.text ?? specifier.name.text;148}149 150function isLoggerModule(moduleName) {151 return /(?:^|\/)logger(?:\.[cm]?[jt]s)?$/.test(moduleName);152}153 154function isSensitiveHelperModule(moduleName) {155 return isLoggerModule(moduleName) || SENSITIVE_HELPER_MODULES.has(moduleName);156}157 158function propertyNameText(name, sourceFile) {159 if (!name) {160 return null;161 }162 if (163 ts.isIdentifier(name) ||164 ts.isStringLiteral(name) ||165 ts.isNoSubstitutionTemplateLiteral(name) ||166 ts.isNumericLiteral(name)167 ) {168 return name.text;169 }170 return normalizeNodeText(name, sourceFile);171}172 173function typeIsLoggerValue(typeNode, loggerTypeBindings) {174 if (!typeNode) {175 return false;176 }177 if (ts.isParenthesizedTypeNode(typeNode) || ts.isTypeOperatorNode(typeNode)) {178 return typeIsLoggerValue(typeNode.type, loggerTypeBindings);179 }180 if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {181 return typeNode.types.some((type) =>182 typeIsLoggerValue(type, loggerTypeBindings),183 );184 }185 if (!ts.isTypeReferenceNode(typeNode)) {186 return false;187 }188 if (189 (ts.isIdentifier(typeNode.typeName) &&190 loggerTypeBindings.has(typeNode.typeName.text)) ||191 (ts.isQualifiedName(typeNode.typeName) &&192 typeNode.typeName.right.text === 'Logger')193 ) {194 return true;195 }196 return (197 ts.isIdentifier(typeNode.typeName) &&198 ['Partial', 'Readonly', 'Required'].includes(typeNode.typeName.text) &&199 typeNode.typeArguments?.some((type) =>200 typeIsLoggerValue(type, loggerTypeBindings),201 ) === true202 );203}204 205function heritageReferencesLogger(node, loggerTypeBindings) {206 return Boolean(207 node.heritageClauses?.some((clause) =>208 clause.types.some((heritageType) => {209 const expression = unwrapExpression(heritageType.expression);210 if (211 (ts.isIdentifier(expression) &&212 loggerTypeBindings.has(expression.text)) ||213 (ts.isPropertyAccessExpression(expression) &&214 expression.name.text === 'Logger')215 ) {216 return true;217 }218 return (219 heritageType.typeArguments?.some((type) =>220 typeIsLoggerValue(type, loggerTypeBindings),221 ) === true222 );223 }),224 ),225 );226}227 228function collectLoggingSymbols(sourceFile, checker) {229 const consoleBindings = new Set(['console']);230 const consoleMethodBindings = new Map();231 const consolePropertyNames = new Set();232 const loggerBindings = new Set();233 const loggerFactoryBindings = new Set(['getLogger']);234 const loggerMethodBindings = new Map();235 const loggerNamespaceBindings = new Set();236 const loggerObjectTypeProperties = new Map();237 const loggerPropertyNames = new Set();238 const loggerTypeBindings = new Set(['Logger']);239 const sensitiveHelperBindings = new Map();240 const sensitiveHelperNamespaceBindings = new Set();241 242 for (const statement of sourceFile.statements) {243 if (!ts.isImportDeclaration(statement)) {244 continue;245 }246 const moduleName = ts.isStringLiteral(statement.moduleSpecifier)247 ? statement.moduleSpecifier.text248 : '';249 const trustedSensitiveHelperModule = isSensitiveHelperModule(moduleName);250 const clause = statement.importClause;251 if (!clause) {252 continue;253 }254 if (clause.name && isLoggerModule(moduleName)) {255 loggerBindings.add(clause.name.text);256 }257 const bindings = clause.namedBindings;258 if (bindings && ts.isNamespaceImport(bindings)) {259 loggerNamespaceBindings.add(bindings.name.text);260 if (trustedSensitiveHelperModule) {261 sensitiveHelperNamespaceBindings.add(bindings.name.text);262 }263 continue;264 }265 if (!bindings || !ts.isNamedImports(bindings)) {266 continue;267 }268 for (const specifier of bindings.elements) {269 const imported = importName(specifier);270 const local = specifier.name.text;271 if (imported === 'getLogger') {272 loggerFactoryBindings.add(local);273 } else if (imported === 'logger') {274 loggerBindings.add(local);275 } else if (imported === 'Logger') {276 loggerTypeBindings.add(local);277 }278 const sensitivePolicy = SENSITIVE_HELPERS.get(imported);279 if (sensitivePolicy && trustedSensitiveHelperModule) {280 sensitiveHelperBindings.set(local, {281 method: imported,282 policy: sensitivePolicy,283 });284 }285 }286 }287 288 let changed = true;289 while (changed) {290 changed = false;291 function add(set, value) {292 if (value && !set.has(value)) {293 set.add(value);294 changed = true;295 }296 }297 function addMappings(map, key, values) {298 if (!key) {299 return;300 }301 for (const value of values) {302 if (!value) {303 continue;304 }305 let mapped = map.get(key);306 if (!mapped) {307 mapped = new Set();308 map.set(key, mapped);309 }310 if (!mapped.has(value)) {311 mapped.add(value);312 changed = true;313 }314 }315 }316 function loggerPropertiesForType(typeNode) {317 if (!typeNode) {318 return new Set();319 }320 if (321 ts.isParenthesizedTypeNode(typeNode) ||322 ts.isTypeOperatorNode(typeNode)323 ) {324 return loggerPropertiesForType(typeNode.type);325 }326 if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {327 return new Set(328 typeNode.types.flatMap((type) => [...loggerPropertiesForType(type)]),329 );330 }331 if (ts.isTypeLiteralNode(typeNode)) {332 return new Set(333 typeNode.members.flatMap((member) =>334 ts.isPropertySignature(member) &&335 typeIsLoggerValue(member.type, loggerTypeBindings)336 ? [propertyNameText(member.name, sourceFile)]337 : [],338 ),339 );340 }341 if (!ts.isTypeReferenceNode(typeNode)) {342 return new Set();343 }344 const properties = new Set();345 if (ts.isIdentifier(typeNode.typeName)) {346 for (const property of loggerObjectTypeProperties.get(347 typeNode.typeName.text,348 ) ?? []) {349 properties.add(property);350 }351 }352 for (const typeArgument of typeNode.typeArguments ?? []) {353 for (const property of loggerPropertiesForType(typeArgument)) {354 properties.add(property);355 }356 }357 return properties;358 }359 function isKnownConsoleExpression(expression) {360 const current = unwrapExpression(expression);361 if (ts.isIdentifier(current)) {362 return consoleBindings.has(current.text);363 }364 if (isGloballyQualifiedConsole(current)) {365 return true;366 }367 const access = propertyAccessParts(current);368 if (access && consolePropertyNames.has(access.method)) {369 return true;370 }371 if (ts.isConditionalExpression(current)) {372 return (373 isKnownConsoleExpression(current.whenTrue) ||374 isKnownConsoleExpression(current.whenFalse)375 );376 }377 if (378 ts.isBinaryExpression(current) &&379 current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken380 ) {381 return (382 isKnownConsoleExpression(current.left) ||383 isKnownConsoleExpression(current.right)384 );385 }386 return false;387 }388 function resolveConsoleMethods(expression) {389 const current = unwrapExpression(expression);390 if (ts.isIdentifier(current)) {391 return consoleMethodBindings.get(current.text) ?? [];392 }393 const access = propertyAccessParts(current);394 return access && isKnownConsoleExpression(access.receiver)395 ? [access.method]396 : [];397 }398 function recordConsoleDeclaration(declaration) {399 if (!declaration.initializer) {400 return;401 }402 if (ts.isIdentifier(declaration.name)) {403 if (isKnownConsoleExpression(declaration.initializer)) {404 add(consoleBindings, declaration.name.text);405 }406 addMappings(407 consoleMethodBindings,408 declaration.name.text,409 resolveConsoleMethods(declaration.initializer),410 );411 return;412 }413 if (414 !ts.isObjectBindingPattern(declaration.name) ||415 !isKnownConsoleExpression(declaration.initializer)416 ) {417 return;418 }419 for (const element of declaration.name.elements) {420 if (element.dotDotDotToken || !ts.isIdentifier(element.name)) {421 continue;422 }423 addMappings(consoleMethodBindings, element.name.text, [424 propertyNameText(element.propertyName ?? element.name, sourceFile),425 ]);426 }427 }428 function isLoggerFactoryCall(expression) {429 const current = unwrapExpression(expression);430 if (!ts.isCallExpression(current)) {431 return false;432 }433 const callee = unwrapExpression(current.expression);434 if (ts.isIdentifier(callee) && loggerFactoryBindings.has(callee.text)) {435 return true;436 }437 const access = propertyAccessParts(callee);438 return Boolean(439 access &&440 access.method === 'getLogger' &&441 ts.isIdentifier(unwrapExpression(access.receiver)) &&442 loggerNamespaceBindings.has(unwrapExpression(access.receiver).text),443 );444 }445 function isKnownLoggerExpression(expression) {446 const current = unwrapExpression(expression);447 if (checkerRecognizesLogger(current, checker)) {448 return true;449 }450 if (ts.isIdentifier(current)) {451 return loggerBindings.has(current.text);452 }453 if (isLoggerFactoryCall(current)) {454 return true;455 }456 const access = propertyAccessParts(current);457 if (access) {458 const receiver = unwrapExpression(access.receiver);459 return (460 loggerPropertyNames.has(access.method) ||461 (ts.isIdentifier(receiver) &&462 loggerNamespaceBindings.has(receiver.text) &&463 access.method === 'logger')464 );465 }466 if (ts.isConditionalExpression(current)) {467 return (468 isKnownLoggerExpression(current.whenTrue) ||469 isKnownLoggerExpression(current.whenFalse)470 );471 }472 if (473 ts.isBinaryExpression(current) &&474 current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken475 ) {476 return (477 isKnownLoggerExpression(current.left) ||478 isKnownLoggerExpression(current.right)479 );480 }481 return false;482 }483 function resolveLoggerMethods(expression) {484 const current = unwrapExpression(expression);485 if (ts.isIdentifier(current)) {486 return loggerMethodBindings.get(current.text) ?? [];487 }488 const access = propertyAccessParts(current);489 return access &&490 (access.computed || LOGGER_METHODS.has(access.method)) &&491 isKnownLoggerExpression(access.receiver)492 ? [access.method]493 : [];494 }495 function recordLoggerMethodDeclaration(declaration) {496 if (!declaration.initializer) {497 return;498 }499 if (ts.isIdentifier(declaration.name)) {500 addMappings(501 loggerMethodBindings,502 declaration.name.text,503 resolveLoggerMethods(declaration.initializer),504 );505 return;506 }507 if (508 !ts.isObjectBindingPattern(declaration.name) ||509 !isKnownLoggerExpression(declaration.initializer)510 ) {511 return;512 }513 for (const element of declaration.name.elements) {514 if (element.dotDotDotToken || !ts.isIdentifier(element.name)) {515 continue;516 }517 const method = propertyNameText(518 element.propertyName ?? element.name,519 sourceFile,520 );521 if (LOGGER_METHODS.has(method)) {522 addMappings(loggerMethodBindings, element.name.text, [method]);523 }524 }525 }526 function recordDeclaration(declaration) {527 if (ts.isObjectBindingPattern(declaration.name)) {528 const loggerProperties = loggerPropertiesForType(declaration.type);529 for (const element of declaration.name.elements) {530 if (element.dotDotDotToken || !ts.isIdentifier(element.name)) {531 continue;532 }533 const propertyName = propertyNameText(534 element.propertyName ?? element.name,535 sourceFile,536 );537 if (loggerProperties.has(propertyName)) {538 add(loggerBindings, element.name.text);539 }540 }541 }542 if (typeIsLoggerValue(declaration.type, loggerTypeBindings)) {543 if (ts.isPropertyDeclaration(declaration)) {544 add(545 loggerPropertyNames,546 propertyNameText(declaration.name, sourceFile),547 );548 } else if (ts.isIdentifier(declaration.name)) {549 add(loggerBindings, declaration.name.text);550 if (ts.isParameter(declaration) && declaration.modifiers?.length) {551 add(552 loggerPropertyNames,553 propertyNameText(declaration.name, sourceFile),554 );555 }556 }557 }558 if (559 !declaration.initializer ||560 !isKnownLoggerExpression(declaration.initializer)561 ) {562 return;563 }564 if (ts.isPropertyDeclaration(declaration)) {565 add(566 loggerPropertyNames,567 propertyNameText(declaration.name, sourceFile),568 );569 } else if (ts.isIdentifier(declaration.name)) {570 add(loggerBindings, declaration.name.text);571 } else {572 add(573 loggerPropertyNames,574 propertyNameText(declaration.name, sourceFile),575 );576 }577 }578 function visit(current) {579 if (580 ts.isTypeAliasDeclaration(current) &&581 typeIsLoggerValue(current.type, loggerTypeBindings)582 ) {583 add(loggerTypeBindings, current.name.text);584 }585 if (ts.isTypeAliasDeclaration(current)) {586 addMappings(587 loggerObjectTypeProperties,588 current.name.text,589 loggerPropertiesForType(current.type),590 );591 }592 if (ts.isInterfaceDeclaration(current)) {593 const properties = current.members.flatMap((member) =>594 ts.isPropertySignature(member) &&595 typeIsLoggerValue(member.type, loggerTypeBindings)596 ? [propertyNameText(member.name, sourceFile)]597 : [],598 );599 for (const clause of current.heritageClauses ?? []) {600 for (const heritageType of clause.types) {601 const expression = unwrapExpression(heritageType.expression);602 if (ts.isIdentifier(expression)) {603 properties.push(604 ...(loggerObjectTypeProperties.get(expression.text) ?? []),605 );606 }607 }608 }609 addMappings(loggerObjectTypeProperties, current.name.text, properties);610 }611 if (612 (ts.isInterfaceDeclaration(current) ||613 ts.isClassDeclaration(current)) &&614 current.name &&615 heritageReferencesLogger(current, loggerTypeBindings)616 ) {617 add(loggerTypeBindings, current.name.text);618 }619 if (620 ts.isPropertySignature(current) &&621 typeIsLoggerValue(current.type, loggerTypeBindings)622 ) {623 add(loggerPropertyNames, propertyNameText(current.name, sourceFile));624 }625 if (626 ts.isVariableDeclaration(current) ||627 ts.isParameter(current) ||628 ts.isPropertyDeclaration(current)629 ) {630 recordDeclaration(current);631 }632 if (ts.isVariableDeclaration(current)) {633 recordConsoleDeclaration(current);634 recordLoggerMethodDeclaration(current);635 }636 if (637 ts.isPropertyAssignment(current) &&638 isKnownConsoleExpression(current.initializer)639 ) {640 add(consolePropertyNames, propertyNameText(current.name, sourceFile));641 }642 if (643 ts.isShorthandPropertyAssignment(current) &&644 consoleBindings.has(current.name.text)645 ) {646 add(consolePropertyNames, current.name.text);647 }648 if (649 ts.isPropertyAssignment(current) &&650 isKnownLoggerExpression(current.initializer)651 ) {652 add(loggerPropertyNames, propertyNameText(current.name, sourceFile));653 }654 if (655 ts.isShorthandPropertyAssignment(current) &&656 loggerBindings.has(current.name.text)657 ) {658 add(loggerPropertyNames, current.name.text);659 }660 if (661 ts.isBinaryExpression(current) &&662 current.operatorToken.kind === ts.SyntaxKind.EqualsToken663 ) {664 const target = unwrapExpression(current.left);665 if (isKnownLoggerExpression(current.right)) {666 if (ts.isIdentifier(target)) {667 add(loggerBindings, target.text);668 } else {669 const targetAccess = propertyAccessParts(target);670 if (targetAccess) {671 add(loggerPropertyNames, targetAccess.method);672 }673 }674 }675 if (ts.isIdentifier(target)) {676 if (isKnownConsoleExpression(current.right)) {677 add(consoleBindings, target.text);678 }679 addMappings(680 consoleMethodBindings,681 target.text,682 resolveConsoleMethods(current.right),683 );684 addMappings(685 loggerMethodBindings,686 target.text,687 resolveLoggerMethods(current.right),688 );689 } else if (isKnownConsoleExpression(current.right)) {690 const targetAccess = propertyAccessParts(target);691 if (targetAccess) {692 add(consolePropertyNames, targetAccess.method);693 }694 }695 }696 ts.forEachChild(current, visit);697 }698 visit(sourceFile);699 }700 701 function isLoggerReceiver(expression) {702 const current = unwrapExpression(expression);703 if (checkerRecognizesLogger(current, checker)) {704 return true;705 }706 if (ts.isIdentifier(current)) {707 return loggerBindings.has(current.text);708 }709 if (ts.isCallExpression(current)) {710 const callee = unwrapExpression(current.expression);711 if (ts.isIdentifier(callee) && loggerFactoryBindings.has(callee.text)) {712 return true;713 }714 const access = propertyAccessParts(callee);715 return Boolean(716 access &&717 access.method === 'getLogger' &&718 ts.isIdentifier(unwrapExpression(access.receiver)) &&719 loggerNamespaceBindings.has(unwrapExpression(access.receiver).text),720 );721 }722 const access = propertyAccessParts(current);723 if (access) {724 const receiver = unwrapExpression(access.receiver);725 return (726 loggerPropertyNames.has(access.method) ||727 (receiver.kind === ts.SyntaxKind.ThisKeyword &&728 access.method === 'logger') ||729 (ts.isIdentifier(receiver) &&730 loggerNamespaceBindings.has(receiver.text) &&731 access.method === 'logger')732 );733 }734 return false;735 }736 737 function resolveSensitiveHelper(expression) {738 const current = unwrapExpression(expression);739 if (ts.isIdentifier(current)) {740 return sensitiveHelperBindings.get(current.text) ?? null;741 }742 const access = propertyAccessParts(current);743 if (!access) {744 return null;745 }746 const receiver = unwrapExpression(access.receiver);747 if (748 !ts.isIdentifier(receiver) ||749 !sensitiveHelperNamespaceBindings.has(receiver.text)750 ) {751 return null;752 }753 const policy = SENSITIVE_HELPERS.get(access.method);754 return policy ? { method: access.method, policy } : null;755 }756 757 function resolveConsoleMethod(expression) {758 const current = unwrapExpression(expression);759 if (ts.isIdentifier(current)) {760 const methods = consoleMethodBindings.get(current.text);761 return methods ? [...methods].sort().join('|') : null;762 }763 const access = propertyAccessParts(current);764 if (!access) {765 return null;766 }767 const receiver = unwrapExpression(access.receiver);768 const receiverAccess = propertyAccessParts(receiver);769 return (ts.isIdentifier(receiver) && consoleBindings.has(receiver.text)) ||770 isGloballyQualifiedConsole(receiver) ||771 (receiverAccess && consolePropertyNames.has(receiverAccess.method))772 ? access.method773 : null;774 }775 776 function resolveLoggerMethod(expression) {777 const current = unwrapExpression(expression);778 if (ts.isIdentifier(current)) {779 const methods = loggerMethodBindings.get(current.text);780 return methods ? [...methods].sort().join('|') : null;781 }782 const access = propertyAccessParts(current);783 return access &&784 (access.computed || LOGGER_METHODS.has(access.method)) &&785 isLoggerReceiver(access.receiver)786 ? access.method787 : null;788 }789 790 return {791 isLoggerReceiver,792 resolveConsoleMethod,793 resolveLoggerMethod,794 resolveSensitiveHelper,795 };796}797 798function isStaticString(expression) {799 const unwrapped = unwrapExpression(expression);800 return (801 ts.isStringLiteral(unwrapped) ||802 ts.isNoSubstitutionTemplateLiteral(unwrapped)803 );804}805 806function hasDynamicMessage(call) {807 return call.arguments.length > 0 && !isStaticString(call.arguments[0]);808}809 810function referencesIdentifier(node, identifier) {811 let found = false;812 function visit(current) {813 if (found) {814 return;815 }816 if (ts.isIdentifier(current) && current.text === identifier) {817 found = true;818 return;819 }820 ts.forEachChild(current, visit);821 }822 visit(node);823 return found;824}825 826function bindingIdentifiers(name) {827 const identifiers = [];828 829 function visit(current) {830 if (ts.isIdentifier(current)) {831 identifiers.push(current.text);832 return;833 }834 if (835 ts.isObjectBindingPattern(current) ||836 ts.isArrayBindingPattern(current)837 ) {838 for (const element of current.elements) {839 if (ts.isBindingElement(element)) {840 visit(element.name);841 }842 }843 }844 }845 846 visit(name);847 return identifiers;848}849 850function isRejectionCallbackArgument(call, callbackIndex) {851 const access = propertyAccessParts(call.expression);852 return (853 (access?.method === 'catch' && callbackIndex === 0) ||854 (access?.method === 'then' && callbackIndex === 1) ||855 ((access?.method === 'on' ||856 access?.method === 'once' ||857 access?.method === 'addListener') &&858 callbackIndex === 1 &&859 ts.isStringLiteral(call.arguments[0]) &&860 call.arguments[0].text === 'unhandledRejection')861 );862}863 864function enclosingRejectedValueIdentifiers(node) {865 const identifiers = new Set();866 let current = node.parent;867 while (current) {868 if (ts.isCatchClause(current)) {869 const declaration = current.variableDeclaration;870 if (declaration) {871 for (const identifier of bindingIdentifiers(declaration.name)) {872 identifiers.add(identifier);873 }874 }875 }876 if (ts.isFunctionLike(current)) {877 const callback = current;878 const call = callback.parent;879 if (ts.isCallExpression(call)) {880 const callbackIndex = call.arguments.indexOf(callback);881 if (isRejectionCallbackArgument(call, callbackIndex)) {882 const parameter = callback.parameters[0];883 if (parameter) {884 for (const identifier of bindingIdentifiers(parameter.name)) {885 identifiers.add(identifier);886 }887 }888 }889 }890 }891 current = current.parent;892 }893 return [...identifiers];894}895 896function normalizeNodeText(node, sourceFile) {897 return node.getText(sourceFile).replace(/\s+/g, ' ').trim();898}899 900function declarationName(node, sourceFile) {901 if (902 (ts.isFunctionDeclaration(node) ||903 ts.isClassDeclaration(node) ||904 ts.isMethodDeclaration(node) ||905 ts.isPropertyDeclaration(node)) &&906 node.name907 ) {908 return normalizeNodeText(node.name, sourceFile);909 }910 if (ts.isConstructorDeclaration(node)) {911 return 'constructor';912 }913 if (ts.isVariableDeclaration(node)) {914 return normalizeNodeText(node.name, sourceFile);915 }916 if (ts.isPropertyAssignment(node)) {917 return normalizeNodeText(node.name, sourceFile);918 }919 return null;920}921 922function callSiteContext(node, sourceFile) {923 const parts = [];924 let child = node;925 let current = node.parent;926 while (current && !ts.isSourceFile(current)) {927 const name = declarationName(current, sourceFile);928 if (name) {929 parts.push(`${ts.SyntaxKind[current.kind]}:${name}`);930 }931 if (ts.isIfStatement(current)) {932 const branch =933 current.expression === child934 ? 'condition'935 : current.thenStatement === child936 ? 'then'937 : 'else';938 parts.push(939 `if:${normalizeNodeText(current.expression, sourceFile)}:${branch}`,940 );941 }942 if (ts.isConditionalExpression(current)) {943 const branch =944 current.condition === child945 ? 'condition'946 : current.whenTrue === child947 ? 'true'948 : 'false';949 parts.push(950 `conditional:${normalizeNodeText(current.condition, sourceFile)}:${branch}`,951 );952 }953 if (ts.isCaseClause(current)) {954 parts.push(`case:${normalizeNodeText(current.expression, sourceFile)}`);955 } else if (ts.isDefaultClause(current)) {956 parts.push('case:default');957 }958 if (ts.isSwitchStatement(current)) {959 parts.push(`switch:${normalizeNodeText(current.expression, sourceFile)}`);960 }961 if (ts.isTryStatement(current)) {962 const branch =963 current.tryBlock === child964 ? 'try'965 : current.catchClause === child966 ? 'catch'967 : 'finally';968 parts.push(`try:${branch}`);969 }970 if (ts.isCallExpression(current) && current.arguments.includes(child)) {971 const callbackIndex = current.arguments.indexOf(child);972 parts.push(973 `callback:${normalizeNodeText(current.expression, sourceFile)}:${callbackIndex}`,974 );975 }976 child = current;977 current = current.parent;978 }979 return parts.reverse().join('>') || '<module>';980}981 982function possibleBooleanResults(expression, flagName, flagValue) {983 const current = unwrapExpression(expression);984 if (985 referencesIdentifier(current, flagName) &&986 ((ts.isIdentifier(current) && current.text === flagName) ||987 propertyAccessParts(current)?.method === flagName)988 ) {989 return new Set([flagValue]);990 }991 if (current.kind === ts.SyntaxKind.TrueKeyword) {992 return new Set([true]);993 }994 if (current.kind === ts.SyntaxKind.FalseKeyword) {995 return new Set([false]);996 }997 if (998 ts.isPrefixUnaryExpression(current) &&999 current.operator === ts.SyntaxKind.ExclamationToken1000 ) {1001 return new Set(1002 [...possibleBooleanResults(current.operand, flagName, flagValue)].map(1003 (value) => !value,1004 ),1005 );1006 }1007 if (ts.isBinaryExpression(current)) {1008 const operator = current.operatorToken.kind;1009 const left = possibleBooleanResults(current.left, flagName, flagValue);1010 const right = possibleBooleanResults(current.right, flagName, flagValue);1011 if (1012 operator === ts.SyntaxKind.AmpersandAmpersandToken ||1013 operator === ts.SyntaxKind.BarBarToken1014 ) {1015 const results = new Set();1016 for (const leftValue of left) {1017 for (const rightValue of right) {1018 results.add(1019 operator === ts.SyntaxKind.AmpersandAmpersandToken1020 ? leftValue && rightValue1021 : leftValue || rightValue,1022 );1023 }1024 }1025 return results;1026 }1027 if (1028 operator === ts.SyntaxKind.EqualsEqualsToken ||1029 operator === ts.SyntaxKind.EqualsEqualsEqualsToken ||1030 operator === ts.SyntaxKind.ExclamationEqualsToken ||1031 operator === ts.SyntaxKind.ExclamationEqualsEqualsToken1032 ) {1033 const negated =1034 operator === ts.SyntaxKind.ExclamationEqualsToken ||1035 operator === ts.SyntaxKind.ExclamationEqualsEqualsToken;1036 const results = new Set();1037 for (const leftValue of left) {1038 for (const rightValue of right) {1039 results.add(1040 negated ? leftValue !== rightValue : leftValue === rightValue,1041 );1042 }1043 }1044 return results;1045 }1046 }1047 return new Set([false, true]);1048}1049 1050function branchGuaranteesFlagDisabled(condition, branchValue, flagName) {1051 return (1052 referencesIdentifier(condition, flagName) &&1053 !possibleBooleanResults(condition, flagName, true).has(branchValue)1054 );1055}1056 1057function guardedPolicy(node) {1058 let child = node;1059 let current = node.parent;1060 let modelGuard = false;1061 let toolGuard = false;1062 while (current) {1063 if (ts.isFunctionLike(current)) {1064 break;1065 }1066 if (ts.isIfStatement(current) || ts.isConditionalExpression(current)) {1067 const conditionNode = ts.isIfStatement(current)1068 ? current.expression1069 : current.condition;1070 const trueBranch = ts.isIfStatement(current)1071 ? current.thenStatement1072 : current.whenTrue;1073 const falseBranch = ts.isIfStatement(current)1074 ? current.elseStatement1075 : current.whenFalse;1076 const branchValue =1077 child === trueBranch ? true : child === falseBranch ? false : null;1078 if (branchValue !== null) {1079 modelGuard ||= branchGuaranteesFlagDisabled(1080 conditionNode,1081 branchValue,1082 'dontLogModelData',1083 );1084 toolGuard ||= branchGuaranteesFlagDisabled(1085 conditionNode,1086 branchValue,1087 'dontLogToolData',1088 );1089 }1090 }1091 child = current;1092 current = current.parent;1093 }1094 if (modelGuard && toolGuard) {1095 return 'model+tool-guard';1096 }1097 if (modelGuard) {1098 return 'model-guard';1099 }1100 if (toolGuard) {1101 return 'tool-guard';1102 }1103 return 'none';1104}1105 1106function normalizeCallText(call, sourceFile) {1107 return call.getText(sourceFile).replace(/\s+/g, ' ').trim();1108}1109 1110function fingerprint(path, context, normalizedCall, occurrence) {1111 return createHash('sha256')1112 .update(`${path}\0${context}\0${normalizedCall}\0${occurrence}`)1113 .digest('hex')1114 .slice(0, 12);1115}1116 1117function signalsFor(text) {1118 const normalized = text.toLowerCase();1119 const signals = [];1120 const groups = [1121 ['model', /\b(model|response|request|completion|llm|realtime event)\b/],1122 [1123 'tool',1124 /\b(tool|function call|arguments|computer action|shell action|apply_patch|mcp)\b/,1125 ],1126 ['error', /\b(error|err|exception|failure|failed|reason)\b/],1127 ['payload', /\b(input|output|item|event|payload|data|trace|span)\b/],1128 ];1129 for (const [name, pattern] of groups) {1130 if (pattern.test(normalized)) {1131 signals.push(name);1132 }1133 }1134 return signals;1135}1136 1137function inventoryParsedSource(sourceFile, filePath, checker = null) {1138 const findings = [];1139 const occurrences = new Map();1140 const {1141 isLoggerReceiver,1142 resolveConsoleMethod,1143 resolveLoggerMethod,1144 resolveSensitiveHelper,1145 } = collectLoggingSymbols(sourceFile, checker);1146 1147 function recordFinding(1148 node,1149 normalizedCall,1150 kind,1151 method,1152 shape,1153 policy,1154 catchValue,1155 ) {1156 const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());1157 const context = callSiteContext(node, sourceFile);1158 const occurrenceKey = `${context}\0${normalizedCall}`;1159 const occurrence = occurrences.get(occurrenceKey) ?? 0;1160 occurrences.set(occurrenceKey, occurrence + 1);1161 findings.push({1162 fingerprint: fingerprint(filePath, context, normalizedCall, occurrence),1163 file: filePath,1164 line: start.line + 1,1165 column: start.character + 1,1166 kind,1167 method,1168 shape,1169 policy,1170 catchValue,1171 context,1172 signals: signalsFor(normalizedCall),1173 call: normalizedCall,1174 });1175 }1176 1177 function recordCall(call, kind, method, policy) {1178 const normalizedCall = normalizeCallText(call, sourceFile);1179 const catchValues = enclosingRejectedValueIdentifiers(call);1180 const referencedCatchValues = catchValues.filter((catchValue) =>1181 call.arguments.some((argument) =>1182 referencesIdentifier(argument, catchValue),1183 ),1184 );1185 const dynamicMessage = hasDynamicMessage(call);1186 const hasPayload = call.arguments.length > 1;1187 recordFinding(1188 call,1189 normalizedCall,1190 kind,1191 method,1192 hasPayload1193 ? 'payload'1194 : dynamicMessage1195 ? 'dynamic-message'1196 : 'static-message',1197 policy,1198 referencedCatchValues.length > 01199 ? referencedCatchValues.join(', ')1200 : null,1201 );1202 }1203 1204 function recordCallbackReference(1205 reference,1206 kind,1207 method,1208 policy,1209 parentCall,1210 callbackIndex,1211 ) {1212 recordFinding(1213 reference,1214 normalizeNodeText(reference, sourceFile),1215 kind,1216 method,1217 'dynamic-message',1218 policy,1219 isRejectionCallbackArgument(parentCall, callbackIndex)1220 ? 'rejection reason'1221 : null,1222 );1223 }1224 1225 function recordCallbackReferences(call, parentIsSink) {1226 if (parentIsSink) {1227 return;1228 }1229 for (const [callbackIndex, argument] of call.arguments.entries()) {1230 const consoleMethod = resolveConsoleMethod(argument);1231 if (consoleMethod) {1232 recordCallbackReference(1233 argument,1234 'console',1235 consoleMethod,1236 'none',1237 call,1238 callbackIndex,1239 );1240 continue;1241 }1242 const loggerMethod = resolveLoggerMethod(argument);1243 if (loggerMethod) {1244 recordCallbackReference(1245 argument,1246 'logger',1247 loggerMethod,1248 guardedPolicy(argument),1249 call,1250 callbackIndex,1251 );1252 }1253 }1254 }1255 1256 function visit(node) {1257 if (ts.isCallExpression(node)) {1258 const consoleMethod = resolveConsoleMethod(node.expression);1259 const loggerMethod = resolveLoggerMethod(node.expression);1260 const sensitiveHelper = resolveSensitiveHelper(node.expression);1261 const parentIsSink = Boolean(1262 consoleMethod || loggerMethod || sensitiveHelper,1263 );1264 if (consoleMethod) {1265 recordCall(node, 'console', consoleMethod, 'none');1266 } else if (loggerMethod) {1267 recordCall(node, 'logger', loggerMethod, guardedPolicy(node));1268 } else if (sensitiveHelper) {1269 recordCall(1270 node,1271 'sensitive-helper',1272 sensitiveHelper.method,1273 sensitiveHelper.policy,1274 );1275 } else {1276 const access = propertyAccessParts(node.expression);1277 if (access) {1278 if (1279 (access.computed || LOGGER_METHODS.has(access.method)) &&1280 isLoggerReceiver(access.receiver)1281 ) {1282 recordCall(node, 'logger', access.method, guardedPolicy(node));1283 }1284 }1285 }1286 recordCallbackReferences(node, parentIsSink);1287 }1288 ts.forEachChild(node, visit);1289 }1290 1291 visit(sourceFile);1292 return findings;1293}1294 1295export function inventorySource(sourceText, filePath = 'fixture.ts') {1296 filePath = normalizeFilePath(filePath);1297 const scriptKind = filePath.endsWith('.tsx')1298 ? ts.ScriptKind.TSX1299 : ts.ScriptKind.TS;1300 const sourceFile = ts.createSourceFile(1301 filePath,1302 sourceText,1303 ts.ScriptTarget.Latest,1304 true,1305 scriptKind,1306 );1307 return inventoryParsedSource(sourceFile, filePath);1308}1309 1310export function inventorySources(sources) {1311 const virtualRoot = resolve('/__sensitive_logging_inventory__');1312 const entries = Object.entries(sources).map(([filePath, sourceText]) => {1313 const normalizedPath = normalizeFilePath(filePath).replace(/^\/+/, '');1314 return {1315 absolutePath: resolve(virtualRoot, normalizedPath),1316 filePath: normalizedPath,1317 sourceText,1318 };1319 });1320 const sourceByPath = new Map(1321 entries.map((entry) => [entry.absolutePath, entry.sourceText]),1322 );1323 const options = {1324 module: ts.ModuleKind.ESNext,1325 moduleResolution: ts.ModuleResolutionKind.Bundler,1326 noEmit: true,1327 skipLibCheck: true,1328 target: ts.ScriptTarget.Latest,1329 };1330 const host = ts.createCompilerHost(options);1331 const defaultDirectoryExists = host.directoryExists?.bind(host);1332 const defaultFileExists = host.fileExists.bind(host);1333 const defaultGetSourceFile = host.getSourceFile.bind(host);1334 const defaultRealpath = host.realpath?.bind(host);1335 const defaultReadFile = host.readFile.bind(host);1336 host.directoryExists = (directoryName) => {1337 const absoluteDirectory = resolve(directoryName);1338 return (1339 absoluteDirectory === virtualRoot ||1340 [...sourceByPath.keys()].some((fileName) =>1341 fileName.startsWith(`${absoluteDirectory}/`),1342 ) ||1343 defaultDirectoryExists?.(directoryName) === true1344 );1345 };1346 host.fileExists = (fileName) =>1347 sourceByPath.has(resolve(fileName)) || defaultFileExists(fileName);1348 host.realpath = (fileName) =>1349 sourceByPath.has(resolve(fileName))1350 ? resolve(fileName)1351 : (defaultRealpath?.(fileName) ?? fileName);1352 host.readFile = (fileName) =>1353 sourceByPath.get(resolve(fileName)) ?? defaultReadFile(fileName);1354 host.getSourceFile = (fileName, languageVersion, onError) => {1355 const sourceText = sourceByPath.get(resolve(fileName));1356 return sourceText === undefined1357 ? defaultGetSourceFile(fileName, languageVersion, onError)1358 : ts.createSourceFile(1359 fileName,1360 sourceText,1361 languageVersion,1362 true,1363 fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,1364 );1365 };1366 host.getCurrentDirectory = () => virtualRoot;1367 const program = ts.createProgram({1368 rootNames: entries.map((entry) => entry.absolutePath),1369 options,1370 host,1371 });1372 const checker = program.getTypeChecker();1373 return entries.flatMap((entry) => {1374 const sourceFile = program.getSourceFile(entry.absolutePath);1375 return sourceFile1376 ? inventoryParsedSource(sourceFile, entry.filePath, checker)1377 : [];1378 });1379}1380 1381function summarize(findings) {1382 const dynamic = findings.filter(1383 (finding) => finding.shape !== 'static-message',1384 );1385 return {1386 total: findings.length,1387 dynamic: dynamic.length,1388 unclassifiedDynamic: dynamic.filter((finding) => finding.policy === 'none')1389 .length,1390 catchValueLogs: findings.filter((finding) => finding.catchValue).length,1391 unclassifiedCatchValueLogs: findings.filter(1392 (finding) => finding.catchValue && finding.policy === 'none',1393 ).length,1394 rawConsoleCalls: findings.filter((finding) => finding.kind === 'console')1395 .length,1396 };1397}1398 1399function markdown(findings, summary, summaryOnly) {1400 const lines = [1401 '# Sensitive logging inventory',1402 '',1403 `- Total logging calls: ${summary.total}`,1404 `- Dynamic calls: ${summary.dynamic}`,1405 `- Dynamic review candidates without an explicit model/tool policy: ${summary.unclassifiedDynamic}`,1406 `- Calls that log a caught value: ${summary.catchValueLogs}`,1407 `- Caught-value calls without an explicit model/tool policy: ${summary.unclassifiedCatchValueLogs}`,1408 `- Raw console calls: ${summary.rawConsoleCalls}`,1409 ];1410 1411 if (summaryOnly) {1412 return `${lines.join('\n')}\n`;1413 }1414 1415 lines.push(1416 '',1417 '| Location | Kind | Shape | Policy | Catch value | Lexical hints | Fingerprint |',1418 '| --- | --- | --- | --- | --- | --- | --- |',1419 );1420 for (const finding of findings) {1421 lines.push(1422 `| ${finding.file}:${finding.line} | ${finding.kind}.${finding.method} | ${finding.shape} | ${finding.policy} | ${finding.catchValue ?? ''} | ${finding.signals.join(', ')} | ${finding.fingerprint} |`,1423 );1424 }1425 lines.push('');1426 return `${lines.join('\n')}\n`;1427}1428 1429function usage() {1430 return `Usage: node .agents/skills/sensitive-logging-audit/scripts/inventory-logging.mjs [options] [roots...]1431 1432Inventory runtime logger and console calls so every dynamic log candidate can1433be reviewed as model data, tool data, both, or operationally safe. Lexical1434hints prioritize review; they do not classify a finding as sensitive.1435 1436Options:1437 --format <markdown|json> Output format (default: markdown)1438 --summary-only Print counts without the per-call ledger1439 -h, --help Show this help1440 1441Default roots: packages/*/src1442`;1443}1444 1445function createInventoryProgram(cwd, absolutePaths) {1446 let compilerOptions = {1447 module: ts.ModuleKind.ESNext,1448 moduleResolution: ts.ModuleResolutionKind.Node10,1449 noEmit: true,1450 skipLibCheck: true,1451 target: ts.ScriptTarget.Latest,1452 };1453 const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, 'tsconfig.json');1454 if (configPath) {1455 const loaded = ts.readConfigFile(configPath, ts.sys.readFile);1456 if (!loaded.error) {1457 const parsed = ts.parseJsonConfigFileContent(1458 loaded.config,1459 ts.sys,1460 dirname(configPath),1461 );1462 compilerOptions = {1463 ...parsed.options,1464 noEmit: true,1465 skipLibCheck: true,1466 };1467 }1468 }1469 return ts.createProgram({1470 rootNames: absolutePaths,1471 options: compilerOptions,1472 });1473}1474 1475export function run(argv = process.argv.slice(2)) {1476 const options = parseArguments(argv);1477 if (options.help) {1478 process.stdout.write(usage());1479 return;1480 }1481 1482 const cwd = process.cwd();1483 const roots =1484 options.roots.length > 01485 ? options.roots1486 : readdirSync(resolve(cwd, 'packages'), { withFileTypes: true })1487 .filter((entry) => entry.isDirectory())1488 .map((entry) => resolve(cwd, 'packages', entry.name, 'src'))1489 .filter((path) => {1490 try {1491 return statSync(path).isDirectory();1492 } catch {1493 return false;1494 }1495 });1496 1497 const absolutePaths = roots.flatMap(collectSourceFiles).sort();1498 const program = createInventoryProgram(cwd, absolutePaths);1499 const checker = program.getTypeChecker();1500 const findings = absolutePaths.flatMap((absolutePath) => {1501 const filePath = normalizeFilePath(relative(cwd, absolutePath));1502 const sourceFile = program.getSourceFile(absolutePath);1503 return sourceFile1504 ? inventoryParsedSource(sourceFile, filePath, checker)1505 : inventorySource(readFileSync(absolutePath, 'utf8'), filePath);1506 });1507 const summary = summarize(findings);1508 1509 if (options.format === 'json') {1510 process.stdout.write(1511 `${JSON.stringify(1512 options.summaryOnly ? { summary } : { summary, findings },1513 null,1514 2,1515 )}\n`,1516 );1517 } else {1518 process.stdout.write(markdown(findings, summary, options.summaryOnly));1519 }1520}1521 1522if (1523 process.argv[1] &&1524 resolve(process.argv[1]) === resolve(import.meta.filename)1525) {1526 try {1527 run();1528 } catch (error) {1529 process.stderr.write(1530 `Sensitive logging audit failed: ${error instanceof Error ? error.message : String(error)}\n`,1531 );1532 process.exitCode = 1;1533 }1534}1535