mirror of
https://github.com/Bw3ll/ShellWasp
synced 2026-06-08 10:31:49 +00:00
8651c88cbf
Adding the ability to get illustrative sample values from offline mode - and generated on the fly via AI - this can help significantly with trying to figure out how to build syscalls.
631 lines
16 KiB
Python
631 lines
16 KiB
Python
from pathlib import Path
|
|
from datetime import datetime
|
|
from contextlib import redirect_stdout
|
|
import io
|
|
import os
|
|
import json
|
|
import time
|
|
import copy
|
|
|
|
from openai import OpenAI
|
|
from openai import BadRequestError, APIConnectionError, APITimeoutError, RateLimitError, InternalServerError
|
|
|
|
from .myKeys import OPENAI_API_KEY
|
|
from .syscallAiPrompts import PROMPT_PREFIX, schema
|
|
|
|
|
|
SAVE_STEM = "calls"
|
|
CURRENT_JSON_NAME = f"{SAVE_STEM}_current.json"
|
|
RESUME_CURRENT = False
|
|
AUTO_SAVE_CURRENT = True
|
|
SHOW_STRUCTURE_FIELDS_IN_TEXT = True
|
|
|
|
MODEL = "gpt-4.1"
|
|
|
|
DEBUG_OUTPUT = False
|
|
JSON_SUBDIR_NAME = "json"
|
|
|
|
|
|
MODEL_PRICING = {
|
|
"gpt-4.1": {
|
|
"input_per_million": 2.0,
|
|
"output_per_million": 8.0
|
|
},
|
|
"gpt-4o": {
|
|
"input_per_million": 2.5,
|
|
"output_per_million": 10.0
|
|
}
|
|
}
|
|
|
|
|
|
runStats = {
|
|
"numCalls": 0,
|
|
"promptTokens": 0,
|
|
"completionTokens": 0,
|
|
"totalTokens": 0,
|
|
"estimatedCost": 0.0
|
|
}
|
|
|
|
|
|
def dprint(*args, **kwargs):
|
|
if DEBUG_OUTPUT:
|
|
print(*args, **kwargs)
|
|
|
|
|
|
def resetRunStats():
|
|
runStats["numCalls"] = 0
|
|
runStats["promptTokens"] = 0
|
|
runStats["completionTokens"] = 0
|
|
runStats["totalTokens"] = 0
|
|
runStats["estimatedCost"] = 0.0
|
|
|
|
|
|
def estimateCostFromCounts(modelName: str, promptTokens: int, completionTokens: int) -> float:
|
|
pricing = MODEL_PRICING[modelName]
|
|
inputCost = (promptTokens / 1_000_000) * pricing["input_per_million"]
|
|
outputCost = (completionTokens / 1_000_000) * pricing["output_per_million"]
|
|
return inputCost + outputCost
|
|
|
|
|
|
def addUsageToRunStats(modelName: str, usageObj) -> float:
|
|
callCost = estimateCostFromCounts(
|
|
modelName,
|
|
usageObj.prompt_tokens,
|
|
usageObj.completion_tokens
|
|
)
|
|
|
|
runStats["numCalls"] += 1
|
|
runStats["promptTokens"] += usageObj.prompt_tokens
|
|
runStats["completionTokens"] += usageObj.completion_tokens
|
|
runStats["totalTokens"] += usageObj.total_tokens
|
|
runStats["estimatedCost"] += callCost
|
|
|
|
return callCost
|
|
|
|
|
|
def printRunStats():
|
|
dprint("\n--- Running Totals ---")
|
|
dprint("OpenAI calls:", runStats["numCalls"])
|
|
dprint("Prompt tokens:", runStats["promptTokens"])
|
|
dprint("Completion tokens:", runStats["completionTokens"])
|
|
dprint("Total tokens:", runStats["totalTokens"])
|
|
dprint(f"Estimated total cost: ${runStats['estimatedCost']:.6f}")
|
|
|
|
|
|
def makeEmptyAggregate():
|
|
return {
|
|
"calls": [],
|
|
"structures": {}
|
|
}
|
|
|
|
|
|
def getWorkingDir():
|
|
return Path.cwd()
|
|
|
|
|
|
def getJsonDir(baseDir=None):
|
|
if baseDir is None:
|
|
baseDir = getWorkingDir()
|
|
|
|
baseDir = Path(baseDir)
|
|
jsonDir = baseDir / JSON_SUBDIR_NAME
|
|
jsonDir.mkdir(parents=True, exist_ok=True)
|
|
return jsonDir
|
|
|
|
|
|
def getTimestamp():
|
|
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
def sanitizeFilenamePiece(text: str) -> str:
|
|
if not text:
|
|
return "noCalls"
|
|
|
|
safeChars = []
|
|
for ch in str(text):
|
|
if ch.isalnum() or ch in ("-", "_"):
|
|
safeChars.append(ch)
|
|
else:
|
|
safeChars.append("_")
|
|
|
|
cleaned = "".join(safeChars).strip("_")
|
|
return cleaned or "noCalls"
|
|
|
|
|
|
def getFirstFuncName(result: dict) -> str:
|
|
calls = result.get("calls", [])
|
|
if not calls:
|
|
return "noCalls"
|
|
|
|
firstName = calls[0].get("ntFunc", "noCalls")
|
|
return sanitizeFilenamePiece(firstName)
|
|
|
|
|
|
def writeTextAtomic(path: Path, text: str):
|
|
tempPath = path.with_suffix(path.suffix + ".tmp")
|
|
tempPath.write_text(text, encoding="utf-8")
|
|
tempPath.replace(path)
|
|
|
|
|
|
def saveJsonFile(path: Path, data: dict):
|
|
jsonText = json.dumps(data, indent=2)
|
|
writeTextAtomic(path, jsonText)
|
|
|
|
|
|
def loadJsonFile(path: Path):
|
|
if not path.exists():
|
|
return None
|
|
|
|
raw = path.read_text(encoding="utf-8")
|
|
|
|
if not raw or not raw.strip():
|
|
dprint(f"Warning: JSON file is empty: {path}")
|
|
return None
|
|
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError as e:
|
|
dprint(f"Warning: invalid JSON in {path}: {e}")
|
|
return None
|
|
|
|
def convertChunkStructuresToDict(result: dict) -> dict:
|
|
if not isinstance(result, dict):
|
|
return {
|
|
"calls": [],
|
|
"structures": {}
|
|
}
|
|
|
|
calls = result.get("calls", [])
|
|
if not isinstance(calls, list):
|
|
calls = []
|
|
|
|
structures = result.get("structures", {})
|
|
structuresDict = {}
|
|
|
|
# Canonical format: dict already
|
|
if isinstance(structures, dict):
|
|
for structId, structDef in structures.items():
|
|
if isinstance(structDef, dict):
|
|
structuresDict[structId] = {
|
|
"type": structDef.get("type", "<unknown>"),
|
|
"fields": structDef.get("fields", [])
|
|
}
|
|
|
|
# Legacy compatibility only: list with embedded "id"
|
|
elif isinstance(structures, list):
|
|
for structEntry in structures:
|
|
if not isinstance(structEntry, dict):
|
|
continue
|
|
|
|
structId = structEntry.get("id")
|
|
if not structId:
|
|
continue
|
|
|
|
structuresDict[structId] = {
|
|
"type": structEntry.get("type", "<unknown>"),
|
|
"fields": structEntry.get("fields", [])
|
|
}
|
|
|
|
return {
|
|
"calls": calls,
|
|
"structures": structuresDict
|
|
}
|
|
|
|
|
|
def normalizeAggregate(data):
|
|
if not isinstance(data, dict):
|
|
return makeEmptyAggregate()
|
|
|
|
if "calls" not in data or not isinstance(data["calls"], list):
|
|
data["calls"] = []
|
|
|
|
if "structures" not in data or not isinstance(data["structures"], dict):
|
|
data["structures"] = {}
|
|
|
|
return data
|
|
|
|
|
|
def loadCurrentAggregate(jsonDir: Path):
|
|
currentPath = jsonDir / CURRENT_JSON_NAME
|
|
loaded = loadJsonFile(currentPath)
|
|
|
|
if loaded is None:
|
|
return makeEmptyAggregate()
|
|
|
|
return normalizeAggregate(loaded)
|
|
|
|
|
|
def saveCurrentAggregate(aggregate: dict, jsonDir: Path):
|
|
currentPath = jsonDir / CURRENT_JSON_NAME
|
|
saveJsonFile(currentPath, aggregate)
|
|
return currentPath
|
|
|
|
|
|
def renderCallsNicerText(result, showStructureFields=True):
|
|
buffer = io.StringIO()
|
|
with redirect_stdout(buffer):
|
|
formatCallsNicer(result, showStructureFields=showStructureFields)
|
|
return buffer.getvalue()
|
|
|
|
|
|
def getNextStructNum(structureDict: dict) -> int:
|
|
maxNum = 0
|
|
|
|
for structId in structureDict.keys():
|
|
if not isinstance(structId, str):
|
|
continue
|
|
|
|
if not structId.startswith("struct"):
|
|
continue
|
|
|
|
suffix = structId[len("struct"):]
|
|
if suffix.isdigit():
|
|
maxNum = max(maxNum, int(suffix))
|
|
|
|
return maxNum + 1
|
|
|
|
|
|
def saveResultsBundle(result, baseDir=None, saveStem=SAVE_STEM, showStructureFields=True):
|
|
"""
|
|
Manual save function.
|
|
Saves:
|
|
- dated JSON snapshot into ./json
|
|
- current working JSON into ./json
|
|
- dated text rendering using formatCallsNicer() into ./json
|
|
"""
|
|
if baseDir is None:
|
|
baseDir = getWorkingDir()
|
|
|
|
jsonDir = getJsonDir(baseDir)
|
|
timestamp = getTimestamp()
|
|
firstFunc = getFirstFuncName(result)
|
|
|
|
datedJsonPath = jsonDir / f"{saveStem}_{firstFunc}_{timestamp}.json"
|
|
currentJsonPath = jsonDir / CURRENT_JSON_NAME
|
|
datedTextPath = jsonDir / f"{saveStem}_{firstFunc}_{timestamp}.txt"
|
|
|
|
saveJsonFile(datedJsonPath, result)
|
|
saveJsonFile(currentJsonPath, result)
|
|
|
|
prettyText = renderCallsNicerText(
|
|
result,
|
|
showStructureFields=showStructureFields
|
|
)
|
|
writeTextAtomic(datedTextPath, prettyText)
|
|
|
|
dprint("\n--- Saved Files ---")
|
|
dprint("Dated JSON:", datedJsonPath)
|
|
dprint("Current JSON:", currentJsonPath)
|
|
dprint("Dated text:", datedTextPath)
|
|
|
|
return {
|
|
"datedJson": datedJsonPath,
|
|
"currentJson": currentJsonPath,
|
|
"datedText": datedTextPath
|
|
}
|
|
|
|
|
|
def mergeChunkResult(aggregate: dict, chunkResult: dict, chunkIndex: int) -> dict:
|
|
"""
|
|
Merge one chunk result into the aggregate JSON.
|
|
|
|
Behavior:
|
|
- preserves call order by appending
|
|
- stores structures in a top-level dictionary keyed by struct ID
|
|
- remaps structure IDs so there are no collisions across chunks
|
|
- updates each push's structureRef accordingly
|
|
- does NOT deduplicate anything
|
|
"""
|
|
|
|
aggregate = normalizeAggregate(aggregate)
|
|
chunkResult = normalizeAggregate(copy.deepcopy(chunkResult))
|
|
|
|
structureIdMap = {}
|
|
nextStructNum = getNextStructNum(aggregate["structures"])
|
|
|
|
for oldId, structEntry in chunkResult["structures"].items():
|
|
newId = f"struct{nextStructNum}"
|
|
nextStructNum += 1
|
|
|
|
structureIdMap[oldId] = newId
|
|
aggregate["structures"][newId] = copy.deepcopy(structEntry)
|
|
|
|
for callEntry in chunkResult["calls"]:
|
|
newCallEntry = copy.deepcopy(callEntry)
|
|
|
|
for pushEntry in newCallEntry.get("pushes", []):
|
|
oldRef = pushEntry.get("structureRef")
|
|
if oldRef:
|
|
pushEntry["structureRef"] = structureIdMap.get(oldRef, oldRef)
|
|
|
|
aggregate["calls"].append(newCallEntry)
|
|
|
|
return aggregate
|
|
|
|
|
|
def formatField(field, nameWidth=24, typeWidth=20):
|
|
fieldName = field.get("fieldName", "<unknown>")
|
|
fieldType = field.get("fieldType", "<unknown>")
|
|
fieldValue = field.get("fieldValue", "<unknown>")
|
|
fieldComment = field.get("fieldComment") or ""
|
|
|
|
line = f"\t\t{fieldName:<{nameWidth}} {fieldType:<{typeWidth}} = {fieldValue}"
|
|
if fieldComment:
|
|
line += f" ; {fieldComment}"
|
|
return line
|
|
|
|
|
|
def formatStructure(structId, structDef):
|
|
if not structDef:
|
|
return
|
|
|
|
structType = structDef.get("type", "<unknown>")
|
|
fields = structDef.get("fields", [])
|
|
|
|
print(f"\tstructure definition: {structType} ({structId})")
|
|
if not fields:
|
|
print("\t\t<no fields>")
|
|
return
|
|
|
|
for field in fields:
|
|
print(formatField(field))
|
|
|
|
|
|
def formatPushEntry(entry, structureMap=None, commentColumn=24, showStructureFields=True):
|
|
value = entry.get("value", "<unknown>")
|
|
comment = entry.get("additionalComment") or ""
|
|
structurePointer = entry.get("structurePointer")
|
|
structureRef = entry.get("structureRef")
|
|
structureValueExpectations = entry.get("structureValueExpectations")
|
|
pointedValue = entry.get("pointedValue")
|
|
|
|
line = f"push {value}"
|
|
if comment:
|
|
padding = max(1, commentColumn - len(value))
|
|
line += (" " * padding) + f"; {comment}"
|
|
print(line)
|
|
|
|
if pointedValue:
|
|
print(f"\tpointed value: {pointedValue}")
|
|
|
|
if structurePointer:
|
|
if structureRef:
|
|
print(f"\tstructure: {structurePointer} ({structureRef})")
|
|
else:
|
|
print(f"\tstructure: {structurePointer}")
|
|
|
|
if structureValueExpectations:
|
|
print(f"\texpected fields: {structureValueExpectations}")
|
|
|
|
if structureRef and structureMap:
|
|
structDef = structureMap.get(structureRef)
|
|
if structDef and showStructureFields:
|
|
formatStructure(structureRef, structDef)
|
|
elif showStructureFields:
|
|
print(f"\tstructure definition: <missing for {structureRef}>")
|
|
|
|
print()
|
|
|
|
|
|
def formatPushes(pushes, structureMap=None, commentColumn=24, showStructureFields=True):
|
|
for entry in pushes:
|
|
formatPushEntry(
|
|
entry,
|
|
structureMap=structureMap,
|
|
commentColumn=commentColumn,
|
|
showStructureFields=showStructureFields
|
|
)
|
|
|
|
|
|
def formatPushesNicer(pushes, structureMap=None, showStructureFields=True):
|
|
formatPushes(
|
|
pushes,
|
|
structureMap=structureMap,
|
|
commentColumn=24,
|
|
showStructureFields=showStructureFields
|
|
)
|
|
|
|
|
|
def formatCalls(result, showStructureFields=True):
|
|
calls = result.get("calls", [])
|
|
structureMap = result.get("structures", {})
|
|
|
|
for idx, call in enumerate(calls, start=1):
|
|
ntFunc = call.get("ntFunc", "<unknown>")
|
|
pushes = call.get("pushes", [])
|
|
|
|
print(f"=== Call {idx}: {ntFunc} ===")
|
|
formatPushes(
|
|
pushes,
|
|
structureMap=structureMap,
|
|
commentColumn=24,
|
|
showStructureFields=showStructureFields
|
|
)
|
|
|
|
|
|
def formatCallsNicer(result, showStructureFields=True):
|
|
formatCalls(result, showStructureFields=showStructureFields)
|
|
|
|
|
|
def runPrompt(myPrompt: str):
|
|
client = OpenAI(api_key=OPENAI_API_KEY)
|
|
|
|
response = client.chat.completions.create(
|
|
model=MODEL,
|
|
messages=[{"role": "user", "content": myPrompt}],
|
|
response_format={
|
|
"type": "json_schema",
|
|
"json_schema": schema
|
|
},
|
|
temperature=0
|
|
)
|
|
|
|
raw = response.choices[0].message.content
|
|
usage = response.usage
|
|
|
|
callCost = addUsageToRunStats(MODEL, usage)
|
|
|
|
dprint("\n--- Token Usage For This Call ---")
|
|
dprint("Prompt tokens:", usage.prompt_tokens)
|
|
dprint("Completion tokens:", usage.completion_tokens)
|
|
dprint("Total tokens:", usage.total_tokens)
|
|
dprint(f"Estimated cost for this call: ${callCost:.6f}")
|
|
|
|
parsed = json.loads(raw)
|
|
|
|
# json.loads() already converts JSON null -> Python None in memory.
|
|
# This additionally converts structures from list form into dict form in memory.
|
|
parsed = convertChunkStructuresToDict(parsed)
|
|
return parsed
|
|
|
|
|
|
def runPromptWithRetry(myPrompt: str, maxRetries=5, baseDelay=3):
|
|
attempt = 0
|
|
|
|
while True:
|
|
try:
|
|
return runPrompt(myPrompt)
|
|
|
|
except KeyboardInterrupt:
|
|
raise
|
|
|
|
except BadRequestError:
|
|
raise
|
|
|
|
except (APIConnectionError, APITimeoutError, RateLimitError, InternalServerError) as e:
|
|
attempt += 1
|
|
if attempt > maxRetries:
|
|
dprint(f"Giving up after {maxRetries} retries: {e}")
|
|
raise
|
|
|
|
delay = baseDelay * (2 ** (attempt - 1))
|
|
dprint(f"Retryable OpenAI error on attempt {attempt}/{maxRetries}: {e}")
|
|
dprint(f"Sleeping {delay} seconds before retry...")
|
|
time.sleep(delay)
|
|
|
|
except Exception as e:
|
|
attempt += 1
|
|
if attempt > maxRetries:
|
|
dprint(f"Giving up after {maxRetries} retries: {e}")
|
|
raise
|
|
|
|
delay = baseDelay * (2 ** (attempt - 1))
|
|
dprint(f"Unexpected error on attempt {attempt}/{maxRetries}: {e}")
|
|
dprint(f"Sleeping {delay} seconds before retry...")
|
|
time.sleep(delay)
|
|
|
|
|
|
def chunkList(items, chunkSize):
|
|
for i in range(0, len(items), chunkSize):
|
|
yield items[i:i + chunkSize]
|
|
|
|
|
|
def buildChunkPrompt(promptPrefix: str, apiChunk: list[str]) -> str:
|
|
return promptPrefix.rstrip() + "\n\n" + "\n\n".join(apiChunk)
|
|
|
|
|
|
def processApiBlocksInChunks(
|
|
apiBlocks: list[str],
|
|
chunkSize: int,
|
|
promptPrefix: str = PROMPT_PREFIX,
|
|
resumeCurrent: bool = RESUME_CURRENT,
|
|
autoSaveCurrent: bool = AUTO_SAVE_CURRENT,
|
|
baseDir=None,
|
|
debugOutput: bool = DEBUG_OUTPUT
|
|
):
|
|
global DEBUG_OUTPUT
|
|
DEBUG_OUTPUT = debugOutput
|
|
|
|
if baseDir is None:
|
|
baseDir = getWorkingDir()
|
|
|
|
baseDir = Path(baseDir)
|
|
baseDir.mkdir(parents=True, exist_ok=True)
|
|
jsonDir = getJsonDir(baseDir)
|
|
|
|
if resumeCurrent:
|
|
aggregate = loadCurrentAggregate(jsonDir)
|
|
dprint(f"\n--- Resuming from existing current JSON: {jsonDir / CURRENT_JSON_NAME} ---\n")
|
|
else:
|
|
aggregate = makeEmptyAggregate()
|
|
dprint(f"\n--- Starting fresh; current JSON will be overwritten: {jsonDir / CURRENT_JSON_NAME} ---\n")
|
|
if autoSaveCurrent:
|
|
saveCurrentAggregate(aggregate, jsonDir)
|
|
|
|
for chunkIndex, apiChunk in enumerate(chunkList(apiBlocks, chunkSize), start=1):
|
|
chunkPrompt = buildChunkPrompt(promptPrefix, apiChunk)
|
|
|
|
dprint(f"\n=== Processing chunk {chunkIndex} with {len(apiChunk)} API block(s) ===\n")
|
|
|
|
chunkResult = runPromptWithRetry(chunkPrompt)
|
|
|
|
aggregate = mergeChunkResult(
|
|
aggregate=aggregate,
|
|
chunkResult=chunkResult,
|
|
chunkIndex=chunkIndex
|
|
)
|
|
|
|
if autoSaveCurrent:
|
|
currentPath = saveCurrentAggregate(aggregate, jsonDir)
|
|
dprint(f"Updated current JSON: {currentPath}")
|
|
|
|
return aggregate
|
|
|
|
|
|
def buildPossibleValues(
|
|
apiBlocks: list[str],
|
|
chunkSize: int = 1,
|
|
resumeCurrent: bool = RESUME_CURRENT,
|
|
autoSaveCurrent: bool = AUTO_SAVE_CURRENT,
|
|
baseDir=None,
|
|
debugOutput: bool = DEBUG_OUTPUT
|
|
):
|
|
resetRunStats()
|
|
|
|
result = processApiBlocksInChunks(
|
|
apiBlocks=apiBlocks,
|
|
chunkSize=chunkSize,
|
|
promptPrefix=PROMPT_PREFIX,
|
|
resumeCurrent=resumeCurrent,
|
|
autoSaveCurrent=autoSaveCurrent,
|
|
baseDir=baseDir,
|
|
debugOutput=debugOutput
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# if __name__ == "__main__":
|
|
# # from printApiBlocks import api_blocks
|
|
|
|
# # result = buildPossibleValues(
|
|
# # apiBlocks=api_blocks,
|
|
# # chunkSize=1,
|
|
# # resumeCurrent=False,
|
|
# # autoSaveCurrent=True,
|
|
# # baseDir=Path.cwd(),
|
|
# # debugOutput=True
|
|
# # )
|
|
|
|
# dprint("\n--- Pretty JSON (entire result) ---\n")
|
|
# dprint(json.dumps(result, indent=2))
|
|
|
|
# dprint("\n--- Pretty JSON (calls only) ---\n")
|
|
# dprint(json.dumps(result["calls"], indent=2))
|
|
|
|
# dprint("\n--- Assembly Style ---\n")
|
|
# if DEBUG_OUTPUT:
|
|
# formatCalls(result)
|
|
|
|
# dprint("\n--- Assembly Style 2 ---\n")
|
|
# if DEBUG_OUTPUT:
|
|
# formatCallsNicer(result)
|
|
|
|
# printRunStats()
|
|
|
|
# saveResultsBundle(
|
|
# result,
|
|
# baseDir=Path.cwd(),
|
|
# saveStem=SAVE_STEM,
|
|
# showStructureFields=SHOW_STRUCTURE_FIELDS_IN_TEXT
|
|
# ) |