pwn 0.5.673 → 0.5.675
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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/bin/pwn +2 -0
- data/bin/pwn_ai_red_team +7 -2
- data/bin/pwn_setup +25 -2
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/WWW.md +17 -6
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +1 -1
- data/documentation/diagrams/dot/web-application-testing.dot +1 -1
- data/documentation/diagrams/overall-pwn-architecture.svg +1 -1
- data/documentation/diagrams/web-application-testing.svg +1 -1
- data/lib/pwn/ai/agent/loop.rb +10 -0
- data/lib/pwn/ai/agent/reflect.rb +3 -1
- data/lib/pwn/ai/agent/reward.rb +27 -3
- data/lib/pwn/ai/agent/task_summarizer.rb +20 -3
- data/lib/pwn/ai/anthropic.rb +128 -4
- data/lib/pwn/ai/gemini.rb +15 -0
- data/lib/pwn/ai/grok.rb +185 -9
- data/lib/pwn/ai/ollama.rb +30 -5
- data/lib/pwn/ai/open_ai.rb +136 -9
- data/lib/pwn/ai/open_web_ui.rb +12 -0
- data/lib/pwn/ai/red_team/excessive_agency.rb +9 -7
- data/lib/pwn/ai/red_team/insecure_output_handling.rb +9 -7
- data/lib/pwn/ai/red_team/jailbreak.rb +9 -7
- data/lib/pwn/ai/red_team/overreliance.rb +9 -7
- data/lib/pwn/ai/red_team/payload_splitting.rb +9 -7
- data/lib/pwn/ai/red_team/prompt_injection.rb +9 -7
- data/lib/pwn/ai/red_team/sensitive_information_disclosure.rb +9 -7
- data/lib/pwn/ai/red_team/system_prompt_extraction.rb +9 -7
- data/lib/pwn/ai/red_team/test_case_engine.rb +171 -13
- data/lib/pwn/ai/red_team/token_smuggling.rb +9 -7
- data/lib/pwn/ai/red_team.rb +0 -1
- data/lib/pwn/ai.rb +86 -0
- data/lib/pwn/cron.rb +428 -15
- data/lib/pwn/plugins/repl.rb +3 -2
- data/lib/pwn/setup.rb +70 -0
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/github.rb +174 -0
- data/lib/pwn/www.rb +1 -0
- data/spec/documentation/installation_md_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +7 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +6 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +4 -0
- data/spec/lib/pwn/ai/gemini_spec.rb +4 -0
- data/spec/lib/pwn/ai/grok_spec.rb +14 -0
- data/spec/lib/pwn/ai/ollama_spec.rb +30 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +33 -0
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +9 -0
- data/spec/lib/pwn/ai/red_team/excessive_agency_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/insecure_output_handling_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/jailbreak_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/overreliance_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/payload_splitting_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/prompt_injection_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/sensitive_information_disclosure_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/system_prompt_extraction_spec.rb +6 -0
- data/spec/lib/pwn/ai/red_team/test_case_engine_spec.rb +99 -0
- data/spec/lib/pwn/ai/red_team/token_smuggling_spec.rb +6 -0
- data/spec/lib/pwn/ai_spec.rb +23 -0
- data/spec/lib/pwn/cron_spec.rb +143 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +12 -0
- data/spec/lib/pwn/setup_spec.rb +43 -0
- data/spec/lib/pwn/www/github_spec.rb +15 -0
- data/spec/support/sandbox.rb +4 -0
- data/third_party/pwn_rdoc.jsonl +67 -14
- metadata +5 -5
- data/lib/pwn/ai/red_team/model_denial_of_service.rb +0 -89
- data/spec/lib/pwn/ai/red_team/model_denial_of_service_spec.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ed5f171854df678e76d477ad52fee2dd42e1687afc6e950281d2b3e5fe75ab9
|
|
4
|
+
data.tar.gz: 35712ab0300a98de2dfc52430278088561b84fcd15e4dd70a3c95cd90ccf4645
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 563ee1d396728ef91353cfc4e6d82da6b90662d03e55d1c027319203e47a8bd91d9c723a09cda3a6836dc9b1ed380bb3e0bb76d802dd23157ff7c92d52a3958b
|
|
7
|
+
data.tar.gz: 05e3c313c8262273f5799f24a9359b3cadc5448ccebfc5ae031a99a71865b548d24489208f4b7be6e0d4790485ebcc58d35a3ccdafbfddfecdb27eaddd531b9c
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Red teamers, pentesters, and vulnerability researchers get one place to script
|
|
|
34
34
|
and automate instead of gluing together a pile of separate CLIs.
|
|
35
35
|
|
|
36
36
|
**In numbers:** 66 `PWN::Plugins` · 48 `PWN::SAST` rules · 90 `PWN::AWS`
|
|
37
|
-
service wrappers ·
|
|
37
|
+
service wrappers · 22 `PWN::WWW` site drivers · 53 `bin/pwn_*` CLI drivers ·
|
|
38
38
|
6 LLM engines · 13 agent toolsets · 85 LLM-callable tools.
|
|
39
39
|
|
|
40
40
|
Full page: [What is PWN](documentation/What-is-PWN.md)
|
|
@@ -139,7 +139,7 @@ The complete wiki lives in this repo at **[`documentation/Home.md`](documentatio
|
|
|
139
139
|
| [What is PWN](documentation/What-is-PWN.md) | [`pwn` REPL](documentation/pwn-REPL.md) | [AI / LLM Integration](documentation/AI-Integration.md) | [Plugins (66)](documentation/Plugins.md) |
|
|
140
140
|
| [Why PWN](documentation/Why-PWN.md) | [`pwn-ai` Agent](documentation/pwn-ai-Agent.md) | [Agent Tool Registry](documentation/Agent-Tool-Registry.md) | [SAST (48)](documentation/SAST.md) |
|
|
141
141
|
| [How PWN Works](documentation/How-PWN-Works.md) | [CLI Drivers (53)](documentation/CLI-Drivers.md) | [Memory · Skills · Learning](documentation/Skills-Memory-Learning.md) | [AWS (90)](documentation/AWS.md) |
|
|
142
|
-
| [Installation](documentation/Installation.md) | [Build a Driver](documentation/Drivers.md) | [Mistakes (neg-feedback)](documentation/Mistakes.md) | [WWW (
|
|
142
|
+
| [Installation](documentation/Installation.md) | [Build a Driver](documentation/Drivers.md) | [Mistakes (neg-feedback)](documentation/Mistakes.md) | [WWW (22)](documentation/WWW.md) |
|
|
143
143
|
| [General Usage](documentation/General-PWN-Usage.md) | | [Reinforcement Learning](documentation/Reinforcement-Learning.md) | [SDR / Radio](documentation/SDR.md) |
|
|
144
144
|
| [Configuration](documentation/Configuration.md) | | [Extrospection](documentation/Extrospection.md) | [Hardware](documentation/Hardware.md) |
|
|
145
145
|
| [`~/.pwn/` Persistence](documentation/Persistence.md) | | [Swarm (multi-agent)](documentation/Swarm.md) | [Reports](documentation/Reporting.md) |
|
data/bin/pwn
CHANGED
|
@@ -51,9 +51,11 @@ begin
|
|
|
51
51
|
# ------------------------------------------------------------------
|
|
52
52
|
if opts[:setup] == true
|
|
53
53
|
r = PWN::Setup.check
|
|
54
|
+
PWN::Setup.ensure_cron(restart: false)
|
|
54
55
|
exit(r[:ok] ? 0 : 1)
|
|
55
56
|
else
|
|
56
57
|
PWN::Setup.deps(profile: opts[:setup])
|
|
58
|
+
PWN::Setup.ensure_cron(restart: false)
|
|
57
59
|
end
|
|
58
60
|
elsif opts[:ai]
|
|
59
61
|
# ------------------------------------------------------------------
|
data/bin/pwn_ai_red_team
CHANGED
|
@@ -14,6 +14,10 @@ PWN::Driver::Parser.new do |options|
|
|
|
14
14
|
opts[:target_model] = m
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
options.on('-cCOUNT', '--payload-count=COUNT', '<Optional - Number of LLM-generated payloads per test case (Default 10)>') do |c|
|
|
18
|
+
opts[:payload_count] = c
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
options.on('-rROLE', '--system-role=ROLE', '<Optional - System Prompt Applied to the Target for Every Payload>') do |r|
|
|
18
22
|
opts[:system_role_content] = r
|
|
19
23
|
end
|
|
@@ -60,6 +64,7 @@ begin
|
|
|
60
64
|
target_engine = opts[:target_engine]
|
|
61
65
|
target_model = opts[:target_model]
|
|
62
66
|
system_role_content = opts[:system_role_content]
|
|
67
|
+
payload_count = (opts[:payload_count] || 10).to_i
|
|
63
68
|
|
|
64
69
|
max_threads = opts[:max_threads]
|
|
65
70
|
max_threads ||= 5
|
|
@@ -80,7 +85,6 @@ begin
|
|
|
80
85
|
ExcessiveAgency
|
|
81
86
|
InsecureOutputHandling
|
|
82
87
|
Jailbreak
|
|
83
|
-
ModelDenialOfService
|
|
84
88
|
Overreliance
|
|
85
89
|
PayloadSplitting
|
|
86
90
|
PromptInjection
|
|
@@ -115,7 +119,8 @@ begin
|
|
|
115
119
|
).scan(
|
|
116
120
|
target_engine: target_engine,
|
|
117
121
|
target_model: target_model,
|
|
118
|
-
system_role_content: system_role_content
|
|
122
|
+
system_role_content: system_role_content,
|
|
123
|
+
payload_count: payload_count
|
|
119
124
|
)
|
|
120
125
|
|
|
121
126
|
rt_arr.each { |hash_line| mutex.synchronize { results_hash[:data].push(hash_line) } }
|
data/bin/pwn_setup
CHANGED
|
@@ -59,6 +59,18 @@ pwn_driver.auto_opts_help = false
|
|
|
59
59
|
pwn_driver.parse!
|
|
60
60
|
|
|
61
61
|
begin
|
|
62
|
+
status = 0
|
|
63
|
+
# Seed default jobs + start/reuse the in-process scheduler so last_run
|
|
64
|
+
# actually advances. Do this BEFORE the doctor so `pwn setup` reports
|
|
65
|
+
# the worker as running. Skip list/terminal (no host scheduler needed)
|
|
66
|
+
# and dry-run (must not spawn).
|
|
67
|
+
unless opts[:list] || opts[:terminal] || opts[:dry_run]
|
|
68
|
+
PWN::Setup.ensure_cron(
|
|
69
|
+
restart: false,
|
|
70
|
+
dry_run: false
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
62
74
|
if opts[:list]
|
|
63
75
|
PWN::Setup.list_profiles
|
|
64
76
|
elsif opts[:migrate]
|
|
@@ -66,7 +78,7 @@ begin
|
|
|
66
78
|
fix: opts[:fix] || opts[:yes],
|
|
67
79
|
dry_run: opts[:dry_run]
|
|
68
80
|
)
|
|
69
|
-
|
|
81
|
+
status = 1 unless r && r[:after] && r[:after][:incompatible].empty?
|
|
70
82
|
elsif opts[:terminal]
|
|
71
83
|
PWN::Setup.terminal(
|
|
72
84
|
yes: opts[:yes],
|
|
@@ -83,8 +95,19 @@ begin
|
|
|
83
95
|
# `pwn setup --check` → same
|
|
84
96
|
# `pwn setup --check --profile net` → gate ONLY on the :net profile
|
|
85
97
|
r = PWN::Setup.check(profile: opts[:profile])
|
|
86
|
-
|
|
98
|
+
status = 1 unless r[:ok]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Seed default jobs + start/reuse the in-process scheduler so
|
|
102
|
+
# last_run actually advances. Skip list/terminal (no host scheduler
|
|
103
|
+
# needed) and dry-run (must not spawn).
|
|
104
|
+
unless opts[:list] || opts[:terminal] || opts[:dry_run]
|
|
105
|
+
PWN::Setup.ensure_cron(
|
|
106
|
+
restart: false,
|
|
107
|
+
dry_run: false
|
|
108
|
+
)
|
|
87
109
|
end
|
|
110
|
+
exit status
|
|
88
111
|
rescue Interrupt
|
|
89
112
|
puts "\naborted."
|
|
90
113
|
exit 130
|
data/documentation/Home.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **PWN** (/pōn/) - an open-source offensive-security automation framework and
|
|
4
4
|
> continuous-security-integration platform written in Ruby.
|
|
5
|
-
> 66 plugins · 48 SAST rules · 90 AWS wrappers ·
|
|
5
|
+
> 66 plugins · 48 SAST rules · 90 AWS wrappers · 22 WWW drivers · 53 CLI
|
|
6
6
|
> drivers · 6 LLM engines · a self-improving multi-agent AI · one REPL.
|
|
7
7
|
|
|
8
8
|
**Repo root:** `/opt/pwn` · **This wiki:** `/opt/pwn/documentation/` ·
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
| ↳ [Fuzzing](Fuzzing.md) | `Fuzz` · `Sock` · `Packet` |
|
|
58
58
|
| ↳ [Hardware](Hardware.md) | Serial · BusPirate · MSR206 · Android · BareSIP |
|
|
59
59
|
| [SAST (48 rules)](SAST.md) | Static analysis + test-case engine |
|
|
60
|
-
| [WWW (
|
|
60
|
+
| [WWW (22 drivers)](WWW.md) | Browser automations for real sites |
|
|
61
61
|
| [AWS (90 services)](AWS.md) | Cloud-security enumeration |
|
|
62
62
|
| [SDR](SDR.md) | GQRX · FlipperZero · RFIDler · SonMicro · **Decoder::* (20 protocols + Base/DSP)** · FFI-accelerated DSP |
|
|
63
63
|
| [Blockchain](Blockchain.md) | BTC · ETH helpers |
|
|
@@ -45,7 +45,7 @@ call, and [Reinforcement Learning](Reinforcement-Learning.md) for how
|
|
|
45
45
|
|
|
46
46
|
## L3 - Capability namespaces (`lib/pwn/*`)
|
|
47
47
|
|
|
48
|
-
`Plugins` (66) · `SAST` (48) · `WWW` (
|
|
48
|
+
`Plugins` (66) · `SAST` (48) · `WWW` (22) · `AWS` (90) · `SDR` · `Blockchain` ·
|
|
49
49
|
`Bounty` · `Reports` · `FFI` · `Banner` · **`Setup`** · **`Migrate`**. Each is
|
|
50
50
|
a plain module of `public_class_method def self.x(opts = {})` methods -
|
|
51
51
|
callable the same way from the REPL, from `pwn_eval`, or from a driver.
|
data/documentation/WWW.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `PWN::WWW` - Site-Specific Browser Automations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
22 modules, each a scripted [`TransparentBrowser`](Transparent-Browser.md)
|
|
4
4
|
session against a real site: log in, navigate, scrape, submit.
|
|
5
5
|
Source: `lib/pwn/www/*.rb`.
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ Source: `lib/pwn/www/*.rb`.
|
|
|
9
9
|
| Category | Modules |
|
|
10
10
|
|---|---|
|
|
11
11
|
| **Bug bounty** | `HackerOne` · `BugCrowd` · `Synack` · `AppCobaltIO` |
|
|
12
|
+
| **Code hosts** | `GitHub` |
|
|
12
13
|
| **Search / OSINT** | `Google` · `Bing` · `DuckDuckGo` · `Torch` · `WaybackMachine` · `Pastebin` · `Checkip` |
|
|
13
14
|
| **Social** | `Twitter` · `Facebook` · `LinkedIn` · `Youtube` · `Pandora` |
|
|
14
15
|
| **Finance / Work** | `CoinbasePro` · `Paypal` · `TradingView` · `Uber` · `Upwork` |
|
|
@@ -16,14 +17,24 @@ Source: `lib/pwn/www/*.rb`.
|
|
|
16
17
|
## Pattern
|
|
17
18
|
|
|
18
19
|
```ruby
|
|
19
|
-
b = PWN::WWW::
|
|
20
|
-
|
|
21
|
-
PWN::WWW::
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
b = PWN::WWW::GitHub.open(browser_type: :headless,
|
|
21
|
+
proxy: 'http://127.0.0.1:8080')
|
|
22
|
+
PWN::WWW::GitHub.login(
|
|
23
|
+
browser_obj: b,
|
|
24
|
+
username: '...',
|
|
25
|
+
password: '...',
|
|
26
|
+
mfa: '123456'
|
|
27
|
+
)
|
|
28
|
+
# scripted navigation
|
|
29
|
+
PWN::WWW::GitHub.logout(browser_obj: b)
|
|
24
30
|
PWN::Plugins::TransparentBrowser.close(browser_obj: b)
|
|
25
31
|
```
|
|
26
32
|
|
|
33
|
+
`login` takes `username`, `password`, and an MFA token (`mfa:` or
|
|
34
|
+
`mfa_token:`). Pass the token string to stay non-interactive, or set
|
|
35
|
+
`mfa: true` to prompt. The browser stays on the post-auth GitHub session
|
|
36
|
+
when the method returns.
|
|
37
|
+
|
|
27
38
|
Because traffic goes through TransparentBrowser, you can point `proxy:` at
|
|
28
39
|
[BurpSuite](BurpSuite.md) and passively capture every request the automation
|
|
29
40
|
makes.
|
|
@@ -15,7 +15,7 @@ with a **tool-calling AI agent** on top that can run the same methods.
|
|
|
15
15
|
| `PWN::Plugins::*` | **66** | Wrappers for external and native tooling (Burp, Nmap, Metasploit, Shodan, browsers, serial, ...) |
|
|
16
16
|
| `PWN::SAST::*` | **48** | Static-analysis rules across C/Java/Go/Python/Ruby/Scala/PHP/TS |
|
|
17
17
|
| `PWN::AWS::*` | **90** | One module per AWS service for cloud enumeration |
|
|
18
|
-
| `PWN::WWW::*` | **
|
|
18
|
+
| `PWN::WWW::*` | **22** | Site-specific browser automations (HackerOne, BugCrowd, GitHub, Google, LinkedIn, ...) |
|
|
19
19
|
| `PWN::SDR::*` | **6** (+ **20** protocol decoders + Base/DSP) | GQRX, FlipperZero, RFIDler, SonMicro, band tables, `Decoder::{ADSB,POCSAG,RDS,LoRa,...}` |
|
|
20
20
|
| `PWN::FFI::*` | **8** | Native DSP/RF backends: Volk · Liquid · FFTW · RTLSdr · HackRF · AdalmPluto · SoapySDR · Stdio |
|
|
21
21
|
| `PWN::AI::*` | **6** engines | OpenAI, Anthropic, Grok (OAuth device-flow), Gemini, Ollama, Open WebUI |
|
|
@@ -49,7 +49,7 @@ digraph "PWN_Overall_Architecture" {
|
|
|
49
49
|
style=rounded; color="#047857"; bgcolor="#022c22"; penwidth=2;
|
|
50
50
|
Plugins [label="Plugins\n66 modules", fillcolor="#6ee7b7"];
|
|
51
51
|
SAST [label="SAST\n48 rules", fillcolor="#6ee7b7"];
|
|
52
|
-
WWW [label="WWW\
|
|
52
|
+
WWW [label="WWW\n22 site drivers", fillcolor="#6ee7b7"];
|
|
53
53
|
AWS [label="AWS\n90 services", fillcolor="#6ee7b7"];
|
|
54
54
|
SDR [label="SDR\nGQRX · Flipper · RFID\nDecoder::* (20+Base/DSP)", fillcolor="#6ee7b7"];
|
|
55
55
|
Chain [label="Blockchain\nBTC · ETH", fillcolor="#6ee7b7"];
|
|
@@ -17,7 +17,7 @@ digraph "PWN_Web" {
|
|
|
17
17
|
color="#047857"; bgcolor="#022c22";
|
|
18
18
|
TB [label="TransparentBrowser\nheadless · watir · devtools", fillcolor="#6ee7b7"];
|
|
19
19
|
Spider [label="Plugins::Spider\nURI buster", fillcolor="#6ee7b7"];
|
|
20
|
-
WWW [label="PWN::WWW::*\
|
|
20
|
+
WWW [label="PWN::WWW::*\n22 site drivers", fillcolor="#6ee7b7"];
|
|
21
21
|
}
|
|
22
22
|
subgraph cluster_proxy {
|
|
23
23
|
label="Intercept + Scan"; fontcolor="#fde68a"; style=rounded;
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
<title>WWW</title>
|
|
220
220
|
<path fill="#6ee7b7" stroke="#334155" stroke-width="1.4" d="M495.38,-198.88C495.38,-198.88 436.62,-198.88 436.62,-198.88 430.62,-198.88 424.62,-192.88 424.62,-186.88 424.62,-186.88 424.62,-174.88 424.62,-174.88 424.62,-168.88 430.62,-162.88 436.62,-162.88 436.62,-162.88 495.38,-162.88 495.38,-162.88 501.38,-162.88 507.38,-168.88 507.38,-174.88 507.38,-174.88 507.38,-186.88 507.38,-186.88 507.38,-192.88 501.38,-198.88 495.38,-198.88"/>
|
|
221
221
|
<text xml:space="preserve" text-anchor="middle" x="466" y="-183.93" font-family="sans-Serif" font-size="11.00" fill="#0f172a">WWW</text>
|
|
222
|
-
<text xml:space="preserve" text-anchor="middle" x="466" y="-170.43" font-family="sans-Serif" font-size="11.00" fill="#0f172a">
|
|
222
|
+
<text xml:space="preserve" text-anchor="middle" x="466" y="-170.43" font-family="sans-Serif" font-size="11.00" fill="#0f172a">22 site drivers</text>
|
|
223
223
|
</g>
|
|
224
224
|
<!-- Registry->WWW -->
|
|
225
225
|
<g id="edge12" class="edge">
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
<title>WWW</title>
|
|
65
65
|
<path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M256.25,-52C256.25,-52 196,-52 196,-52 190,-52 184,-46 184,-40 184,-40 184,-28 184,-28 184,-22 190,-16 196,-16 196,-16 256.25,-16 256.25,-16 262.25,-16 268.25,-22 268.25,-28 268.25,-28 268.25,-40 268.25,-40 268.25,-46 262.25,-52 256.25,-52"/>
|
|
66
66
|
<text xml:space="preserve" text-anchor="middle" x="226.12" y="-36.5" font-family="sans-Serif" font-size="10.00" fill="#0f172a">PWN::WWW::*</text>
|
|
67
|
-
<text xml:space="preserve" text-anchor="middle" x="226.12" y="-24.5" font-family="sans-Serif" font-size="10.00" fill="#0f172a">
|
|
67
|
+
<text xml:space="preserve" text-anchor="middle" x="226.12" y="-24.5" font-family="sans-Serif" font-size="10.00" fill="#0f172a">22 site drivers</text>
|
|
68
68
|
</g>
|
|
69
69
|
<!-- Target->WWW -->
|
|
70
70
|
<g id="edge3" class="edge">
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -1063,6 +1063,16 @@ module PWN
|
|
|
1063
1063
|
# 3.2 — local models cannot afford auto_introspect (judge+prm+critic+
|
|
1064
1064
|
# sentinel+extro) on every success. Default :failure_only when local.
|
|
1065
1065
|
private_class_method def self.should_auto_introspect?(opts = {})
|
|
1066
|
+
kind = (opts[:kind] || Thread.current[:pwn_request_kind]).to_s.to_sym
|
|
1067
|
+
intent = (opts[:intent] || Thread.current[:pwn_request_intent]).to_s.to_sym
|
|
1068
|
+
fails = opts[:turn_fails].is_a?(Hash) ? opts[:turn_fails].values.sum : 0
|
|
1069
|
+
# Cheap answers already returned user-visible text. The post-answer
|
|
1070
|
+
# critic + 12s ORM printed ERROR: Timed out reading data from server
|
|
1071
|
+
# after greetings / takes / questions.
|
|
1072
|
+
return false if %i[greeting howto recall].include?(intent)
|
|
1073
|
+
return false if kind == :statement
|
|
1074
|
+
return false if kind == :question && fails.zero?
|
|
1075
|
+
|
|
1066
1076
|
return true unless opts[:local]
|
|
1067
1077
|
|
|
1068
1078
|
policy = agent_flag(key: :local_introspect, default: :failure_only).to_s.to_sym
|
data/lib/pwn/ai/agent/reflect.rb
CHANGED
|
@@ -97,7 +97,8 @@ module PWN
|
|
|
97
97
|
system_role_content: system_role_content,
|
|
98
98
|
spinner: spinner,
|
|
99
99
|
timeout: timeout,
|
|
100
|
-
temp: temp
|
|
100
|
+
temp: temp,
|
|
101
|
+
quiet: opts[:quiet]
|
|
101
102
|
)
|
|
102
103
|
end
|
|
103
104
|
ensure
|
|
@@ -132,6 +133,7 @@ module PWN
|
|
|
132
133
|
chat_opts[:timeout] = opts[:timeout] unless opts[:timeout].nil?
|
|
133
134
|
chat_opts[:temp] = opts[:temp] unless opts[:temp].nil?
|
|
134
135
|
chat_opts[:model] = opts[:model] unless opts[:model].to_s.empty?
|
|
136
|
+
chat_opts[:quiet] = opts[:quiet] unless opts[:quiet].nil?
|
|
135
137
|
mod.chat(chat_opts)
|
|
136
138
|
end
|
|
137
139
|
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -1157,11 +1157,20 @@ module PWN
|
|
|
1157
1157
|
spinner: false,
|
|
1158
1158
|
model: judge_model,
|
|
1159
1159
|
timeout: cheap_orm_timeout,
|
|
1160
|
-
temp: CHEAP_ORM_TEMP
|
|
1160
|
+
temp: CHEAP_ORM_TEMP,
|
|
1161
|
+
quiet: true
|
|
1161
1162
|
)
|
|
1162
1163
|
text = extract_chat_text(resp: raw)
|
|
1163
1164
|
return text unless text.strip.empty?
|
|
1164
|
-
|
|
1165
|
+
|
|
1166
|
+
# Quiet ReadTimeout now returns nil instead of raising. That
|
|
1167
|
+
# empty response already spent the cheap-ORM budget — do not
|
|
1168
|
+
# immediately fire engine_chat_cheap (second 12s stall).
|
|
1169
|
+
return nil
|
|
1170
|
+
rescue StandardError => e
|
|
1171
|
+
# A 12s ReadTimeout already spent the cheap-ORM budget.
|
|
1172
|
+
return nil if timeout_error?(err: e)
|
|
1173
|
+
|
|
1165
1174
|
nil
|
|
1166
1175
|
end
|
|
1167
1176
|
end
|
|
@@ -1191,7 +1200,8 @@ module PWN
|
|
|
1191
1200
|
system_role_content: opts[:system_role_content],
|
|
1192
1201
|
spinner: false,
|
|
1193
1202
|
timeout: cheap_orm_timeout,
|
|
1194
|
-
temp: CHEAP_ORM_TEMP
|
|
1203
|
+
temp: CHEAP_ORM_TEMP,
|
|
1204
|
+
quiet: true
|
|
1195
1205
|
}
|
|
1196
1206
|
model = judge_model
|
|
1197
1207
|
chat_opts[:model] = model unless model.to_s.empty?
|
|
@@ -1200,6 +1210,20 @@ module PWN
|
|
|
1200
1210
|
nil
|
|
1201
1211
|
end
|
|
1202
1212
|
|
|
1213
|
+
private_class_method def self.timeout_error?(opts = {})
|
|
1214
|
+
err = opts[:err]
|
|
1215
|
+
return false if err.nil?
|
|
1216
|
+
|
|
1217
|
+
klass = err.class
|
|
1218
|
+
name = klass.name.to_s
|
|
1219
|
+
return true if name.include?('Timeout') || name.include?('ReadTimeout') || name.include?('OpenTimeout')
|
|
1220
|
+
return true if err.message.to_s.match?(/timed out reading|read timeout|open timeout/i)
|
|
1221
|
+
|
|
1222
|
+
false
|
|
1223
|
+
rescue StandardError
|
|
1224
|
+
false
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1203
1227
|
private_class_method def self.cheap_orm_timeout
|
|
1204
1228
|
n = agent_flag(key: :reward_llm_timeout, default: CHEAP_ORM_TIMEOUT).to_i
|
|
1205
1229
|
n = CHEAP_ORM_TIMEOUT if n < 2
|
|
@@ -566,7 +566,9 @@ module PWN
|
|
|
566
566
|
request: user,
|
|
567
567
|
system_role_content: system,
|
|
568
568
|
suppress_pii_warning: true,
|
|
569
|
-
spinner: false
|
|
569
|
+
spinner: false,
|
|
570
|
+
timeout: sidecar_timeout,
|
|
571
|
+
quiet: true
|
|
570
572
|
)
|
|
571
573
|
text = reflect_text(resp: resp)
|
|
572
574
|
return text unless text.to_s.strip.empty?
|
|
@@ -846,7 +848,9 @@ module PWN
|
|
|
846
848
|
request: user,
|
|
847
849
|
system_role_content: system,
|
|
848
850
|
suppress_pii_warning: true,
|
|
849
|
-
spinner: false
|
|
851
|
+
spinner: false,
|
|
852
|
+
timeout: sidecar_timeout,
|
|
853
|
+
quiet: true
|
|
850
854
|
)
|
|
851
855
|
text = reflect_text(resp: resp)
|
|
852
856
|
return text unless text.to_s.strip.empty?
|
|
@@ -896,7 +900,9 @@ module PWN
|
|
|
896
900
|
r = mod.chat(
|
|
897
901
|
request: opts[:request],
|
|
898
902
|
system_role_content: opts[:system_role_content],
|
|
899
|
-
spinner: false
|
|
903
|
+
spinner: false,
|
|
904
|
+
timeout: sidecar_timeout,
|
|
905
|
+
quiet: true
|
|
900
906
|
)
|
|
901
907
|
reflect_text(resp: r)
|
|
902
908
|
rescue StandardError => e
|
|
@@ -911,6 +917,17 @@ module PWN
|
|
|
911
917
|
:ollama
|
|
912
918
|
end
|
|
913
919
|
|
|
920
|
+
# Kind / plan classification is a sidecar hop. Bound it so a hung
|
|
921
|
+
# model cannot stall the user's real answer, and stay quiet on timeout.
|
|
922
|
+
private_class_method def self.sidecar_timeout
|
|
923
|
+
n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
|
|
924
|
+
n = 20 if n < 5
|
|
925
|
+
n = 60 if n > 60
|
|
926
|
+
n
|
|
927
|
+
rescue StandardError
|
|
928
|
+
20
|
|
929
|
+
end
|
|
930
|
+
|
|
914
931
|
# Parse JSON array, fenced JSON, or numbered/bulleted plain text.
|
|
915
932
|
#
|
|
916
933
|
# Supported Method Parameters::
|
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -362,7 +362,7 @@ module PWN
|
|
|
362
362
|
real_config_value?(value: oauth[:bearer_token]) ||
|
|
363
363
|
real_config_value?(value: oauth[:refresh_token])
|
|
364
364
|
|
|
365
|
-
if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key]))
|
|
365
|
+
if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key])) && !opts[:non_interactive]
|
|
366
366
|
token = obtain_oauth_bearer_token(oauth)
|
|
367
367
|
using_oauth = true
|
|
368
368
|
end
|
|
@@ -372,9 +372,15 @@ module PWN
|
|
|
372
372
|
using_oauth = false
|
|
373
373
|
end
|
|
374
374
|
|
|
375
|
-
token
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
if token.nil?
|
|
376
|
+
return nil if opts[:non_interactive]
|
|
377
|
+
|
|
378
|
+
token = PWN::Plugins::AuthenticationHelper.mask_password(
|
|
379
|
+
prompt: 'Anthropic API Key (or run PWN::AI::Anthropic.obtain_oauth_bearer_token for Claude Pro/Max OAuth)'
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
return nil if token.nil?
|
|
378
384
|
|
|
379
385
|
http_method = if opts[:http_method].nil?
|
|
380
386
|
:get
|
|
@@ -488,6 +494,122 @@ module PWN
|
|
|
488
494
|
raise e
|
|
489
495
|
end
|
|
490
496
|
|
|
497
|
+
# Supported Method Parameters::
|
|
498
|
+
# usage = PWN::AI::Anthropic.get_plan_usage(
|
|
499
|
+
# timeout: 'optional - seconds (default 8)'
|
|
500
|
+
# )
|
|
501
|
+
#
|
|
502
|
+
# Admin Usage API (organizations/usage_report/messages) yields tokens
|
|
503
|
+
# used; organizations/rate_limits yields the configured ceiling so the
|
|
504
|
+
# PS1 can show a percent. Individual accounts without an Admin key
|
|
505
|
+
# return {available:false}. Never prompts for credentials.
|
|
506
|
+
public_class_method def self.get_plan_usage(opts = {})
|
|
507
|
+
timeout = opts[:timeout] || 8
|
|
508
|
+
return { available: false, engine: :anthropic } unless plan_usage_credentials?
|
|
509
|
+
|
|
510
|
+
now = Time.now.utc
|
|
511
|
+
start_of_month = Time.utc(now.year, now.month, 1)
|
|
512
|
+
report = parse_plan_usage_json(
|
|
513
|
+
raw: anthropic_rest_call(
|
|
514
|
+
rest_call: 'organizations/usage_report/messages',
|
|
515
|
+
params: {
|
|
516
|
+
starting_at: start_of_month.strftime('%Y-%m-%dT00:00:00Z'),
|
|
517
|
+
ending_at: now.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
518
|
+
bucket_width: '1d'
|
|
519
|
+
},
|
|
520
|
+
timeout: timeout,
|
|
521
|
+
spinner: false,
|
|
522
|
+
non_interactive: true
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
used_tokens = sum_usage_tokens(report: report)
|
|
526
|
+
|
|
527
|
+
limits = parse_plan_usage_json(
|
|
528
|
+
raw: anthropic_rest_call(
|
|
529
|
+
rest_call: 'organizations/rate_limits',
|
|
530
|
+
timeout: timeout,
|
|
531
|
+
spinner: false,
|
|
532
|
+
non_interactive: true
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
limit_tokens = first_rate_limit(limits: limits)
|
|
536
|
+
|
|
537
|
+
normalized = PWN::AI.normalize_plan_usage(
|
|
538
|
+
used: used_tokens,
|
|
539
|
+
limit: limit_tokens,
|
|
540
|
+
source: 'organizations/usage_report',
|
|
541
|
+
engine: :anthropic
|
|
542
|
+
)
|
|
543
|
+
return normalized if normalized[:available]
|
|
544
|
+
|
|
545
|
+
{ available: false, engine: :anthropic }
|
|
546
|
+
rescue StandardError
|
|
547
|
+
{ available: false, engine: :anthropic }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
private_class_method def self.plan_usage_credentials?
|
|
551
|
+
engine = PWN::Env.dig(:ai, :anthropic) || {}
|
|
552
|
+
oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
|
|
553
|
+
real_config_value?(value: engine[:key]) ||
|
|
554
|
+
real_config_value?(value: oauth[:bearer_token]) ||
|
|
555
|
+
real_config_value?(value: oauth[:refresh_token])
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
private_class_method def self.parse_plan_usage_json(opts = {})
|
|
559
|
+
raw = opts[:raw]
|
|
560
|
+
return nil if raw.nil?
|
|
561
|
+
|
|
562
|
+
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
563
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
564
|
+
rescue JSON::ParserError, TypeError
|
|
565
|
+
nil
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
private_class_method def self.sum_usage_tokens(opts = {})
|
|
569
|
+
report = opts[:report]
|
|
570
|
+
return nil unless report.is_a?(Hash)
|
|
571
|
+
|
|
572
|
+
buckets = report[:data] || report[:usage] || []
|
|
573
|
+
total = 0
|
|
574
|
+
Array(buckets).each do |bucket|
|
|
575
|
+
next unless bucket.is_a?(Hash)
|
|
576
|
+
|
|
577
|
+
results = bucket[:results] || bucket[:usage] || [bucket]
|
|
578
|
+
Array(results).each do |row|
|
|
579
|
+
next unless row.is_a?(Hash)
|
|
580
|
+
|
|
581
|
+
%i[
|
|
582
|
+
uncached_input_tokens
|
|
583
|
+
input_tokens
|
|
584
|
+
cache_read_input_tokens
|
|
585
|
+
cache_creation_input_tokens
|
|
586
|
+
output_tokens
|
|
587
|
+
].each { |k| total += row[k].to_i }
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
total.positive? ? total : nil
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
private_class_method def self.first_rate_limit(opts = {})
|
|
594
|
+
limits = opts[:limits]
|
|
595
|
+
return nil unless limits.is_a?(Hash)
|
|
596
|
+
|
|
597
|
+
Array(limits[:data]).each do |group|
|
|
598
|
+
next unless group.is_a?(Hash)
|
|
599
|
+
|
|
600
|
+
Array(group[:limits]).each do |lim|
|
|
601
|
+
next unless lim.is_a?(Hash)
|
|
602
|
+
|
|
603
|
+
type = lim[:type].to_s
|
|
604
|
+
next unless type.match?(/tokens_per|input_tokens|output_tokens/)
|
|
605
|
+
|
|
606
|
+
v = lim[:value].to_f
|
|
607
|
+
return v if v.positive?
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
nil
|
|
611
|
+
end
|
|
612
|
+
|
|
491
613
|
# ----------------------------------------------------------------------
|
|
492
614
|
# Native tool-calling adapter for PWN::AI::Agent::Loop.
|
|
493
615
|
#
|
|
@@ -832,6 +954,8 @@ module PWN
|
|
|
832
954
|
puts "USAGE:
|
|
833
955
|
models = #{self}.get_models
|
|
834
956
|
|
|
957
|
+
usage = #{self}.get_plan_usage
|
|
958
|
+
|
|
835
959
|
# One-time Claude Pro/Max OAuth enrollment (PKCE paste flow):
|
|
836
960
|
bearer = #{self}.obtain_oauth_bearer_token
|
|
837
961
|
# Subsequent runs silently refresh via ai.anthropic.oauth.refresh_token
|
data/lib/pwn/ai/gemini.rb
CHANGED
|
@@ -118,6 +118,19 @@ module PWN
|
|
|
118
118
|
raise e
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# Supported Method Parameters::
|
|
122
|
+
# usage = PWN::AI::Gemini.get_plan_usage(
|
|
123
|
+
# timeout: 'optional - seconds (default 8)'
|
|
124
|
+
# )
|
|
125
|
+
#
|
|
126
|
+
# Gemini Generative Language has no subscription-usage endpoint that
|
|
127
|
+
# returns used-vs-limit for an API key. Cached-miss / unavailable so
|
|
128
|
+
# the PS1 shows ∞ unless the operator later injects used/limit.
|
|
129
|
+
public_class_method def self.get_plan_usage(opts = {})
|
|
130
|
+
_timeout = opts[:timeout] || 8
|
|
131
|
+
{ available: false, engine: :gemini }
|
|
132
|
+
end
|
|
133
|
+
|
|
121
134
|
# ----------------------------------------------------------------------
|
|
122
135
|
# Native tool-calling adapter for PWN::AI::Agent::Loop.
|
|
123
136
|
#
|
|
@@ -432,6 +445,8 @@ module PWN
|
|
|
432
445
|
puts "USAGE:
|
|
433
446
|
models = #{self}.get_models
|
|
434
447
|
|
|
448
|
+
usage = #{self}.get_plan_usage
|
|
449
|
+
|
|
435
450
|
response = #{self}.chat(
|
|
436
451
|
request: 'required - message to Gemini',
|
|
437
452
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:gemini][:model])',
|