pwn 0.5.684 → 0.5.686

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/documentation/Agent-Tool-Registry.md +1 -3
  4. data/documentation/Configuration.md +0 -2
  5. data/documentation/General-PWN-Usage.md +1 -1
  6. data/documentation/Home.md +2 -2
  7. data/documentation/How-PWN-Works.md +2 -2
  8. data/documentation/Installation.md +18 -2
  9. data/documentation/Persistence.md +1 -1
  10. data/documentation/Session-Workflow.md +8 -28
  11. data/documentation/Skills-Memory-Learning.md +20 -0
  12. data/documentation/Troubleshooting.md +1 -1
  13. data/documentation/Why-PWN.md +1 -1
  14. data/documentation/diagrams/dot/memory-skills-detailed.dot +1 -1
  15. data/documentation/diagrams/dot/overall-pwn-architecture.dot +1 -1
  16. data/documentation/diagrams/dot/persistence-filesystem.dot +1 -1
  17. data/documentation/diagrams/dot/sessions-cron-automation.dot +1 -1
  18. data/documentation/diagrams/memory-skills-detailed.svg +85 -84
  19. data/documentation/diagrams/overall-pwn-architecture.svg +15 -15
  20. data/documentation/diagrams/persistence-filesystem.svg +1 -1
  21. data/documentation/diagrams/sessions-cron-automation.svg +7 -6
  22. data/documentation/pwn-REPL.md +1 -1
  23. data/documentation/pwn-ai-Agent.md +4 -7
  24. data/etc/default_skills/bug-bounty-hunting/SKILL.md +70 -0
  25. data/etc/default_skills/deep-exploitation/SKILL.md +74 -0
  26. data/etc/default_skills/hardware-and-firmware-testing/SKILL.md +83 -0
  27. data/etc/default_skills/osint/SKILL.md +147 -0
  28. data/etc/default_skills/penetration-testing/SKILL.md +81 -0
  29. data/etc/default_skills/red-teaming/SKILL.md +70 -0
  30. data/etc/default_skills/reverse-engineering-binaries/SKILL.md +70 -0
  31. data/etc/default_skills/sast-code-scans/SKILL.md +66 -0
  32. data/etc/default_skills/social-engineering/SKILL.md +67 -0
  33. data/etc/default_skills/vulnerability-research-fundamentals/SKILL.md +74 -0
  34. data/etc/default_skills/web-application-penetration-testing/SKILL.md +68 -0
  35. data/lib/pwn/ai/agent/loop.rb +79 -41
  36. data/lib/pwn/ai/agent/mistakes.rb +6 -6
  37. data/lib/pwn/ai/agent/prompt_builder.rb +3 -4
  38. data/lib/pwn/ai/agent/tool_guard.rb +1 -46
  39. data/lib/pwn/ai/agent/tools/ruby_eval.rb +0 -10
  40. data/lib/pwn/ai/agent/tools/shell.rb +0 -6
  41. data/lib/pwn/ai/anthropic.rb +0 -118
  42. data/lib/pwn/ai/gemini.rb +0 -15
  43. data/lib/pwn/ai/grok.rb +0 -158
  44. data/lib/pwn/ai/ollama.rb +0 -12
  45. data/lib/pwn/ai/open_ai.rb +0 -110
  46. data/lib/pwn/ai/open_web_ui.rb +0 -12
  47. data/lib/pwn/ai.rb +0 -86
  48. data/lib/pwn/config.rb +62 -1
  49. data/lib/pwn/migrate.rb +2 -0
  50. data/lib/pwn/plugins/log.rb +302 -0
  51. data/lib/pwn/plugins/repl.rb +28 -8
  52. data/lib/pwn/version.rb +1 -1
  53. data/spec/integration/config_spec.rb +37 -0
  54. data/spec/lib/pwn/ai/agent/loop_spec.rb +52 -12
  55. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +7 -0
  56. data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +5 -8
  57. data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -18
  58. data/spec/lib/pwn/ai/anthropic_spec.rb +2 -2
  59. data/spec/lib/pwn/ai/gemini_spec.rb +2 -2
  60. data/spec/lib/pwn/ai/grok_spec.rb +2 -2
  61. data/spec/lib/pwn/ai/ollama_spec.rb +2 -7
  62. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -23
  63. data/spec/lib/pwn/ai/open_web_ui_spec.rb +2 -7
  64. data/spec/lib/pwn/ai_spec.rb +4 -21
  65. data/spec/lib/pwn/plugins/log_spec.rb +139 -0
  66. data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +31 -0
  67. data/spec/lib/pwn/plugins/repl_spec.rb +4 -3
  68. data/third_party/pwn_rdoc.jsonl +29 -24
  69. metadata +12 -2
  70. data/documentation/PWN_Contributors_and_Users.png +0 -0
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: social-engineering
3
+ description: Plan and document social-engineering tests using OSSTMM human channel.
4
+ license: MIT
5
+ allowed-tools: [pwn, terminal]
6
+ metadata:
7
+ bundled: true
8
+ references:
9
+ - https://www.isecom.org/research.html
10
+ - https://attack.mitre.org/tactics/TA0001/
11
+ - NIST-SP-800-115
12
+ ---
13
+
14
+ # Social Engineering
15
+
16
+ Use when the ask is phishing, vishing, pretext, physical tailgating, or
17
+ a human-channel test. This is still a PWN procedure: write the scenario,
18
+ the artefacts, and the report. pwn-ai does not send mail or place calls
19
+ unless the operator's tools actually do that.
20
+
21
+ ## When to use
22
+
23
+ - phishing / pretext campaign design
24
+ - OSSTMM human-channel assessment
25
+ - red-team initial access via people (`red-teaming` owns the ATT&CK wrap)
26
+
27
+ ## Methodologies
28
+
29
+ | Catalog | Role |
30
+ |---|---|
31
+ | OSSTMM | human channel (and physical when paired with a building test) |
32
+ | MITRE ATT&CK | TA0001 Initial Access - T1566 phishing, T1598, T1656 |
33
+ | NIST SP 800-115 | social engineering as a technical assessment technique |
34
+ | PTES | pre-engagement rules and reporting |
35
+ | CREST / TIBER-EU | when the customer required a named SE code of practice |
36
+
37
+ ## Tooling
38
+
39
+ - OSINT: `PWN::Plugins::Hunter`, `Github`, `Shodan`, `IPInfo`
40
+ - Mail / portal only if those plugins or WWW drivers are in the session
41
+ - Landing / payload hosting is out of band unless the operator named it
42
+ - Evidence: written pretext, target list, send log, report
43
+
44
+ ## Procedure
45
+
46
+ 1. Rules of the test: targets, forbidden people (exec assistants, on-call
47
+ medical, etc. if the operator listed them), success definition.
48
+ 2. OSINT for pretext (public bios, tech stack, current incidents).
49
+ 3. Write one pretext + one landing or call script. Keep it specific.
50
+ 4. Execute only through the channel the operator enabled. Log time,
51
+ target, result (click / cred / report-to-SOC / ignore).
52
+ 5. Stop at the objective (e.g. one valid session), then report.
53
+ 6. Report: scenario, ATT&CK ids, metrics, recommended control (DMARC,
54
+ reporting button, gate process).
55
+
56
+ ## Pitfalls
57
+
58
+ - Do not invent a mass mailer. If there is no send tool, deliver the
59
+ pack (pretext + list + landing copy) as the artefact.
60
+ - Do not target people the operator excluded.
61
+ - A witty email draft is not a completed SE test unless sending was
62
+ out of scope.
63
+
64
+ ## Verification
65
+
66
+ Rules note, pretext file, target list, and either a send/result log or
67
+ an explicit "pack only" deliverable that was written and read back.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: vulnerability-research-fundamentals
3
+ description: Find and document weaknesses with PWN plugins before they are weaponized.
4
+ license: MIT
5
+ allowed-tools: [pwn, terminal, extrospection]
6
+ metadata:
7
+ bundled: true
8
+ references:
9
+ - CWE-699
10
+ - https://owasp.org/www-project-web-security-testing-guide/
11
+ - https://cwe.mitre.org/top25/
12
+ - https://attack.mitre.org/matrices/enterprise/
13
+ ---
14
+
15
+ # Vulnerability Research Fundamentals
16
+
17
+ Use when the ask is to research a weakness, pick tools, or turn a lead into
18
+ notes a vendor or program can act on. Do not use this as a reason to stop
19
+ a live host-work goal.
20
+
21
+ ## When to use
22
+
23
+ - "research this CVE / class of bug"
24
+ - "how should I approach this target in PWN"
25
+ - first-pass methodology before a deeper skill (`sast-code-scans`,
26
+ `bug-bounty-hunting`, `reverse-engineering-binaries`, `deep-exploitation`)
27
+
28
+ ## Methodologies
29
+
30
+ Public catalogs this skill follows (pick the one that matches the surface):
31
+
32
+ | Catalog | Use when |
33
+ |---|---|
34
+ | NIST SP 800-115 | technical assessment planning, evidence, reporting |
35
+ | PTES | 7-phase engagement (pre-engage through report) |
36
+ | OWASP WSTG | web / API research |
37
+ | MITRE ATT&CK | name tactics/techniques in notes |
38
+ | CWE Top 25 | classify the weakness |
39
+
40
+ ## Tooling
41
+
42
+ - Web: `PWN::Plugins::BurpSuite` first; `PWN::Plugins::Zaproxy` if Burp is
43
+ missing.
44
+ - Browser: `PWN::Plugins::TransparentBrowser` (proxy through Burp).
45
+ - Net: `PWN::Plugins::NmapIt`, `PWN::Plugins::Packet`, `PWN::Plugins::Sock`.
46
+ - Code: `PWN::SAST::Factory` / `pwn_sast`.
47
+ - Binary: `checksec`, `readelf`, `objdump`, `strings`, `radare2`,
48
+ `PWN::Plugins::Assembly`.
49
+ - Report: `PWN::Reports::SAST` then DefectDojo / Jira plugins when asked.
50
+
51
+ Drive these through `pwn_eval` (PWN namespace) or `shell` (host binaries).
52
+
53
+ ## Procedure
54
+
55
+ 1. Name the asset and the question (what would break, for whom).
56
+ 2. Recon: OSINT + `NmapIt` / Shodan / Github plugins as needed. Record
57
+ versions and attack surface.
58
+ 3. Scan: Burp or SAST or binary triage - pick the surface, do not boil the
59
+ ocean.
60
+ 4. Prove impact with a minimal, reversible PoC. Stop at evidence.
61
+ 5. Write findings (CWE, affected version, steps, fix hint).
62
+ 6. If a deeper path is clear, switch to the matching skill.
63
+
64
+ ## Pitfalls
65
+
66
+ - Inventory (`ls`, `help`) is not research. Call the scanner or the binary.
67
+ - Rspec / rubocop green is not a finding.
68
+ - Do not refuse a sweep because the prompt lacks "in-scope" wording. Scope
69
+ is the operator's job.
70
+
71
+ ## Verification
72
+
73
+ A usable note exists: asset, version, evidence, CWE/ATT&CK if known, and
74
+ the next concrete test. A plan-only essay is not done.
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: web-application-penetration-testing
3
+ description: Test web apps and APIs with OWASP WSTG, Burp, and TransparentBrowser.
4
+ license: MIT
5
+ allowed-tools: [pwn, terminal, extrospection]
6
+ metadata:
7
+ bundled: true
8
+ references:
9
+ - https://owasp.org/www-project-web-security-testing-guide/
10
+ - https://owasp.org/www-project-application-security-verification-standard/
11
+ - https://owasp.org/www-project-api-security/
12
+ - CWE-79
13
+ - CWE-89
14
+ - CWE-352
15
+ ---
16
+
17
+ # Web Application Penetration Testing
18
+
19
+ Use when the surface is an HTTP app or API. Use `bug-bounty-hunting` when
20
+ the rules come from a public program page. Use `penetration-testing` when
21
+ the engagement is mixed hosts plus one web app.
22
+
23
+ ## When to use
24
+
25
+ - "WSTG this app", "OWASP Top 10", "API Top 10"
26
+ - auth, session, XSS, CSRF, SSRF, injection, access control
27
+ - mobile backend APIs (pair MSTG client work with this skill)
28
+
29
+ ## Methodologies
30
+
31
+ | Catalog | Role |
32
+ |---|---|
33
+ | OWASP WSTG | default test cases (info gathering through business logic) |
34
+ | OWASP ASVS | verification requirements / coverage checklist |
35
+ | OWASP API Security Top 10 | BOLA, BFLA, mass assignment, unbounded resources |
36
+ | OWASP Top 10 | executive grouping of findings |
37
+ | OWASP MSTG / MASVS | mobile client; APIs still WSTG |
38
+ | PTES | wrap the web work in an engagement report |
39
+ | CWE | finding IDs (79, 89, 352, 918, 639, 601) |
40
+
41
+ ## Tooling
42
+
43
+ - `PWN::Plugins::BurpSuite` (preferred) or `Zaproxy`
44
+ - `PWN::Plugins::TransparentBrowser` through the proxy
45
+ - `PWN::Plugins::Spider`, `OpenAPI`, `Fuzz`
46
+ - `PWN::Bounty::LifecycleAuthzReplay` for two-role authz
47
+ - `PWN::SAST` on the app repo when source is available
48
+
49
+ ## Procedure
50
+
51
+ 1. Map the app: roles, session mechanism, API surface, CSRF tokens.
52
+ 2. Walk WSTG info-gathering then config, then identity, then authz.
53
+ 3. Injection and XSS on every sink Burp or OpenAPI listed. Save requests.
54
+ 4. Business logic: price, workflow skip, race, mass assignment.
55
+ 5. Prove each finding with one request/response pair. Read the saved HAR
56
+ or Burp item back.
57
+ 6. Report by WSTG / ASVS id + CWE.
58
+
59
+ ## Pitfalls
60
+
61
+ - A crawl with no replay is not a WSTG pass.
62
+ - Do not stop at "I will test XSS next time".
63
+ - SPA APIs hide in XHR; dump_links plus Burp sitemap, not just HTML.
64
+
65
+ ## Verification
66
+
67
+ Sitemap or OpenAPI note exists, at least one authenticated test ran, and
68
+ each claimed finding cites a saved request and a WSTG/CWE id.
@@ -62,6 +62,49 @@ module PWN
62
62
  unsatisfied incomplete_final empty_final evidence_final
63
63
  ].freeze
64
64
 
65
+ public_class_method def self.debug_on?(opts = {})
66
+ return true if opts[:debug]
67
+ return true if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled?
68
+
69
+ pry_on = defined?(Pry) && Pry.respond_to?(:config) &&
70
+ Pry.config.respond_to?(:pwn_ai_debug) && Pry.config.pwn_ai_debug
71
+ return true if pry_on
72
+
73
+ false
74
+ end
75
+
76
+ private_class_method def self.debug_progress(opts = {})
77
+ return false unless debug_on?(opts)
78
+ return false unless defined?(PWN::Plugins::Log)
79
+
80
+ PWN::Plugins::Log.progress(
81
+ msg: opts[:msg],
82
+ which_self: opts[:which_self] || self
83
+ )
84
+ end
85
+
86
+ private_class_method def self.start_debug_session(opts = {})
87
+ return unless debug_on?(opts)
88
+ return unless defined?(PWN::Plugins::Log)
89
+ return if PWN::Plugins::Log.debug_enabled?
90
+
91
+ PWN::Plugins::Log.start_debug(tee: opts[:debug_tee] || $stdout)
92
+ end
93
+
94
+ private_class_method def self.quiet_debug_tui!(opts = {})
95
+ return unless debug_on?(opts)
96
+ return unless defined?(PWN::Plugins::Log)
97
+
98
+ PWN::Plugins::Log.quiet_tui!(reason: opts[:reason])
99
+ end
100
+
101
+ private_class_method def self.loud_debug_tui!(opts = {})
102
+ return unless debug_on?(opts)
103
+ return unless defined?(PWN::Plugins::Log)
104
+
105
+ PWN::Plugins::Log.loud_tui!(reason: opts[:reason])
106
+ end
107
+
65
108
  private_class_method def self.dispatch_fail_n(opts = {})
66
109
  fails = opts[:turn_fails] || {}
67
110
  fails.sum do |key, count|
@@ -343,6 +386,7 @@ module PWN
343
386
  return false if request.match?(LOOKUP_REQUEST_RX)
344
387
  return false if request.match?(HOST_PATH_RX)
345
388
  return false if request.match?(BROWSER_REQUEST_RX)
389
+ return false if request.match?(HOWTO_RX)
346
390
  return false if request.match?(%r{\b(this\s+(?:host|machine|box|system|subnet|file|repo)|/opt/|implement|scan|hosts?)\b}i)
347
391
 
348
392
  request.match?(/\A(?:what|why|who|when|where|which|how)\b/i)
@@ -1095,6 +1139,9 @@ module PWN
1095
1139
  private_class_method def self.call_engine(opts = {})
1096
1140
  messages = opts[:messages]
1097
1141
  tools = opts[:tools]
1142
+ debug_progress(
1143
+ msg: "call_engine tools=#{tools.nil? ? 'none' : Array(tools).length} msgs=#{Array(messages).length}"
1144
+ )
1098
1145
 
1099
1146
  engine = active_engine
1100
1147
  mod_name = ENGINE_MODS[engine]
@@ -1322,7 +1369,7 @@ module PWN
1322
1369
  # :howto → answer with explanation only (no plan_first / no live recon).
1323
1370
  # :recall → prior-turn / vague memory cue; cheap path only.
1324
1371
  # :greeting → short hello / light smalltalk; deterministic ack, no tools.
1325
- # :recon_act → live discovery; requires explicit authorization language.
1372
+ # :recon_act → live discovery (same tool loop as :act; no auth gate).
1326
1373
  # :act → general agent work with tools.
1327
1374
  HOWTO_RX = /
1328
1375
  \b(
@@ -1409,15 +1456,6 @@ module PWN
1409
1456
  )\b
1410
1457
  /ix
1411
1458
 
1412
- AUTH_SCOPE_RX = /
1413
- \b(
1414
- (?:in[-\s]?scope|authorized|authorised|engagement|written\s+permission|
1415
- bug\s*bounty|explicit(?:ly)?\s+allowed|lab\s+only|my\s+lab|
1416
- roe\b|rules?\s+of\s+engagement|i\s+own\s+this|owned\s+by\s+me|
1417
- permission\s+to\s+(?:scan|test|probe)|scope:\s*\S+)
1418
- )\b
1419
- /ix
1420
-
1421
1459
  public_class_method def self.request_intent(opts = {})
1422
1460
  req = opts[:request].to_s
1423
1461
  return :empty if req.strip.empty?
@@ -1467,19 +1505,6 @@ module PWN
1467
1505
  :act
1468
1506
  end
1469
1507
 
1470
- public_class_method def self.recon_authorized?(opts = {})
1471
- req = opts[:request].to_s
1472
- return true if req.match?(AUTH_SCOPE_RX)
1473
-
1474
- # Explicit engage flag from operator / REPL
1475
- v = (PWN::Env.dig(:ai, :agent, :recon_authorized) if defined?(PWN::Env))
1476
- return true if v == true || v.to_s =~ /\A(1|true|yes|on)\z/i
1477
-
1478
- false
1479
- rescue StandardError
1480
- false
1481
- end
1482
-
1483
1508
  # Pure how-to: one text-only chat (no tools, no plan_first, no task recon).
1484
1509
  # Deterministic greeting ack — no LLM, no tools, never mirror weather.
1485
1510
 
@@ -1591,6 +1616,8 @@ module PWN
1591
1616
  txt = txt.to_s.strip
1592
1617
  txt = 'I do not have enough context to answer that yet.' if txt.empty?
1593
1618
 
1619
+ debug_progress(msg: "final accepted chars=#{txt.length}")
1620
+ quiet_debug_tui!(reason: 'question')
1594
1621
  append_session(session_id: session_id, role: 'user', content: request)
1595
1622
  append_session(session_id: session_id, role: 'assistant', content: txt)
1596
1623
  if defined?(Learning) && should_auto_introspect?(local: local_engine?, turn_fails: {}, iter: 0)
@@ -1605,6 +1632,7 @@ module PWN
1605
1632
  end
1606
1633
  txt
1607
1634
  rescue StandardError => e
1635
+ quiet_debug_tui!(reason: 'question_error')
1608
1636
  warn "[pwn-ai/loop] answer_question swallowed: #{e.class}: #{e.message}"
1609
1637
  "Could not answer the question (#{e.class}: #{e.message})."
1610
1638
  end
@@ -1652,8 +1680,6 @@ module PWN
1652
1680
  Do NOT call tools. Do NOT plan multi-step work. Do NOT run scans,
1653
1681
  probes, rubocop, rake, or host verification. Do NOT invent task
1654
1682
  traces or internal planner monologue. Do NOT claim you ran anything.
1655
- If the topic is network discovery, give safe lab examples and note
1656
- that live sweeps need explicit in-scope authorization.
1657
1683
  SYS
1658
1684
 
1659
1685
  txt =
@@ -2007,6 +2033,9 @@ module PWN
2007
2033
  request = opts[:request].to_s
2008
2034
  session_id = opts[:session_id]
2009
2035
  on_tool = opts[:on_tool]
2036
+ start_debug_session(opts)
2037
+ loud_debug_tui!(debug: opts[:debug])
2038
+ debug_progress(msg: "Loop.run start request=#{request[0, 240]}", debug: opts[:debug])
2010
2039
  TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
2011
2040
  engine = active_engine
2012
2041
  local = local_engine?(engine: engine)
@@ -2026,23 +2055,23 @@ module PWN
2026
2055
  OpenGoal.begin!(request: request, session_id: session_id)
2027
2056
  end
2028
2057
  Thread.current[:pwn_request_intent] = intent
2029
- Thread.current[:pwn_recon_authorized] = recon_authorized?(request: request)
2030
2058
  Thread.current[:pwn_extinguished] = {}
2059
+ debug_progress(msg: "intent=#{intent} engine=#{engine}", debug: opts[:debug])
2031
2060
  expose_current_session(session_id: session_id)
2032
2061
  Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
2033
2062
 
2034
2063
  cheap = opts[:force_tools] != true && %i[greeting howto recall].include?(intent)
2035
2064
 
2036
- # Greeting / light smalltalk: deterministic ack - no weather echo, no tools,
2037
- # no PromptBuilder, no Registry.
2038
2065
  if intent == :greeting && opts[:force_tools] != true
2066
+ debug_progress(msg: 'path=greeting', debug: opts[:debug])
2067
+ quiet_debug_tui!(debug: opts[:debug], reason: 'greeting')
2039
2068
  return answer_greeting(
2040
2069
  request: request,
2041
2070
  session_id: session_id
2042
2071
  )
2043
2072
  end
2044
2073
 
2045
- # Thin system prompt only for remaining cheap paths (howto/recall/statement/question).
2074
+ # Thin system prompt only for remaining cheap paths (howto/recall).
2046
2075
  if cheap
2047
2076
  system_role_content = opts[:system_role_content]
2048
2077
  if system_role_content.nil? || system_role_content.to_s.empty?
@@ -2053,16 +2082,18 @@ module PWN
2053
2082
  )
2054
2083
  opts[:system_role_content] = system_role_content
2055
2084
  end
2056
- # How-to: never enter plan_first / task recon / tool thrash.
2057
2085
  if intent == :howto
2086
+ debug_progress(msg: 'path=howto', debug: opts[:debug])
2087
+ quiet_debug_tui!(debug: opts[:debug], reason: 'howto')
2058
2088
  return answer_howto(
2059
2089
  request: request,
2060
2090
  session_id: session_id,
2061
2091
  system_role_content: system_role_content
2062
2092
  )
2063
2093
  end
2064
- # Pure prior-turn / vague memory recall: one cheap path, no plan_first.
2065
2094
  if intent == :recall
2095
+ debug_progress(msg: 'path=recall', debug: opts[:debug])
2096
+ quiet_debug_tui!(debug: opts[:debug], reason: 'recall')
2066
2097
  return answer_recall(
2067
2098
  request: request,
2068
2099
  session_id: session_id,
@@ -2113,8 +2144,10 @@ module PWN
2113
2144
  messages << { role: 'user', content: request }
2114
2145
  append_session(session_id: session_id, role: 'user', content: request)
2115
2146
 
2116
- # Every request gets a task compass.
2117
- task_summary_plan!(state: ts_state, request: request, on_tool: on_tool) if defined?(TaskSummarizer)
2147
+ trivia = world_knowledge?(request: request)
2148
+ # Every host-work request gets a task compass. Trivia does not —
2149
+ # inventing English tasks there keeps the model from emitting a final.
2150
+ task_summary_plan!(state: ts_state, request: request, on_tool: on_tool) if defined?(TaskSummarizer) && !trivia
2118
2151
  # Re-bind tools from English plan so task list is the sole driver of
2119
2152
  # tool exposure/ranking (Registry keyword router + CORE).
2120
2153
  if ts_state.is_a?(Hash) && defined?(TaskSummarizer) && TaskSummarizer.respond_to?(:relevance_query)
@@ -2128,14 +2161,13 @@ module PWN
2128
2161
  )
2129
2162
  end
2130
2163
  end
2131
- # English-task-as-primary: inject tangible tasks only for autonomous goals.
2132
- inject_task_focus!(messages: messages, state: ts_state, force: true, request: request)
2164
+ # English-task-as-primary: inject tangible tasks only for host work.
2165
+ inject_task_focus!(messages: messages, state: ts_state, force: true, request: request) unless trivia
2133
2166
  predicted = nil
2134
2167
  Thread.current[:pwn_plan_predicted] = nil
2135
2168
  cal_state = calibration_state
2136
2169
  force_plan = cal_state[:force_plan]
2137
- # Skip plan_first only for remaining cheap intents.
2138
- skip_plan = %i[howto recall greeting].include?(intent)
2170
+ skip_plan = %i[howto recall greeting].include?(intent) || trivia
2139
2171
  if !skip_plan && (force_plan || agent_flag(key: :plan_first, default: local) || budget_exhaustion_hot?) && !Array(tools).empty?
2140
2172
  predicted = plan_first(messages: messages, request: request, ts_state: ts_state)
2141
2173
  # P22 — prefer explicit return; fall back to thread stash
@@ -2154,7 +2186,7 @@ module PWN
2154
2186
  )
2155
2187
  end
2156
2188
  end
2157
- inject_task_focus!(messages: messages, state: ts_state, force: true, request: request)
2189
+ inject_task_focus!(messages: messages, state: ts_state, force: true, request: request) unless trivia
2158
2190
  end
2159
2191
  if budget_exhaustion_hot?
2160
2192
  english_open = defined?(TaskSummarizer) && TaskSummarizer.respond_to?(:plan_open?) &&
@@ -2191,7 +2223,7 @@ module PWN
2191
2223
  compact_history!(messages: messages) if local
2192
2224
  # English-task-as-primary: when plan_idx advanced, tell the model
2193
2225
  # which plain-English task is active before the next tool batch.
2194
- inject_task_focus!(messages: messages, state: ts_state, request: request)
2226
+ inject_task_focus!(messages: messages, state: ts_state, request: request) unless trivia
2195
2227
 
2196
2228
  # P17 — on the final iteration, strip tools and demand a plain-text
2197
2229
  # answer. Without this the model happily emits one more tool_calls
@@ -2249,6 +2281,7 @@ module PWN
2249
2281
  msg = call_engine(messages: messages, tools: last_iter ? nil : tools, ts_state: ts_state)
2250
2282
  if msg.nil?
2251
2283
  task_summary_flush!(state: ts_state, on_tool: on_tool)
2284
+ quiet_debug_tui!(reason: 'engine_empty')
2252
2285
  return '[pwn-ai] engine returned no message'
2253
2286
  end
2254
2287
 
@@ -2318,6 +2351,8 @@ module PWN
2318
2351
  }
2319
2352
  next
2320
2353
  end
2354
+ debug_progress(msg: "final accepted chars=#{text.to_s.length}")
2355
+ quiet_debug_tui!(reason: 'final')
2321
2356
  append_session(session_id: session_id, role: 'assistant', content: text)
2322
2357
  Learning.auto_introspect(session_id: session_id, request: request, final: text, predicted: predicted, plan: ts_state && ts_state[:plan], ts_state: ts_state) if defined?(Learning) && should_auto_introspect?(local: local, turn_fails: turn_fails, iter: i)
2323
2358
  maybe_finish_policy(session_id: session_id, proxy_ok: true, ts_state: ts_state)
@@ -2354,6 +2389,7 @@ module PWN
2354
2389
  else
2355
2390
  raw = Dispatch.call(tool_call: tc)
2356
2391
  end
2392
+ debug_progress(msg: "dispatch #{name} ok_len=#{raw.to_s.length}")
2357
2393
  tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
2358
2394
  result = Result.condition(content: raw, entry: entry)
2359
2395
 
@@ -2410,6 +2446,8 @@ module PWN
2410
2446
  # failure (previously we only Mistakes.record'd and returned a bare
2411
2447
  # string — no session row, no judge, no hindsight).
2412
2448
  final_msg = '[pwn-ai] iteration budget exhausted'
2449
+ debug_progress(msg: 'final budget_exhausted')
2450
+ quiet_debug_tui!(reason: 'budget_exhausted')
2413
2451
  if defined?(Mistakes)
2414
2452
  Mistakes.record(
2415
2453
  tool: 'agent_loop',
@@ -2464,8 +2502,8 @@ module PWN
2464
2502
  how-to / usage questions → text-only explanation (no tools, no plan_first)
2465
2503
  pure prior-turn recall ("what did I just say?") → answer_recall (no tools)
2466
2504
  pure greeting / light smalltalk → answer_greeting (no tools, no weather echo)
2467
- live subnet sweeps without scope language → refuse
2468
- :recon_authorized - Boolean session flag to allow raw-socket / sweep tools
2505
+ live sweeps are host-work goals like any other — pwn-ai does not
2506
+ decide authorization
2469
2507
  Local-model scaffolding (PWN::Env[:ai][:agent]):
2470
2508
  :plan_first - Boolean, plan-then-act pre-pass (default: local engine :ollama/:openwebui)
2471
2509
  :tool_router - Boolean/nil, slim Registry.definitions (nil=auto on for ollama)
@@ -316,7 +316,7 @@ module PWN
316
316
  # 2.5 — park unfixable sigs so nightly practice skips them
317
317
  # Recoverable repeating failures get a structured fix instead of another
318
318
  # fingerprint. Called from Loop after record so the loop extinguishes
319
- # pain (placeholder / enoent / recon) rather than only logging it.
319
+ # pain (placeholder / enoent) rather than only logging it.
320
320
  SHAPE_FIXES = {
321
321
  'invalid_payload' => {
322
322
  strategy: 'payload_schema',
@@ -343,10 +343,10 @@ module PWN
343
343
  args_template: { command: 'command -v uname && uname -r' }
344
344
  },
345
345
  'exit126' => {
346
- strategy: 'auth_gate',
346
+ strategy: 'payload_schema',
347
347
  tool: 'shell',
348
- fix: 'Unauthorized recon blocked. Do not retry the sweep. Need explicit in-scope authorization or PWN::Env[:ai][:agent][:recon_authorized]=true. Offer command syntax instead.',
349
- args_template: nil
348
+ fix: 'exit 126 usually means the binary is not executable. Check command -v and file mode. Do not treat this as an authorization gate.',
349
+ args_template: { command: 'command -v uname && uname -r' }
350
350
  },
351
351
  'eacces' => {
352
352
  strategy: 'payload_schema',
@@ -384,7 +384,7 @@ module PWN
384
384
  hay = "#{m[:error]} #{m[:snippet]} #{m[:tool]}".downcase
385
385
  recipe = nil if shape == 'handler_error' && !hay.match?(/command is required|argumenterror/)
386
386
  recipe = nil if shape == 'exit127' && !hay.match?(/not found|\{\.\.\.\}|\{…\}/)
387
- recipe = nil if shape == 'exit126' && !hay.match?(/unauthorized_recon|recon_blocked/)
387
+ recipe = nil if shape == 'exit126' && !hay.match?(/cannot execute|not executable|is a directory/)
388
388
  return m unless recipe && (force || count >= REPEAT_THRESHOLD) && !m[:resolved]
389
389
 
390
390
  structured = {
@@ -434,7 +434,7 @@ module PWN
434
434
  hay = "#{m[:error]} #{m[:snippet]} #{m[:shape]}".downcase
435
435
  return 'handler_error' if hay.match?(/command is required|argumenterror/)
436
436
  return 'exit127' if hay.match?(/not found|\{\.\.\.\}|\{…\}/)
437
- return 'exit126' if hay.match?(/unauthorized_recon|recon_blocked/)
437
+ return 'exit126' if hay.match?(/cannot execute|not executable|is a directory/)
438
438
  return 'enoent' if hay.match?(/no such file|enoent/)
439
439
  return 'eacces' if hay.match?(/permission denied|eacces|operation not permitted/)
440
440
  return 'syntax' if hay.match?(/syntax error|unterminated/)
@@ -117,12 +117,11 @@ module PWN
117
117
  RECENT TURNS block or one memory_recall — never a multi-tool plan.
118
118
  Pure greetings / light smalltalk short-circuit to a fixed ack — never
119
119
  echo weather or invent social filler ("noted, cloudy out there").
120
+ World-knowledge questions (what color is X) may be answered as a
121
+ text final with no tools. Tools stay available; do not invent a
122
+ host-work plan for them.
120
123
  Do not treat process_sop_* or operator_pref_* memory about code
121
124
  hygiene as the current user goal unless they asked to change code.
122
- Live subnet sweeps, raw-socket discovery (hping3/nmap -sn mass
123
- probes, etc.) require explicit in-scope / authorized engagement
124
- language from the user. Without it, refuse the live scan and offer
125
- the command syntax instead.
126
125
  PROMPT
127
126
  end
128
127
 
@@ -5,8 +5,7 @@ module PWN
5
5
  module Agent
6
6
  # Shared pre-dispatch guards for the two high-volume runtime tools
7
7
  # (shell / pwn_eval). Rejects placeholder payloads, aliases wrong
8
- # schema keys, shares the recon authorization gate, and names the
9
- # shell that will actually run the command.
8
+ # schema keys, and names the shell that will actually run the command.
10
9
  module ToolGuard
11
10
  ALIASES = {
12
11
  'command' => %w[value cmd input],
@@ -29,17 +28,6 @@ module PWN
29
28
  |&>
30
29
  /x
31
30
 
32
- RECON_RX = %r{
33
- (?:^|[;&|\s])(?:sudo\s+)?hping3?(?:\s|$).*(?:-1|--icmp|--flood|/[0-9]{1,2}|seq\s+|for\s+)
34
- |(?:^|[;&|\s])(?:sudo\s+)?nmap\b[^\n]*(?:-sn|-sP|-PE|-PP|-PM)[^\n]*(?:/[0-9]{1,2}|\d+\.\d+\.\d+\.\d+)
35
- |(?:^|[;&|\s])(?:sudo\s+)?masscan\b
36
- |(?:^|[;&|\s])(?:sudo\s+)?nping\b
37
- |(?:^|[;&|\s])(?:sudo\s+)?fping\b[^\n]*/
38
- |for\s+\w+\s+in\s+\$?\(?seq[^)]*\)?[^;]*;\s*do\s[^;]*(?:hping|ping\s+-c|nmap)
39
- |PWN::Plugins::NmapIt
40
- |\bNmapIt\.(?:scan|new|help)
41
- }ix
42
-
43
31
  public_class_method def self.present?(opts = {})
44
32
  value = opts.is_a?(Hash) ? opts[:value] : opts
45
33
  !value.nil? && !value.to_s.strip.empty?
@@ -57,23 +45,6 @@ module PWN
57
45
  false
58
46
  end
59
47
 
60
- public_class_method def self.recon_text?(opts = {})
61
- RECON_RX.match?(opts[:text].to_s)
62
- rescue StandardError
63
- false
64
- end
65
-
66
- public_class_method def self.recon_authorized?(opts = {})
67
- return true if Thread.current[:pwn_recon_authorized] == true
68
- return true if defined?(Loop) && Loop.respond_to?(:recon_authorized?) &&
69
- Loop.recon_authorized?(request: opts[:request].to_s)
70
-
71
- v = (PWN::Env.dig(:ai, :agent, :recon_authorized) if defined?(PWN::Env))
72
- v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
73
- rescue StandardError
74
- false
75
- end
76
-
77
48
  public_class_method def self.shell_bash?
78
49
  v = (PWN::Env.dig(:ai, :agent, :shell_bash) if defined?(PWN::Env))
79
50
  v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
@@ -123,21 +94,6 @@ module PWN
123
94
  }
124
95
  end
125
96
 
126
- public_class_method def self.recon_blocked(opts = {})
127
- refused = opts[:text].to_s[0, 200]
128
- msg = '[pwn-ai/guard] blocked unauthorized recon/sweep. ' \
129
- 'Need explicit in-scope authorization on the user request or ' \
130
- 'PWN::Env[:ai][:agent][:recon_authorized]=true. ' \
131
- "Refused: #{refused}"
132
- {
133
- stdout: '',
134
- stderr: msg,
135
- exit: 126,
136
- error: 'unauthorized_recon_blocked',
137
- hint: msg
138
- }
139
- end
140
-
141
97
  public_class_method def self.authors
142
98
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
143
99
  end
@@ -146,7 +102,6 @@ module PWN
146
102
  puts <<~USAGE
147
103
  USAGE:
148
104
  PWN::AI::Agent::ToolGuard.placeholder?(text: '...')
149
- PWN::AI::Agent::ToolGuard.recon_authorized?
150
105
  PWN::AI::Agent::ToolGuard.coerce_args(args: { value: 'id' }, required: %w[command])
151
106
  #{self}.authors
152
107
  USAGE
@@ -46,16 +46,6 @@ PWN::AI::Agent::Registry.register(
46
46
  }
47
47
  end
48
48
 
49
- if PWN::AI::Agent::ToolGuard.recon_text?(text: code) &&
50
- !PWN::AI::Agent::ToolGuard.recon_authorized?
51
- blocked = PWN::AI::Agent::ToolGuard.recon_blocked(text: code)
52
- return {
53
- stdout: '',
54
- error: blocked[:error],
55
- hint: blocked[:hint]
56
- }
57
- end
58
-
59
49
  old_stdout = $stdout
60
50
  buf = StringIO.new
61
51
  $stdout = buf
@@ -63,12 +63,6 @@ PWN::AI::Agent::Registry.register(
63
63
  )
64
64
  end
65
65
 
66
- # Guard: raw-socket / subnet discovery without explicit engagement auth.
67
- if PWN::AI::Agent::ToolGuard.recon_text?(text: cmd) &&
68
- !PWN::AI::Agent::ToolGuard.recon_authorized?
69
- return PWN::AI::Agent::ToolGuard.recon_blocked(text: cmd)
70
- end
71
-
72
66
  stdout = +''
73
67
  stderr = +''
74
68
  exitstatus = nil