Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00:00

441 lines
16 KiB
TypeScript

/**
* Copyright 2026 Reto Meier
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as path from 'path';
import * as fs from 'fs/promises';
import { spawn } from 'child_process';
import { MultiAgentTool } from '../multiAgentTool.js';
import { MultiAgentToolResult, MultiAgentToolContext, ToolParsingResult } from '../../momoa_core/types.js';
import { randomUUID } from 'crypto';
import { tmpdir } from 'os';
import { isLockFile } from '../../utils/diffGenerator.js';
import { fileNameLookup } from '../../utils/fileNameLookup.js';
// --- Utility Functions ---
/**
* Determines the temporary full file path where the file content will be saved.
* We use a simple structure relative to the current working directory.
* @param sessionId A unique identifier for the current session (e.g., clientUUID).
* @param filename The name of the file.
* @returns The full path to the temporary file.
*/
function getFullFilePath(sessionId: string, filename: string): string {
// Correctly uses the system temp directory (usually /tmp in Cloud Run)
const systemTempDir = tmpdir();
// Structure: /tmp/momoa_lint/<sessionId>/<filename>
// Adding 'momoa_lint' namespace prevents collisions with other tools using /tmp
const tempDir = path.join(systemTempDir, 'momoa_lint', sessionId);
return path.join(tempDir, filename);
}
/**
* Saves the file content to the temporary location.
* @param sessionId Unique session identifier.
* @param filename The name of the file.
* @param fileContent The content to save.
*/
async function saveFile(sessionId: string, filename: string, fileContent: string): Promise<void> {
const fullPath = getFullFilePath(sessionId, filename);
const dirPath = path.dirname(fullPath);
await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(fullPath, fileContent, 'utf8');
console.log(`File saved successfully to ${fullPath}`);
}
/**
* Detects the programming language based on the file extension.
*
* @param filename - The name of the file.
* @returns A string indicating the detected language ('python', 'javascript', 'go', 'java', 'cpp', 'kotlin', 'docker', 'maven'), or 'unknown'.
*/
function detectLanguage(filename: string): string {
// One off file names
const baseName = path.basename(filename).toLowerCase();
if (baseName === 'pom.xml')
return 'maven';
else if (baseName === 'dockerfile')
return 'docker';
// Find the last dot in the filename to extract the extension.
const lastDotIndex = filename.lastIndexOf('.');
// If no dot or the dot is the first character (e.g., '.bashrc'), there's no standard extension.
if (lastDotIndex < 1) {
return 'unknown';
}
// Extract the extension including the dot and convert to lowercase for case-insensitive matching.
const extension = filename.substring(lastDotIndex).toLowerCase();
// Files based on extensions
switch (extension) {
case '.py':
return 'python';
case '.tsx':
case '.ts':
case '.js':
return 'javascript';
case '.go':
return 'go';
case '.java':
return 'java';
case '.kt':
return 'kotlin';
case '.cpp':
case '.cxx':
case '.cc':
case '.h':
case '.hpp':
return 'cpp';
default:
return 'unknown';
}
}
/**
* Executes an external command using child_process.spawn.
*
* @param command The command to execute.
* @param args Arguments for the command.
* @param filename The original filename (for output formatting).
* @param fullFileName The full path to the temporary file.
* @param resultPrefixString Prefix for the output string.
* @param successString String to return on successful execution with no output.
* @returns A promise that resolves with the formatted output string.
*/
function processExecution(command: string, args: string[], filename: string, fullFileName: string, resultPrefixString: string, successString: string, cwd?: string, env?: NodeJS.ProcessEnv): Promise<string> {
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
// Use 'spawn' for security to prevent command injection by keeping
// command and arguments separate.
console.log("Execute: " + command + " " + args.join(' '));
const cmdLineProcess = spawn(command, args, {
shell: false,
cwd: cwd,
env: env || process.env
});
// Implement timeout
const timeout = setTimeout(() => {
cmdLineProcess.kill('SIGTERM');
reject(`${resultPrefixString}Error: Process timed out after 60 seconds.`);
}, 60000); // 60 seconds timeout
cmdLineProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
cmdLineProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
cmdLineProcess.on('error', (err) => {
// Handle errors like command not found
clearTimeout(timeout);
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
reject(`${resultPrefixString}Error: Command "${command}" not found. Please ensure it is installed and in the system's PATH.`);
} else {
reject(`${resultPrefixString}Error spawning process: ${err.message}`);
}
console.error("Process Spawn Error");
});
cmdLineProcess.on('close', (_code) => {
clearTimeout(timeout);
let combinedOutput = stdout + stderr;
if (!combinedOutput) {
combinedOutput = successString;
}
const resultPrefix = resultPrefixString;
// We'll generally need to remove the path strings to avoid confusion.
const completePath = path.resolve(fullFileName);
combinedOutput = combinedOutput.replaceAll(completePath, filename);
combinedOutput = combinedOutput.replaceAll(fullFileName, filename);
// Always return whatever came from the linter.
resolve(`${resultPrefix}\n${combinedOutput}`);
});
});
}
// --- LintTool Class ---
export const LintTool: MultiAgentTool = {
displayName: "Lint Tool",
name: 'LINT{',
endToken: '}',
/**
* Executes the linting process for a given file.
* @param params Must contain 'filename'.
* @param context The tool context.
* @returns The linter output.
*/
async execute(params: Record<string, unknown>, context: MultiAgentToolContext): Promise<MultiAgentToolResult> {
const providedFilename = params['filename'] as string;
if (!providedFilename) {
return {
result: `Error: 'filename' parameter is missing for ${this.displayName} tool.`
};
}
// Combine keys from both maps for a comprehensive lookup.
const allFilesMap = new Map<string, string>([
...context.fileMap,
...Array.from(context.binaryFileMap.keys()).map(key => [key, ''] as [string, string])
]);
let filename = providedFilename.trim();
if (!allFilesMap.has(filename)) {
// File not found. NOW, let's try to find a suggestion.
const suggestion = await fileNameLookup(filename, allFilesMap, context.multiAgentGeminiClient);
if (suggestion && suggestion !== filename && allFilesMap.has(suggestion)) {
return {
result: `File '${filename}' was not found. Did you mean '${suggestion}'?`
};
}
return {
result: `---File '${filename}' not found---`
};
}
context.sendMessage({
type: 'PROGRESS_UPDATE',
message: `Linting \`${filename}\``,
}
);
// Handle binary files first.
if (context.binaryFileMap.has(filename)) {
return { result: "Binary files are not supported for linting." }
}
// Handle Lock Files
if (isLockFile(filename)) {
return { result: "Lock files are not supported for linting." }
}
const fileContent = context.fileMap.get(filename);
if (!fileContent) {
return { result: `\`${filename}\` is empty.` }
}
// Use a unique identifier for the session to scope temporary files.
// We use the transcript ID if available, otherwise a fixed string.
const sessionId = context.infrastructureContext.getSessionId() || randomUUID.toString();
const fullFileName = getFullFilePath(sessionId, filename);
try {
// 1. Save the file to disk.
await saveFile(sessionId, filename, fileContent);
const language = detectLanguage(filename);
// If C++, identify and copy sibling header files to the temp directory.
if (language === 'cpp') {
const targetDir = path.dirname(filename);
// Iterate over all available files in the context
for (const [candidateName, candidateContent] of context.fileMap.entries()) {
// skip the file we just saved
if (candidateName === filename) continue;
// Check if it is a sibling (same directory) and a header file
if (path.dirname(candidateName) === targetDir) {
const ext = path.extname(candidateName).toLowerCase();
if (ext === '.h' || ext === '.hpp') {
// Save the header file to the same relative path in the temp dir
await saveFile(sessionId, candidateName, candidateContent);
}
}
}
}
let linterCommand: string | null = null;
let linterArgs: string[] = [];
switch (language) {
case 'python':
linterCommand = 'python3';
linterArgs = ['-m', 'flake8', fullFileName, '--config', 'linter-tool-definition-files/.flake8', '--show-source'];
break;
case 'javascript':
// 1. Point to Hub's Binary (Absolute Path)
linterCommand = path.resolve('node_modules', '.bin', 'eslint');
// 2. Point to Hub's Config (Absolute Path)
const configPath = path.resolve('linter-tool-definition-files', 'eslint.config.js');
// 3. Target the file by BASENAME (since we will be inside the temp dir)
linterArgs = [
path.basename(fullFileName),
'--config', configPath,
'--no-color',
'--format', 'codeframe'
];
break;
case 'go':
linterCommand = 'revive';
// -formatter friendly: output format that is easy to parse
// fullFileName: the file to lint
linterArgs = ['-formatter', 'friendly', fullFileName];
break;
case 'java':
linterCommand = 'java';
// Note: This assumes checkstyle-10.23.1-all.jar is a valid JAR file.
linterArgs = ['-jar', 'linter-tool-definition-files/checkstyle-10.23.1-all.jar', fullFileName, '-c', 'linter-tool-definition-files/google_checks.xml'];
break;
case 'cpp':
linterCommand = 'clang-tidy';
// Add standard flags for C++, ensure filename is first arg
linterArgs = [
fullFileName,
'-checks=-clang-diagnostic-error',
'--',
'-std=c++17',
'-Iinclude',
'-DNDEBUG'
];
break;
case 'docker':
linterCommand = 'hadolint'; // Refactored to use globally installed binary
linterArgs = [fullFileName];
break;
case 'kotlin':
linterCommand = 'ktlint';
linterArgs = [
'--editorconfig=linter-tool-definition-files/.editorconfig',
fullFileName
];
break;
case 'maven':
linterCommand = 'mvn';
linterArgs = ['validate', '-f', fullFileName, '-B', '-q', '-U'];
break;
default:
return { result: `You asked to lint ${filename} and this is the result from the Lint tool:\nUnsupported language or unknown file extension for ${filename}.` };
}
if (!linterCommand) {
return { result: `You asked to lint ${filename} and this is the result from the Linter:\nInternal error: Could not determine linter command for language ${language}.` };
}
// Determine working directory:
// For JavaScript, we MUST run inside the temp dir to avoid "outside of base path" errors.
// For others, we can stay in the Hub (process.cwd()).
let executionCwd: string | undefined;
let executionEnv: NodeJS.ProcessEnv = process.env;
if (language === 'javascript') {
executionCwd = path.dirname(fullFileName);
const projectNodeModules = path.resolve(process.cwd(), 'node_modules');
executionEnv = {
...process.env, // Inherit existing variables
NODE_PATH: projectNodeModules // Add our fix
};
}
const noWarningsOrErrors = "No warnings or errors were found.";
const resultPrefix = `You asked to lint ${filename} and this is the result from the Linter:`;
// Execute the linter command
let output = await processExecution(
linterCommand,
linterArgs,
filename,
fullFileName,
resultPrefix,
noWarningsOrErrors,
executionCwd, // Pass the new CWD
executionEnv
);
// We use a Regex to match the dynamic parts:
// 1. \(node:\d+\) matches "(node:30)", "(node:31)", etc.
// 2. [\s\S]*? matches all the content (lines and text) in between non-greedily.
// 3. The end text matches the specific footer of that Node warning.
const warningRegex = /\(node:\d+\) \[MODULE_TYPELESS_PACKAGE_JSON\][\s\S]*?\(Use `node --trace-warnings \.\.\.` to show where the warning was created\)\s*/;
// Replace the regex match with an empty string
output = output.replace(warningRegex, "").trim();
// Check if the output is empty (or just the prefix)
if (output === resultPrefix || output === "") {
// Ensure we handle cases where resultPrefix might not be present if output was entirely the warning
output = resultPrefix ? `${resultPrefix}\n${noWarningsOrErrors}` : noWarningsOrErrors;
}
context.sendMessage({
type: 'PROGRESS_UPDATE',
message: output,
});
return { result: output };
} catch (error) {
console.error(`LintTool execution failed for ${filename}:`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
return { result: `You asked to lint ${filename} and this is the result from the Linter:\nError during execution: ${errorMessage}` };
} finally {
// --- Cleanup Logic ---
try {
// We should ideally remove all files we created, but removing the sessionDir
// recursively handles everything inside it.
const sessionDir = path.dirname(fullFileName);
await fs.rm(sessionDir, { recursive: true, force: true });
const namespaceDir = path.dirname(sessionDir);
await fs.rmdir(namespaceDir).catch(() => {});
} catch (cleanupError) {
// Ignore errors during cleanup (e.g. file already gone, dir not empty)
}
}
},
/**
* Extract parameters from the tool invocation string.
* @param invocation The string used to invoke the tool.
* @returns The parameter names and corresponding values.
*/
async extractParameters(invocation: string, _context: MultiAgentToolContext): Promise<ToolParsingResult> {
if (invocation.trim().endsWith("}")) {
const filename = invocation.trim().slice(0, -1).trim();
return {
success: true,
params: {
filename
}
};
} else {
return {
success: false,
error: `Invalid syntax for the ${this.displayName} Tool. Make sure you include the curly brackets.`
}
}
}
}