module EhcAgent
EhcAgent — build CTF recon prompts and run AiAgent for hint synthesis. Public: AGENT_MODEL, build_prompt, parse_ai_lines, analyze Depends: AiAgent, AiGuard, EhcProbe, TextUtil Tests: test/lib/ehc_agent_test.rb
Constants
- AGENT_MODEL
- MAX_HINT_CHARS
- MAX_HINT_LINES
Public Instance Methods
Source
# File lib/ehc_agent.rb, line 115 def analyze(url:, host:, probe:, insights:, focus:, nick:, challenge: nil, runner: nil, model: AGENT_MODEL) prompt = build_prompt( url: url, host: host, probe: probe, insights: insights, focus: focus, nick: nick, challenge: challenge ) return { success: false, error: prompt } if prompt == AiGuard.rejection_message result = AiAgent.run_agent(prompt, model: model, runner: runner || AiAgent.method(:default_runner)) unless result.success? message = result.text.to_s.strip return { success: false, error: message.empty? ? "AI request failed" : message } end filtered = AiGuard.filter_output(result.text) if filtered == AiGuard.rejection_message return { success: false, error: filtered } end lines = parse_ai_lines(filtered) return { success: false, error: "Could not parse AI hints." } if lines.empty? { success: true, lines: lines, model: result.model || model, duration_s: result.duration_s } end
Source
# File lib/ehc_agent.rb, line 20 def build_prompt(url:, host:, probe:, insights:, focus:, nick:, challenge: nil) if focus && AiGuard.system_request?(focus) return AiGuard.rejection_message end parts = [ "You are a CTF website recon assistant for authorized lab and competition targets.", "Provide passive recon hints only — no exploitation steps, shell commands, or credential theft.", "Target URL: #{url}", "Host: #{host}", "Requested by IRC nick: #{nick}", "" ] challenge_line = challenge_context_line(challenge) if challenge_line parts << challenge_line parts << "" end if focus && !focus.to_s.strip.empty? parts << "Focus area: #{focus.to_s.strip}" parts << "" end parts << "Page signals:" parts.concat(probe_context_lines(probe)) parts << "" if insights.any? parts << "Prior scans on this host (use only if still relevant):" insights.each { |line| parts << "- #{line}" } parts << "" end parts << "Output #{MAX_HINT_LINES} short bullet lines for IRC (prefix each with '- ')." parts << "Cover likely CTF angles: auth, injection, hidden paths, misconfig, comments, headers." parts << "Plain text only — no markdown fences." AiGuard.guard_prompt(parts.join("\n")) end
Source
# File lib/ehc_agent.rb, line 102 def challenge_context_line(challenge) return nil unless challenge.is_a?(Hash) index = challenge[:index] || challenge["index"] title = (challenge[:title] || challenge["title"]).to_s.strip prize = (challenge[:prize_label] || challenge["prize_label"]).to_s.strip return nil if index.nil? || title.empty? line = "CTF challenge (##{index}): #{title}" line = "#{line} — #{prize}" unless prize.empty? line end
Source
# File lib/ehc_agent.rb, line 90 def parse_ai_lines(text) body = text.to_s.gsub(/\A```[^\n]*\n/m, "").gsub(/```\z/, "") lines = body.lines.map(&:strip).reject(&:empty?) bullets = lines.select { |line| line.start_with?("-", "*", "\u2022") } bullets = lines if bullets.empty? bullets.map { |line| line.sub(/\A[-*\u2022]\s*/, "").strip } .reject(&:empty?) .map { |line| TextUtil.truncate(line, MAX_HINT_CHARS) } .first(MAX_HINT_LINES) end
Source
# File lib/ehc_agent.rb, line 62 def probe_context_lines(probe) lines = [] title = probe[:title].to_s.strip description = probe[:description].to_s.strip lines << "Title: #{title}" unless title.empty? lines << "Description: #{description}" unless description.empty? Array(probe[:forms]).each do |form| line = EhcProbe.form_line(form) lines << line if line end Array(probe[:comments]).each do |comment| line = EhcProbe.comment_line(comment) lines << line if line end Array(probe[:path_hints]).each do |path| lines << "Path hint: #{path}" end Array(probe[:external_hosts]).each do |host| lines << "External host: #{host}" end lines end