pwn 0.5.698 → 0.5.700
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/CHANGELOG_BETWEEN_TAGS.txt +158 -158
- data/lib/pwn/ai/agent/loop.rb +100 -18
- data/lib/pwn/ai/agent/task_summarizer.rb +48 -5
- data/lib/pwn/ai/agent/tool_guard.rb +1 -0
- data/lib/pwn/ai/agent/tools/shell.rb +1 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +91 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +8 -0
- data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +61 -0
- data/third_party/pwn_rdoc.jsonl +5 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffcafd562bd8aa67973626a722c45396187996c5e6ac878a004622c6fc40f335
|
|
4
|
+
data.tar.gz: 9e33ac68ac5f9574e8fc5243deda6745ed3300854af398cb33bbbfa2916fc95d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb91f1c79739e3c6289f1d16bb53b23f6a0d7467943cdf2a8a20217e7a40e6bbfef2a60e0513cb487ee6b53a8efd9a5e6b15b562e1f0d68d05c1c7fe3880aac9
|
|
7
|
+
data.tar.gz: 7ae7c8c7070a8f48a47a6336556f264ac05a98a41f1a10fcb1b49692eaa24c926af34c4d1792b822d1bd817ee3b26a89aa7ba5d08d11c976a6ceab154b74ce67
|
data/CHANGELOG_BETWEEN_TAGS.txt
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
1
|
+
27c8b2b5 Merge pull request #1009 from ninp0/master
|
|
2
|
+
3af915b9 `pwn-ai` - deal with ReadTimeout when dealing w/ large payloads
|
|
3
|
+
a06f59d6 Merge pull request #1008 from ninp0/master
|
|
4
|
+
3a87fdd5 `pwn-ai` - deal with HTTP 502, 503, 504 responses from LLM provider
|
|
5
|
+
7fde94c2 Merge pull request #1007 from ninp0/master
|
|
6
|
+
7a11fabd `pwn-ai` - Create timeout bounds for `shell` and `pwn_eval` tools ranging from 30-10_800 seconds incrementing 180 seconds until command completes or timeout after 3hrs is triggered steering a different approach. Also added `/model` command suite (with command completion) to quickly list / switch providers/model in an active `pwn-ai` session that persist cross-session
|
|
7
|
+
74890a72 Merge pull request #1006 from ninp0/master
|
|
8
|
+
05609003 PWN::AI::RedTeam namespace - extend modules to include attack methods described in OWASP Top 10 for LLM Applications 2025 / 2026 && AI Red Teaming Solutions Taxonomy
|
|
9
|
+
12c51f86 Merge pull request #1005 from ninp0/master
|
|
10
|
+
9a9511a5 Better Google Workspace authZ within `pwn-ai` #rubocop_fixes
|
|
11
|
+
3563cf03 Better Google Workspace authZ within `pwn-ai`
|
|
12
|
+
88fa6bd0 Better Google Workspace authZ within `pwn-ai`
|
|
13
|
+
7aeeb7c1 Merge pull request #1004 from ninp0/master
|
|
14
|
+
31d74e1d Add default skills for the entire PWN namespace
|
|
15
|
+
b13d6392 pwn-ai - toggle-debug tmp file output bugfixes
|
|
16
|
+
341b0063 Merge pull request #1003 from ninp0/master
|
|
17
|
+
0422844d pwn-ai - begin implementing command menu, add path completion, and default cwe, capec, and ATT&CK skills
|
|
18
|
+
2bb98aa8 pwn-ai - begin implementing command menu, add path completion, and default cwe, capec, and ATT&CK skills
|
|
19
|
+
b891eec4 Merge pull request #1002 from ninp0/master
|
|
20
|
+
6f20948b .ruby-version - Bump to 4.0.6 && implement `toggle-debug` in pwn REPL to gernerate `pwn-ai` logs for troubleshooting unexpected behavior
|
|
21
|
+
2a1026a9 .ruby-version - Bump to 4.0.6 && implement `toggle-debug` in pwn REPL to gernerate `pwn-ai` logs for troubleshooting unexpected behavior
|
|
22
|
+
bee1d16e Merge pull request #1001 from ninp0/master
|
|
23
|
+
d9bc8906 `pwn-ai` - implement verbose logging to /tmp and STDOUT when `toggle-pwn-ai-debug` == true
|
|
24
|
+
21433603 Merge pull request #1000 from ninp0/master
|
|
25
|
+
ed4f4963 `pwn setup --migrate` - include default set of skills that live in /opt/pwn/etc/default_skills
|
|
26
|
+
fed5a3be PWN::AI::Agent::Registry module - implement tool priority order preferences (including broader list of tools), `spin.stop` object #bugfix in `pwn-ai`, and documentation updates
|
|
27
|
+
c99edf8b PWN::AI::Agent::Registry module - implement tool priority order preferences (including broader list of tools), `spin.stop` object #bugfix in `pwn-ai`, and documentation updates
|
|
28
|
+
b5cb837b Merge pull request #999 from ninp0/master
|
|
29
|
+
3e26dfdb pwn-ai - eliminate request types and consolidate into the same logic (i.e. question vs statement vs autonomous_goal simply become autonomous_goal)
|
|
30
|
+
c8b19465 pwn-ai #bugfixes / #enhancements related to autonomous_goal completion
|
|
31
|
+
67d30408 Merge pull request #998 from ninp0/master
|
|
32
|
+
c83b239e More `spin.stop` object #bugfixes in `pwn-ai` and enhancement when submitting a statement.
|
|
33
|
+
ba022c5c Merge pull request #997 from ninp0/master
|
|
34
|
+
16c21652 PWN::AI::Agent::Registry module - implement tool priority order preferences, `spin.stop` object #bugfix in `pwn-ai`, and documentation updates
|
|
35
|
+
2fd461a7 Merge pull request #996 from ninp0/master
|
|
36
|
+
212ac1be spinner bugfixes
|
|
37
|
+
270223c2 Merge pull request #995 from ninp0/master
|
|
38
|
+
64cc08fa pwn setup - Implement cron worker line entry to ensure pwn-ai cron jobs are executed on the schedule in which they're defined #bugfixesd
|
|
39
|
+
f63e9e58 pwn setup - Implement cron worker line entry to ensure pwn-ai cron jobs are executed on the schedule in which they're defined
|
|
40
|
+
79d9ed03 Merge pull request #994 from ninp0/master
|
|
41
|
+
206c6211 Added PWN::WWW::GitHub module and removed PWN::AI::RedTeam::ModelDenialOfService module - too expensive / exhausts tokens too quickly.
|
|
42
|
+
20fc78c4 Merge pull request #993 from ninp0/master
|
|
43
|
+
f8359e61 RL feedback loop enhancements
|
|
44
|
+
2c4d7df0 Merge pull request #992 from ninp0/master
|
|
45
|
+
ee44dc6d Faster memory recall for current session and distinguish between users statements, questions, and autonomous goal requests where the latter requires a task breakdown.
|
|
46
|
+
8ff5835b Merge pull request #991 from ninp0/master
|
|
47
|
+
f62af554 When using pwn-vault command within the pwn REPL driver, do not allow the editor to leave until PWN::Config confirms a valid config...this prevents cumbersome decrypt/re-edit/encrypt pwn.yaml tasks to get back into the pwn REPL.
|
|
48
|
+
c96b74ea Merge pull request #990 from ninp0/master
|
|
49
|
+
de9505b6 Decouple ollama and openwebui by creating each as their own provider, memory recall enhancements, more resislient request parsing when using ollama or openwebui
|
|
50
|
+
ad53a14e Merge pull request #989 from ninp0/master
|
|
51
|
+
55b0183a README.md - update Groks take on `pwn-ai`.
|
|
52
|
+
908f6ab9 Merge pull request #988 from ninp0/master
|
|
53
|
+
2f3a0ea8 PWN::AI::Grok, PWN::AI::Anthropic, and PWN::AI::OpenAI - persist oauth bearer tokens to pwn.yaml when refresh token are used to refresh AuthZ
|
|
54
|
+
e881ca80 Merge pull request #987 from ninp0/master
|
|
55
|
+
ff56c555 ~/.pwn - keep files lean in RL feedback loopy
|
|
56
|
+
9df6e56e pwn-ai - t retrieve context in current session morr quickly
|
|
57
|
+
210f41ee Merge pull request #986 from ninp0/master
|
|
58
|
+
7d5488a7 Merge remote-tracking branch 'upstream/master'
|
|
59
|
+
af2a13eb pwn-ai - task summarizer to convey task objectives for long running autonomous tasks which drives tools to complete each task until originally submitted user request is complete #round2.
|
|
60
|
+
716fa2a8 Merge pull request #985 from ninp0/master
|
|
61
|
+
b03457ac pwn-ai - task summarizer to convey task objectives for long running autonomous tasks which drives tools to complete each task until originally submitted user request is complete.
|
|
62
|
+
9722b8e5 Merge pull request #984 from ninp0/master
|
|
63
|
+
5854f34a pwn-ai - task summarizer to convey task objectives for long running autonomous tasks, iteration exhaustion, and memory recall / learning outcome #bugfixes
|
|
64
|
+
85f6b6d7 Merge pull request #983 from ninp0/master
|
|
65
|
+
066088e5 pwn-ai - Implement a task summarizer to convey task objectives for long running autonomous tasks
|
|
66
|
+
0efb7de9 Merge pull request #982 from ninp0/master
|
|
67
|
+
989c3cea more autonomous fixes
|
|
68
|
+
d44703ac Merge pull request #981 from ninp0/master
|
|
69
|
+
2dfd5762 PWN::AI::Agent::Tools::Shell module - long-running shell timeouts should no longer spam IOError errors to STDOUT
|
|
70
|
+
c0440049 Merge pull request #980 from ninp0/master
|
|
71
|
+
27ade080 Reinforced learning feedback loop enhancements - better support for long autonomous tasks #round2
|
|
72
|
+
9701e708 Merge pull request #979 from ninp0/master
|
|
73
|
+
52c76428 Reinforced learning feedback loop enhancements - better support for long autonomous tasks
|
|
74
|
+
be84dd69 Merge pull request #978 from ninp0/master
|
|
75
|
+
6311b03e #rubocop_fix
|
|
76
|
+
c758c22b PWN::Plugins::HackerOne module - cast to hacker api #bump_gem_version_compatibility
|
|
77
|
+
8b0aaf79 PWN::Plugins::HackerOne module - cast to hacker api
|
|
78
|
+
712962e1 Merge pull request #977 from ninp0/master
|
|
79
|
+
6ecd9f29 doocumentation updates
|
|
80
|
+
13d8d16f Merge pull request #976 from ninp0/master
|
|
81
|
+
6333e3e4 Reinforced learning feedback loop enhancements && documentation updates
|
|
82
|
+
690654ec Merge pull request #975 from ninp0/master
|
|
83
|
+
0a644588 Reinforced learning feedback loop enhancements && documentation updates
|
|
84
|
+
a23567b7 Merge remote-tracking branch 'upstream/master'
|
|
85
|
+
4330a39a Reinforced learning residual: P26 version-claim widen + pwn_eval EvalWithLocation + style
|
|
86
|
+
1ddd9df5 Reinforced learning residual: deepen P17 budget_hot + P26 claim filter + pwn_eval self-heal
|
|
87
|
+
85534396 Reinforced learning feedback loop: close P0/P1/P2/P17 weakness set
|
|
88
|
+
f3f86bdb Merge pull request #974 from ninp0/master
|
|
89
|
+
fd964ed4 Reinforced learning feedback loop enhancements && documentation updates
|
|
90
|
+
6533fc41 Merge pull request #973 from ninp0/master
|
|
91
|
+
33235063 Reinforced learning feedback loop enhancements && add oAuth support for PWN::AI::OpenAI && PWN::AI::Anthropic
|
|
92
|
+
97fcfc90 Merge pull request #972 from ninp0/master
|
|
93
|
+
3e66ada6 Reinforced learning feedback loop enhancements, convert ollama provider to stream for faster response times, and refresh documentation to reflect latest features
|
|
94
|
+
ebb9c280 Reinforced learning feedback loop enhancements and convert ollama provider to stream for faster response times
|
|
95
|
+
4e7ef952 Merge pull request #971 from ninp0/master
|
|
96
|
+
c8286237 Github install-matrix - arbiitrary fixes.
|
|
97
|
+
29ca2745 Merge pull request #970 from ninp0/master
|
|
98
|
+
516af396 pwn_ai_red_team Driver - Initial commit to enable targeted LLM penetration testing (e.g. Jail Breaking, Prompt Injection, etc.)
|
|
99
|
+
02a33526 Merge pull request #969 from ninp0/master
|
|
100
|
+
cd97809f Github install-matrix - arbiitrary fixes.
|
|
101
|
+
04d271d5 Merge pull request #968 from ninp0/master
|
|
102
|
+
b9111aef More ~/.pwn resilience checking following upgrades
|
|
103
|
+
310df119 Merge pull request #967 from ninp0/master
|
|
104
|
+
d041088e Work towards getting install-matrix workflow operable
|
|
105
|
+
1f089d04 Merge pull request #966 from ninp0/master
|
|
106
|
+
ffee333f Ensure pwn-ai skills conform to https://agentskills.io/specification
|
|
107
|
+
bf484647 Ensure pwn-ai skills conform to https://agentskills.io/specification
|
|
108
|
+
97b444c0 Merge pull request #965 from ninp0/master
|
|
109
|
+
6a000929 More reinforcement learning feedback loop enhancements
|
|
110
|
+
5ace30cb Merge pull request #964 from ninp0/master
|
|
111
|
+
d72fe894 Documentation / DFD updates to reflect proper configs / SDR decoder fixes #rake_fixes
|
|
112
|
+
de3f5557 Documentation / DFD updates to reflect proper configs / SDR decoder fixes
|
|
113
|
+
2b279191 Merge pull request #963 from ninp0/master
|
|
114
|
+
f988e881 Learning FeedBack Loop - enhancements to support smaller LLMs, refined easier installation, and updates to documentation / DFDs
|
|
115
|
+
677fcdc1 Learning FeedBack Loop - enhancements to support smaller, local models
|
|
116
|
+
a9cc3625 Merge pull request #962 from ninp0/master
|
|
117
|
+
0cf0c7de README.md - prefer PWN gif over static png
|
|
118
|
+
5904e90d Extrospection tweaks to support things like, what song is playing on a specific FM station, what planes are flying over me, what are strongest RF signals around me, tell me the weather in Tokyo, has the bug bounty page been updated, any known exploits in Apache in 2026, what devices are connected to this laptop, etc.
|
|
119
|
+
690c7c1a fix(gqrx): restore regular-scan edge detection + sane squelch for fm_radio
|
|
120
|
+
d94f1243 chore: clear rubocop/rake; extrospection limbs + GQRX scanner polish
|
|
121
|
+
90a41a68 Extrospection tweaks to support things like, what song is playing on a specific FM station, what planes are flying over me, what are strongest RF signals around me, tell me the weather in Tokyo, has the bug bounty page been updated, any known exploits in Apache in 2026, what devices are connected to this laptop, etc.
|
|
122
|
+
1949b9e5 Merge pull request #961 from ninp0/master
|
|
123
|
+
79278f1d Gemfile - pull in latest gem versions, documentation/Configuration.md - exhaustively document pwn.yaml && rename some key/value pairs for clarity.
|
|
124
|
+
5ceb5f6d Merge pull request #960 from ninp0/master
|
|
125
|
+
611da4ac More documentation updates and native ruby SDR decoders
|
|
126
|
+
6831b63f More documentation updates, enhanced "learning from mistakes", and added a few SDR decoders
|
|
127
|
+
d65364a5 More documentation updates, enhanced "learning from mistakes", and added a few SDR decoders
|
|
128
|
+
68160bc2 Merge pull request #959 from ninp0/master
|
|
129
|
+
fe461f73 More documentation tweaks
|
|
130
|
+
55392d25 Merge pull request #958 from ninp0/master
|
|
131
|
+
c85981c9 Cron bugfixes. PWN::AI - feedback loop improvements / extrospection to mirror introspection. Also overhaul README.md , implement more documentation, and DFDs easier to understand, implement `pwn --ai` option within pwn REPL driver, remove deprecated pwn-irc command in favor of PWN::AI::Agent::Tools::Swarm module, etc.
|
|
132
|
+
afaad9bc Cron bugfixes. PWN::AI - feedback loop improvements / extrospection to mirror introspection. Also overhaul README.md , implement more documentation, and make DFDs easier to evaluate.
|
|
133
|
+
7d580052 PWN::AI:Grok module - implement oauth. PWN::AI - feedback loop improvements. Also overhaul README.md , implement more documenration, and DFDs.
|
|
134
|
+
f37a4f9b PWN::AI:Grok module - implement oauth. PWN::AI - feedback loop improvements. Also overhaul README.md , implement more documenration, and DFDs.
|
|
135
|
+
00a4ea0c Merge pull request #957 from ninp0/master
|
|
136
|
+
c477a034 Remove backup file
|
|
137
|
+
ee35abbe Merge pull request #956 from ninp0/master
|
|
138
|
+
5119246d Numerous #bugfixes / stability fixes for pwn-ai
|
|
139
|
+
0bdcb3b2 Merge pull request #955 from support-0dayInc/master
|
|
140
|
+
b9093859 fix(ai): PWN::AI::Grok.obtain_oauth_bearer_token now reliable for singular long-lived enrollment; guard for secret+Basic auth+error handling+public method; clear user steps for one-time pwn-vault persist (GPG signed for 0dayinc/pwn merge)
|
|
141
|
+
ae6a4902 fix(ai): correct PWN::AI::Grok oauth trigger to pass hash arg + guard per pwn_sdlc reference; remove debug puts
|
|
142
|
+
3314cedd Merge pull request #954 from ninp0/master
|
|
143
|
+
b12a2745 Gemfile - bump meshtastic version
|
|
144
|
+
0eb50ed7 pwn-ai REPL command - minor bugfixes / optimizations
|
|
145
|
+
ec7cfdee Merge pull request #953 from support-0dayInc/master
|
|
146
|
+
1cf42886 feat(ai): PWN::AI::Grok calls authorize endpoint (URL construction + code exchange) only when oauth configured for bearer_token (xAI SuperGrok); full pwn_sdlc pipeline
|
|
147
|
+
83abdf0d Merge remote-tracking branch 'upstream/master'
|
|
148
|
+
53b2b752 Merge pull request #952 from ninp0/master
|
|
149
|
+
c6e12194 PWN::Plugins::REPL module - bugfixes in pwn-asm / pwn-ai commands within PWN prototyping REPL. (#951)
|
|
150
|
+
77436d17 PWN::Plugins::REPL module - bugfixes in pwn-asm / pwn-ai commands within PWN prototyping REPL.
|
|
151
|
+
04659d7e Merge remote-tracking branch 'upstream/master'
|
|
152
|
+
cbad0e52 pwn: feat(pwn-ai): implement pwn-ai command in pwn REPL driver as an agentic AI harness (GPG signed for 0dayinc/pwn merge) (#950)
|
|
153
|
+
09213be5 feat(pwn-ai): implement pwn-ai command in pwn REPL driver as an agentic AI harness (GPG signed for 0dayinc/pwn merge requirement)
|
|
154
|
+
d950414f Implement pwn-ai command in pwn REPL driver as an agentic AI harness.
|
|
155
|
+
94d4bb3e Merge pull request #949 from support-0dayInc/master
|
|
156
|
+
6a169ab4 Finalize pwn gem visibility outside /opt/pwn (ruby-4.0.5@pwn gemset), MFA/OTP prompt handling for rvmsudo gem push $latest_gem in git_commit.sh, pwn-ai REPL driver anthropic (and other) provider response visibility in TUI + system_role_content override bug fix (full pwn_sdlc pipeline)
|
|
157
|
+
d3497740 Merge remote-tracking branch 'upstream/master'
|
|
158
|
+
e6be6df5 Finalize pwn gem visibility outside /opt/pwn (ruby-4.0.5@pwn gemset), MFA/OTP prompt handling for rvmsudo gem push in git_commit.sh, pwn-ai REPL driver anthropic (and other) provider response visibility in TUI + system_role_content override bug fix (full pwn_sdlc pipeline)
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -855,9 +855,10 @@ module PWN
|
|
|
855
855
|
err = opts[:error]
|
|
856
856
|
return false unless err
|
|
857
857
|
|
|
858
|
-
return
|
|
858
|
+
return true if defined?(PWN::AI::HttpRetry) && PWN::AI::HttpRetry.respond_to?(:retryable?) &&
|
|
859
|
+
PWN::AI::HttpRetry.retryable?(error: err)
|
|
859
860
|
|
|
860
|
-
err.message.to_s.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout/i)
|
|
861
|
+
err.message.to_s.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout|tool_use_id|tool_result/i)
|
|
861
862
|
rescue StandardError
|
|
862
863
|
false
|
|
863
864
|
end
|
|
@@ -905,6 +906,29 @@ module PWN
|
|
|
905
906
|
nil
|
|
906
907
|
end
|
|
907
908
|
|
|
909
|
+
private_class_method def self.payload_sig(opts = {})
|
|
910
|
+
Digest::SHA256.hexdigest("#{opts[:name]}|#{opts[:args]}")[0, 16]
|
|
911
|
+
rescue StandardError
|
|
912
|
+
"nosig-#{opts[:name]}"
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
private_class_method def self.note_same_payload!(opts = {})
|
|
916
|
+
sig = payload_sig(opts)
|
|
917
|
+
counts = Thread.current[:pwn_same_payload] ||= Hash.new(0)
|
|
918
|
+
counts[sig] += 1
|
|
919
|
+
counts[sig]
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
private_class_method def self.no_progress_result(opts = {})
|
|
923
|
+
name = opts[:name].to_s
|
|
924
|
+
sig = payload_sig(opts)
|
|
925
|
+
JSON.generate(
|
|
926
|
+
success: false,
|
|
927
|
+
error: "no_progress: identical #{name} payload repeated (#{sig}). Change the command.",
|
|
928
|
+
result: { stdout: '', stderr: "no_progress: #{name}", exit: 2 }
|
|
929
|
+
)
|
|
930
|
+
end
|
|
931
|
+
|
|
908
932
|
# Repeat circuit-breaker. `count` is max(per-turn, persistent) so a
|
|
909
933
|
# signature that already failed in a PREVIOUS session trips the guard
|
|
910
934
|
# on its FIRST recurrence here — the agent does not get to burn the
|
|
@@ -922,7 +946,7 @@ module PWN
|
|
|
922
946
|
Mistakes.extinguish!(signature: sig, args: opts[:args], shape: opts[:shape], force: true) if sig
|
|
923
947
|
end
|
|
924
948
|
Thread.current[:pwn_extinguished] ||= {}
|
|
925
|
-
Thread.current[:pwn_extinguished][opts[:name]
|
|
949
|
+
Thread.current[:pwn_extinguished][payload_sig(name: opts[:name], args: opts[:args])] = true
|
|
926
950
|
|
|
927
951
|
guard = "[pwn-ai/mistakes] EXTINGUISHED / REPEATED FAILURE — this #{opts[:name]} failure signature has " \
|
|
928
952
|
"occurred #{count}× (across sessions). DO NOT retry it verbatim. Apply the " \
|
|
@@ -1427,8 +1451,10 @@ module PWN
|
|
|
1427
1451
|
messages = opts[:messages]
|
|
1428
1452
|
return messages unless messages.is_a?(Array) && messages.length > 12
|
|
1429
1453
|
|
|
1430
|
-
keep_pairs =
|
|
1431
|
-
|
|
1454
|
+
keep_pairs = opts[:keep_pairs]
|
|
1455
|
+
keep_pairs = (agent_flag(key: :history_keep_tool_pairs, default: 6) || 6).to_i if keep_pairs.nil?
|
|
1456
|
+
max_chars = opts[:max_chars]
|
|
1457
|
+
max_chars = (agent_flag(key: :history_tool_max_chars, default: 2_000) || 2_000).to_i if max_chars.nil?
|
|
1432
1458
|
|
|
1433
1459
|
head = []
|
|
1434
1460
|
rest = messages.dup
|
|
@@ -1445,23 +1471,72 @@ module PWN
|
|
|
1445
1471
|
end
|
|
1446
1472
|
head << rest.shift if rest.any? && rest.first[:role].to_s == 'assistant' && rest.first[:content].to_s.start_with?('PLAN:')
|
|
1447
1473
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1474
|
+
pairs = []
|
|
1475
|
+
idx = 0
|
|
1476
|
+
while idx < rest.length
|
|
1477
|
+
if rest[idx][:role].to_s == 'assistant'
|
|
1478
|
+
group = [rest[idx]]
|
|
1479
|
+
idx += 1
|
|
1480
|
+
while idx < rest.length && rest[idx][:role].to_s == 'tool'
|
|
1481
|
+
group << rest[idx]
|
|
1482
|
+
idx += 1
|
|
1483
|
+
end
|
|
1484
|
+
pairs << group
|
|
1485
|
+
elsif rest[idx][:role].to_s == 'tool'
|
|
1486
|
+
# Orphan tool_result with no preceding tool_use — drop it.
|
|
1487
|
+
idx += 1
|
|
1488
|
+
else
|
|
1489
|
+
pairs << [rest[idx]]
|
|
1490
|
+
idx += 1
|
|
1491
|
+
end
|
|
1492
|
+
end
|
|
1493
|
+
collapsed = []
|
|
1494
|
+
pairs.each do |pair|
|
|
1495
|
+
tool = pair.find { |m| m[:role].to_s == 'tool' }
|
|
1496
|
+
prev = collapsed.last&.find { |m| m[:role].to_s == 'tool' }
|
|
1497
|
+
next if tool && prev && tool[:content].to_s.strip == prev[:content].to_s.strip
|
|
1498
|
+
|
|
1499
|
+
collapsed << pair
|
|
1500
|
+
end
|
|
1501
|
+
kept = collapsed.last(keep_pairs).flatten
|
|
1453
1502
|
kept.each do |m|
|
|
1454
1503
|
next unless m[:role].to_s == 'tool' && m[:content].to_s.length > max_chars
|
|
1455
1504
|
|
|
1456
1505
|
m[:content] = "#{m[:content].to_s[0, max_chars]}…[compacted]"
|
|
1457
1506
|
end
|
|
1458
1507
|
messages.replace(head + kept)
|
|
1508
|
+
repair_tool_history!(messages: messages)
|
|
1459
1509
|
messages
|
|
1460
1510
|
rescue StandardError => e
|
|
1461
1511
|
warn "[pwn-ai/loop] compact_history swallowed: #{e.class}: #{e.message}"
|
|
1462
1512
|
opts[:messages]
|
|
1463
1513
|
end
|
|
1464
1514
|
|
|
1515
|
+
private_class_method def self.repair_tool_history!(opts = {})
|
|
1516
|
+
messages = opts[:messages]
|
|
1517
|
+
return messages unless messages.is_a?(Array)
|
|
1518
|
+
|
|
1519
|
+
open_ids = []
|
|
1520
|
+
kept = []
|
|
1521
|
+
messages.each do |msg|
|
|
1522
|
+
role = msg[:role].to_s
|
|
1523
|
+
case role
|
|
1524
|
+
when 'assistant'
|
|
1525
|
+
open_ids = Array(msg[:tool_calls] || msg['tool_calls']).map { |tc| (tc[:id] || tc['id']).to_s }
|
|
1526
|
+
when 'tool'
|
|
1527
|
+
tid = (msg[:tool_call_id] || msg['tool_call_id']).to_s
|
|
1528
|
+
next if tid.empty? || !open_ids.include?(tid)
|
|
1529
|
+
when 'user'
|
|
1530
|
+
open_ids = []
|
|
1531
|
+
end
|
|
1532
|
+
kept << msg
|
|
1533
|
+
end
|
|
1534
|
+
messages.replace(kept)
|
|
1535
|
+
messages
|
|
1536
|
+
rescue StandardError
|
|
1537
|
+
opts[:messages]
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1465
1540
|
# 3.2 — local models cannot afford auto_introspect (judge+prm+critic+
|
|
1466
1541
|
# sentinel+extro) on every success. Default :failure_only when local.
|
|
1467
1542
|
private_class_method def self.should_auto_introspect?(opts = {})
|
|
@@ -2320,6 +2395,7 @@ module PWN
|
|
|
2320
2395
|
end
|
|
2321
2396
|
Thread.current[:pwn_request_intent] = intent
|
|
2322
2397
|
Thread.current[:pwn_extinguished] = {}
|
|
2398
|
+
Thread.current[:pwn_same_payload] = Hash.new(0)
|
|
2323
2399
|
debug_progress(msg: "intent=#{intent} engine=#{engine}", debug: opts[:debug])
|
|
2324
2400
|
expose_current_session(session_id: session_id)
|
|
2325
2401
|
Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
|
|
@@ -2491,14 +2567,16 @@ module PWN
|
|
|
2491
2567
|
i = 0
|
|
2492
2568
|
loop do
|
|
2493
2569
|
i += 1
|
|
2494
|
-
# 3.1 — compact
|
|
2495
|
-
|
|
2570
|
+
# 3.1 — compact fat tool dumps so remote ReadTimeout hops do not
|
|
2571
|
+
# retry the same 70-message payload (R1 201215 Anthropic 180s×5).
|
|
2572
|
+
compact_history!(messages: messages)
|
|
2496
2573
|
# English-task-as-primary: when plan_idx advanced, tell the model
|
|
2497
2574
|
# which plain-English task is active before the next tool batch.
|
|
2498
2575
|
inject_task_focus!(messages: messages, state: ts_state, request: request) unless skip_compass
|
|
2499
2576
|
|
|
2500
2577
|
t0 = Time.now
|
|
2501
2578
|
begin
|
|
2579
|
+
repair_tool_history!(messages: messages)
|
|
2502
2580
|
msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
|
|
2503
2581
|
rescue StandardError => e
|
|
2504
2582
|
if engine_transient?(error: e)
|
|
@@ -2510,6 +2588,7 @@ module PWN
|
|
|
2510
2588
|
final_chars = txt.length
|
|
2511
2589
|
return txt
|
|
2512
2590
|
end
|
|
2591
|
+
compact_history!(messages: messages, keep_pairs: 3, max_chars: 800)
|
|
2513
2592
|
messages << {
|
|
2514
2593
|
role: 'user',
|
|
2515
2594
|
content: '[pwn-ai] engine hop failed (transient). Keep calling CORE_TOOLS. ' \
|
|
@@ -2630,14 +2709,17 @@ module PWN
|
|
|
2630
2709
|
started = Time.now
|
|
2631
2710
|
argv_s = args.is_a?(String) ? args.to_s : args.inspect
|
|
2632
2711
|
debug_progress(msg: "tool #{name} start:\n#{argv_s}", keep_newlines: true, cap: 0, tee: nil)
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
error: "extinguished_repeat: #{name} already failed this signature this turn — change args or tool",
|
|
2637
|
-
result: { stdout: '', stderr: "extinguished_repeat: #{name}", exit: 2 }
|
|
2638
|
-
)
|
|
2712
|
+
sig = payload_sig(name: name, args: args)
|
|
2713
|
+
if Thread.current[:pwn_extinguished].is_a?(Hash) && Thread.current[:pwn_extinguished][sig]
|
|
2714
|
+
raw = no_progress_result(name: name, args: args)
|
|
2639
2715
|
else
|
|
2640
2716
|
raw = Dispatch.call(tool_call: tc)
|
|
2717
|
+
same_n = note_same_payload!(name: name, args: args)
|
|
2718
|
+
if same_n >= 3
|
|
2719
|
+
Thread.current[:pwn_extinguished] ||= {}
|
|
2720
|
+
Thread.current[:pwn_extinguished][sig] = true
|
|
2721
|
+
raw = no_progress_result(name: name, args: args)
|
|
2722
|
+
end
|
|
2641
2723
|
end
|
|
2642
2724
|
tools_called += 1
|
|
2643
2725
|
tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
|
|
@@ -130,7 +130,8 @@ module PWN
|
|
|
130
130
|
last_advanced_from: nil,
|
|
131
131
|
last_advance_brief: nil,
|
|
132
132
|
tools_on_task: 0,
|
|
133
|
-
evidence_blob: ''
|
|
133
|
+
evidence_blob: '',
|
|
134
|
+
task_evidence: {}
|
|
134
135
|
}
|
|
135
136
|
end
|
|
136
137
|
|
|
@@ -661,6 +662,17 @@ module PWN
|
|
|
661
662
|
]
|
|
662
663
|
end
|
|
663
664
|
|
|
665
|
+
artifact = goal_text[%r{(?:/(?:tmp|var|home|opt|usr)/\S+\.(?:pdf|html|md|json|txt|csv)|~/\S+\.(?:pdf|html|md|json|txt|csv))}i]
|
|
666
|
+
if artifact && goal_lc.match?(/\b(analy[sz]e|test|scan|report|generat|store|write|export)\b/)
|
|
667
|
+
tasks << 'Carry out the requested analysis using the named skills and live evidence'
|
|
668
|
+
tasks << "Write the requested report to #{artifact}"
|
|
669
|
+
if goal_lc.match?(/\b(json|ya?ml|table|csv|tsv)\b/)
|
|
670
|
+
fmt = goal_lc[/\b(json|ya?ml|table|csv|tsv)\b/]
|
|
671
|
+
tasks << "Present the results in #{fmt} format"
|
|
672
|
+
end
|
|
673
|
+
return tasks
|
|
674
|
+
end
|
|
675
|
+
|
|
664
676
|
tasks << "Understand the request: #{truncate_goal(goal: goal_text)}"
|
|
665
677
|
tasks << "Carry out the core work for: #{truncate_goal(goal: goal_text)}"
|
|
666
678
|
|
|
@@ -900,7 +912,9 @@ module PWN
|
|
|
900
912
|
blob = coverage_blob(state: state, messages: opts[:messages])
|
|
901
913
|
n = plan.length
|
|
902
914
|
plan.each_with_index.filter_map do |item, i|
|
|
903
|
-
|
|
915
|
+
slice = state[:task_evidence].is_a?(Hash) ? state[:task_evidence][i].to_s : ''
|
|
916
|
+
use = host_shaped_task?(item: item) && !slice.empty? ? slice : blob
|
|
917
|
+
next if item.empty? || item_covered?(item: item, blob: use)
|
|
904
918
|
|
|
905
919
|
{ idx: i, item: item, label: "task #{i + 1}/#{n}: #{item}" }
|
|
906
920
|
end
|
|
@@ -1360,7 +1374,7 @@ module PWN
|
|
|
1360
1374
|
/\b(implement\w*|fix|patch\w*|chang\w*|improv\w*|write|apply|wire|refactor\w*)\b/
|
|
1361
1375
|
)
|
|
1362
1376
|
return :discover if s.match?(
|
|
1363
|
-
/\b(locat\w*|find|read|inspect|recon\w*|understand|decompos\w*|map|identif\w*|gather|discover|enumerat\w*|scan|probe|determin\w*|root cause|where and why|track|navigat\w*|browse|goto)\b/
|
|
1377
|
+
/\b(locat\w*|find|read|inspect|recon\w*|understand|decompos\w*|map|identif\w*|gather|discover|enumerat\w*|scan|probe|determin\w*|analy[sz]e|analysis|root cause|where and why|track|navigat\w*|browse|goto)\b/
|
|
1364
1378
|
)
|
|
1365
1379
|
|
|
1366
1380
|
:generic
|
|
@@ -1421,6 +1435,19 @@ module PWN
|
|
|
1421
1435
|
''
|
|
1422
1436
|
end
|
|
1423
1437
|
|
|
1438
|
+
HOST_TASK_RX = /
|
|
1439
|
+
\b(?:hosts|subnet|address|mask|cidr|alive|reachab\w*|ipv4|ipv6|live\s+host)
|
|
1440
|
+
/ix
|
|
1441
|
+
HOST_IP_RX = %r{
|
|
1442
|
+
\b(?!127\.)(?:\d{1,3}\.){3}\d{1,3}(?:/\d{1,2})?\b
|
|
1443
|
+
}x
|
|
1444
|
+
|
|
1445
|
+
private_class_method def self.host_shaped_task?(opts = {})
|
|
1446
|
+
opts[:item].to_s.match?(HOST_TASK_RX)
|
|
1447
|
+
rescue StandardError
|
|
1448
|
+
false
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1424
1451
|
private_class_method def self.item_covered?(opts = {})
|
|
1425
1452
|
item = opts[:item].to_s
|
|
1426
1453
|
blob = opts[:blob].to_s.downcase
|
|
@@ -1431,6 +1458,8 @@ module PWN
|
|
|
1431
1458
|
when :verify
|
|
1432
1459
|
blob.match?(VERIFY_DONE_RX) || blob.match?(VERIFY_RAN_RX)
|
|
1433
1460
|
else
|
|
1461
|
+
return blob.match?(HOST_IP_RX) if host_shaped_task?(item: item)
|
|
1462
|
+
|
|
1434
1463
|
# discover / present / generic: some real tool evidence, not empty / tiny JSON
|
|
1435
1464
|
blob.strip.length >= 40
|
|
1436
1465
|
end
|
|
@@ -1456,10 +1485,15 @@ module PWN
|
|
|
1456
1485
|
min_tools = browse_hit ? 1 : DISCOVER_MIN_TOOLS
|
|
1457
1486
|
on_task = opts[:state].is_a?(Hash) ? opts[:state][:tools_on_task].to_i : 0
|
|
1458
1487
|
return false if on_task < min_tools
|
|
1488
|
+
|
|
1489
|
+
host_ok = host_shaped_task?(item: opts[:item]) && HOST_IP_RX.match?(opts[:result].to_s)
|
|
1490
|
+
return host_ok if host_shaped_task?(item: opts[:item])
|
|
1459
1491
|
return false unless item_covered?(item: opts[:item], blob: joined)
|
|
1460
1492
|
|
|
1493
|
+
return true if host_ok || phase == :present || browse_hit
|
|
1494
|
+
|
|
1461
1495
|
intent_s = (Array(opts[:intents]) + Array(opts[:names])).join(' ')
|
|
1462
|
-
task_intent_match?(item: opts[:item], intent: intent_s)
|
|
1496
|
+
task_intent_match?(item: opts[:item], intent: intent_s)
|
|
1463
1497
|
else
|
|
1464
1498
|
false
|
|
1465
1499
|
end
|
|
@@ -1478,6 +1512,12 @@ module PWN
|
|
|
1478
1512
|
nxt_p = task_phase(item: nxt)
|
|
1479
1513
|
return false if nxt_p == :generic
|
|
1480
1514
|
return false if cur_p == nxt_p
|
|
1515
|
+
|
|
1516
|
+
if host_shaped_task?(item: opts[:item])
|
|
1517
|
+
idx = opts[:state][:plan_idx].to_i
|
|
1518
|
+
ev = opts[:state][:task_evidence].is_a?(Hash) ? opts[:state][:task_evidence][idx].to_s : ''
|
|
1519
|
+
return false unless HOST_IP_RX.match?(ev)
|
|
1520
|
+
end
|
|
1481
1521
|
return false unless task_intent_match?(item: nxt, intent: opts[:intent]) || nxt_p == :present
|
|
1482
1522
|
|
|
1483
1523
|
true
|
|
@@ -1625,11 +1665,14 @@ module PWN
|
|
|
1625
1665
|
state[:counts][name.to_s] += 1
|
|
1626
1666
|
state[:total] += 1
|
|
1627
1667
|
state[:since_emit] += 1
|
|
1628
|
-
state[:tools_on_task] = state[:tools_on_task].to_i + 1
|
|
1668
|
+
state[:tools_on_task] = state[:tools_on_task].to_i + 1 unless name.to_s.match?(/^(memory|session|skills)_recall$/)
|
|
1629
1669
|
state[:emitted_for_batch] = false
|
|
1630
1670
|
chunk = "#{name} #{preview} #{rs.to_s[0, 800]}"
|
|
1631
1671
|
state[:evidence_blob] = "#{state[:evidence_blob]} #{chunk}"
|
|
1632
1672
|
state[:evidence_blob] = state[:evidence_blob][-16_000..] if state[:evidence_blob].to_s.length > 20_000
|
|
1673
|
+
te = (state[:task_evidence] ||= {})
|
|
1674
|
+
idx = state[:plan_idx].to_i
|
|
1675
|
+
te[idx] = "#{te[idx]} #{chunk}"
|
|
1633
1676
|
intent = intent_phrase(tools: [{ name: name.to_s, args: args }])
|
|
1634
1677
|
# Live R2-local signal from tool outcome (executive idx only).
|
|
1635
1678
|
# Full ORM/PRM credit stays in Reward during auto_introspect.
|
|
@@ -65,7 +65,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
65
65
|
|
|
66
66
|
if PWN::AI::Agent::ToolGuard.bashism?(text: cmd) && !PWN::AI::Agent::ToolGuard.shell_bash?
|
|
67
67
|
return PWN::AI::Agent::ToolGuard.invalid_payload(
|
|
68
|
-
hint: 'Command uses bash-only syntax (PIPESTATUS, [[ ]], process substitution, ' \
|
|
68
|
+
hint: 'Command uses bash-only syntax (PIPESTATUS, $RANDOM, [[ ]], process substitution, ' \
|
|
69
69
|
'source, &>). This handler runs /bin/sh (dash) unless ' \
|
|
70
70
|
'PWN::Env[:ai][:agent][:shell_bash]=true. Rewrite as POSIX or opt in to bash.'
|
|
71
71
|
)
|
data/lib/pwn/version.rb
CHANGED
|
@@ -434,6 +434,97 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
434
434
|
expect(src).to match(/engine hop failed|engine_blip/)
|
|
435
435
|
end
|
|
436
436
|
|
|
437
|
+
it 'compacts tool history for every engine, not only local' do
|
|
438
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
439
|
+
expect(src).to match(/compact_history!\(messages: messages\)/)
|
|
440
|
+
expect(src).not_to match(/compact_history!\(messages: messages\) if local/)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it 'shrinks tool bodies when history is fat' do
|
|
444
|
+
msgs = [
|
|
445
|
+
{ role: 'system', content: 'sys' },
|
|
446
|
+
{ role: 'user', content: 'goal' }
|
|
447
|
+
]
|
|
448
|
+
8.times do |n|
|
|
449
|
+
msgs << { role: 'assistant', content: '', tool_calls: [{ id: "c#{n}" }] }
|
|
450
|
+
msgs << { role: 'tool', tool_call_id: "c#{n}", name: 'shell', content: ('x' * 4_000) }
|
|
451
|
+
end
|
|
452
|
+
out = described_class.send(:compact_history!, messages: msgs)
|
|
453
|
+
tool_bodies = out.select { |m| m[:role].to_s == 'tool' }
|
|
454
|
+
expect(tool_bodies.length).to be <= 6
|
|
455
|
+
expect(tool_bodies.map { |m| m[:content].to_s.length }.max).to be <= 2_100
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it 'compacts again after a transient engine hop before retrying' do
|
|
459
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
460
|
+
hop = src[/if engine_transient\?.*?raise/m]
|
|
461
|
+
expect(hop).to match(/compact_history!/)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
it 'keeps distinct tool results instead of six copies of the same empty cut' do
|
|
465
|
+
msgs = [
|
|
466
|
+
{ role: 'system', content: 'sys' },
|
|
467
|
+
{ role: 'user', content: 'goal' },
|
|
468
|
+
{ role: 'assistant', content: '', tool_calls: [{ id: 'good' }] },
|
|
469
|
+
{ role: 'tool', tool_call_id: 'good', name: 'shell', content: 'inet 10.3.3.20/27 brd 10.3.3.31 eth0' }
|
|
470
|
+
]
|
|
471
|
+
8.times do |n|
|
|
472
|
+
msgs << { role: 'assistant', content: '', tool_calls: [{ id: "c#{n}" }] }
|
|
473
|
+
msgs << { role: 'tool', tool_call_id: "c#{n}", name: 'shell', content: "\n" }
|
|
474
|
+
end
|
|
475
|
+
out = described_class.send(:compact_history!, messages: msgs)
|
|
476
|
+
tool_bodies = out.select { |m| m[:role].to_s == 'tool' }.map { |m| m[:content].to_s }
|
|
477
|
+
expect(tool_bodies).to include('inet 10.3.3.20/27 brd 10.3.3.31 eth0')
|
|
478
|
+
expect(tool_bodies.count { |c| c.strip.empty? }).to be <= 1
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it 'keeps an assistant with two tool_calls glued to both tool results' do
|
|
482
|
+
msgs = [
|
|
483
|
+
{ role: 'system', content: 'sys' },
|
|
484
|
+
{ role: 'user', content: 'goal' },
|
|
485
|
+
{
|
|
486
|
+
role: 'assistant', content: '',
|
|
487
|
+
tool_calls: [{ id: 'toolu_a' }, { id: 'toolu_b' }]
|
|
488
|
+
},
|
|
489
|
+
{ role: 'tool', tool_call_id: 'toolu_a', name: 'memory_recall', content: 'prior findings' },
|
|
490
|
+
{ role: 'tool', tool_call_id: 'toolu_b', name: 'skills_recall', content: 'cwe skill' }
|
|
491
|
+
]
|
|
492
|
+
8.times do |n|
|
|
493
|
+
msgs << { role: 'assistant', content: '', tool_calls: [{ id: "c#{n}" }] }
|
|
494
|
+
msgs << { role: 'tool', tool_call_id: "c#{n}", name: 'shell', content: "out#{n}" }
|
|
495
|
+
end
|
|
496
|
+
out = described_class.send(:compact_history!, messages: msgs)
|
|
497
|
+
ids = out.select { |m| m[:role].to_s == 'tool' }.map { |m| m[:tool_call_id].to_s }
|
|
498
|
+
ids.each do |tid|
|
|
499
|
+
prev = nil
|
|
500
|
+
out.each do |m|
|
|
501
|
+
break if m[:role].to_s == 'tool' && m[:tool_call_id].to_s == tid
|
|
502
|
+
|
|
503
|
+
prev = m if m[:role].to_s == 'assistant'
|
|
504
|
+
end
|
|
505
|
+
call_ids = Array(prev && prev[:tool_calls]).map { |tc| (tc[:id] || tc['id']).to_s }
|
|
506
|
+
expect(call_ids).to include(tid)
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it 'drops orphan tool results that have no matching tool_use' do
|
|
511
|
+
msgs = [
|
|
512
|
+
{ role: 'system', content: 'sys' },
|
|
513
|
+
{ role: 'user', content: 'goal' },
|
|
514
|
+
{ role: 'tool', tool_call_id: 'toolu_orphan', name: 'shell', content: 'stray' },
|
|
515
|
+
{ role: 'assistant', content: '', tool_calls: [{ id: 'toolu_ok' }] },
|
|
516
|
+
{ role: 'tool', tool_call_id: 'toolu_ok', name: 'shell', content: 'ok' }
|
|
517
|
+
]
|
|
518
|
+
out = described_class.send(:repair_tool_history!, messages: msgs)
|
|
519
|
+
expect(out.map { |m| m[:tool_call_id] }).not_to include('toolu_orphan')
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
it 'extinguishes a repeated identical payload by signature, not the whole tool' do
|
|
523
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
524
|
+
expect(src).to match(/no_progress|same_payload|payload_sig/)
|
|
525
|
+
expect(src).not_to match(/pwn_extinguished\[opts\[:name\]\.to_s\] = true/)
|
|
526
|
+
end
|
|
527
|
+
|
|
437
528
|
it 'records a timeout increment mistake instead of treating success:true as ok' do
|
|
438
529
|
tmp = Dir.mktmpdir
|
|
439
530
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
@@ -36,6 +36,14 @@ describe 'P0 signal hygiene (handler + inbox + policy-cold + calibrate)' do
|
|
|
36
36
|
expect(r[:hint].to_s).to match(/bash-only/i)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
it 'rejects $RANDOM under /bin/sh so the model rewrites instead of getting USER=' do
|
|
40
|
+
expect(PWN::AI::Agent::ToolGuard.bashism?(text: 'U="p4tester$RANDOM"; echo $U')).to eq(true)
|
|
41
|
+
handler = PWN::AI::Agent::Registry.lookup(name: 'shell').handler
|
|
42
|
+
r = handler.call(command: 'U="p4tester$RANDOM"; echo $U')
|
|
43
|
+
expect(r[:error]).to eq('invalid_payload')
|
|
44
|
+
expect(r[:hint].to_s).to match(/RANDOM|bash-only|POSIX/i)
|
|
45
|
+
end
|
|
46
|
+
|
|
39
47
|
it 'does not refuse pwn_eval host-discovery as unauthorized' do
|
|
40
48
|
src = File.read('/opt/pwn/lib/pwn/ai/agent/tools/ruby_eval.rb')
|
|
41
49
|
expect(src).not_to match(/recon_blocked/)
|
|
@@ -425,6 +425,16 @@ describe PWN::AI::Agent::TaskSummarizer do
|
|
|
425
425
|
)
|
|
426
426
|
end
|
|
427
427
|
|
|
428
|
+
it 'splits analyze-plus-artifact goals instead of pasting the full request twice' do
|
|
429
|
+
goal = 'Exhaustively analyze the running application for vulnerabilities. ' \
|
|
430
|
+
'Once complete, generate a complete PDF report and store in /tmp/container_pentest-p4.pdf'
|
|
431
|
+
tasks = described_class.fallback_decompose(goal: goal)
|
|
432
|
+
expect(tasks.length).to be >= 2
|
|
433
|
+
expect(tasks.join("\n")).to include('/tmp/container_pentest-p4.pdf')
|
|
434
|
+
expect(tasks.grep(/Understand the request:/).length).to eq(0)
|
|
435
|
+
expect(tasks.grep(/Carry out the core work for:/).length).to eq(0)
|
|
436
|
+
end
|
|
437
|
+
|
|
428
438
|
it 'accepts injected :tasks / :llm_tasks without network I/O' do
|
|
429
439
|
custom = ['step alpha', 'step beta', 'verify completion']
|
|
430
440
|
tasks = described_class.plan(request: 'anything at all', tasks: custom)
|
|
@@ -884,6 +894,57 @@ describe PWN::AI::Agent::TaskSummarizer do
|
|
|
884
894
|
expect(described_class.plan_open?(state: st)).to eq true
|
|
885
895
|
end
|
|
886
896
|
|
|
897
|
+
it 'does not complete a subnet scan on leftover identify text or Nmap/scan word salad' do
|
|
898
|
+
st = described_class.fresh(request: 'what hosts are alive on this subnet?')
|
|
899
|
+
st[:plan] = [
|
|
900
|
+
'Identify the local network address and mask',
|
|
901
|
+
'Scan the identified subnet for active or reachable hosts',
|
|
902
|
+
'Present the list of discovered live host IP addresses'
|
|
903
|
+
]
|
|
904
|
+
st[:plan_idx] = 0
|
|
905
|
+
3.times do |i|
|
|
906
|
+
described_class.record!(
|
|
907
|
+
state: st,
|
|
908
|
+
name: 'shell',
|
|
909
|
+
args: { 'command' => "ip -4 addr #{i}" },
|
|
910
|
+
result: '{"success":true,"result":{"stdout":"inet 10.3.3.20/27 brd 10.3.3.31 eth0","exit":0}}'
|
|
911
|
+
)
|
|
912
|
+
end
|
|
913
|
+
expect(st[:plan_idx]).to eq 1
|
|
914
|
+
3.times do
|
|
915
|
+
described_class.record!(
|
|
916
|
+
state: st,
|
|
917
|
+
name: 'shell',
|
|
918
|
+
args: { 'command' => 'nmap -sn 10.3.3.20/27 | grep Nmap | cut -d " " -f 2' },
|
|
919
|
+
result: '{"success":true,"result":{"stdout":"Nmap\\nscan\\nscan\\nscan\\nscan","exit":0}}'
|
|
920
|
+
)
|
|
921
|
+
end
|
|
922
|
+
expect(st[:plan_idx]).to eq 1
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
it 'does not finish an analysis task on recall plus one recon shell' do
|
|
926
|
+
st = described_class.fresh(request: 'exhaustively analyze the running app then write a PDF')
|
|
927
|
+
st[:plan] = [
|
|
928
|
+
'Carry out the requested analysis using the named skills and live evidence',
|
|
929
|
+
'Write the requested report to /tmp/container_pentest-p4.pdf'
|
|
930
|
+
]
|
|
931
|
+
st[:plan_idx] = 0
|
|
932
|
+
described_class.record!(
|
|
933
|
+
state: st, name: 'memory_recall', args: { 'query' => 'securebank' },
|
|
934
|
+
result: '{"success":true,"result":{"x":{"value":"old findings list that is easily over 40 characters long"}}}'
|
|
935
|
+
)
|
|
936
|
+
described_class.record!(
|
|
937
|
+
state: st, name: 'skills_recall', args: { 'query' => 'cwe' },
|
|
938
|
+
result: '{"success":true,"result":[{"name":"cwe","snippet":"more than forty characters of skill text here"}]}'
|
|
939
|
+
)
|
|
940
|
+
described_class.record!(
|
|
941
|
+
state: st, name: 'shell',
|
|
942
|
+
args: { 'command' => 'docker ps; curl -s http://127.0.0.1:5000/ | head' },
|
|
943
|
+
result: '{"success":true,"result":{"stdout":"permission denied docker.sock\\nHTTP/1.1 200 OK Kestrel","exit":0}}'
|
|
944
|
+
)
|
|
945
|
+
expect(st[:plan_idx]).to eq 0
|
|
946
|
+
end
|
|
947
|
+
|
|
887
948
|
it 'a verify task is covered after the verifier ran, even with remaining offenses' do
|
|
888
949
|
st = described_class.fresh(request: 'run rubocop')
|
|
889
950
|
st[:plan] = [
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -359,17 +359,21 @@
|
|
|
359
359
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.maybe_refresh_extro_snapshot! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.maybe_refresh_extro_snapshot!`: "}]}
|
|
360
360
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.mistake_ts Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.mistake_ts`: "}]}
|
|
361
361
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.needs_host_work? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.needs_host_work?`: "}]}
|
|
362
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.no_progress_result Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.no_progress_result`: "}]}
|
|
362
363
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.normalize_llm Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.normalize_llm`: Supported Method Parameters\n\nmsg = PWN::AI::Agent::Loop.normalize_llm(\n\nresponse: 'required - chat_with_tools response Hash from any provider'\n\n)\n"}]}
|
|
364
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.note_same_payload! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.note_same_payload!`: "}]}
|
|
363
365
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.ollama_wire_messages Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.ollama_wire_messages`: Supported Method Parameters\n\nwire = PWN::AI::Agent::Loop.ollama_wire_messages(\n\nmessages: 'required - in-memory OpenAI-ish messages (may have String args)'\n\n)\n\nReturns a deep-copied array safe for Ollama / Open WebUI ollama/api/chat:\n\nparses JSON-string function.arguments into Hash/Array objects\n\ncoerces nil assistant content to ” when tool_calls present (Open WebUI GenerateChatCompletionForm rejects content:null alone)\n\ndrops _native_content / _text_tool_coerced / thinking private keys\n\nstringifies Hash/Array message content (tool results) to JSON text\n\n"}]}
|
|
364
366
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.ollama_wire_tool_call Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.ollama_wire_tool_call`: "}]}
|
|
365
367
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.openai_wire_messages Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.openai_wire_messages`: Supported Method Parameters\n\nwire = PWN::AI::Agent::Loop.openai_wire_messages(\n\nmessages: 'required - in-memory OpenAI-ish messages (may have Hash args / internal keys)'\n\n)\n\nReturns a deep-copied array safe for OpenAI / xAI chat.completions:\n\ndrops _native_content / _text_tool_coerced / thinking private keys\n\nstringifies function.arguments maps\n\ncoerces Hash/non-string content to JSON/string (nil kept for assistant tool turns)\n\n"}]}
|
|
366
368
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.openai_wire_tool_call Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.openai_wire_tool_call`: "}]}
|
|
367
369
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.parse_tool_arguments Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.parse_tool_arguments`: "}]}
|
|
370
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.payload_sig Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.payload_sig`: "}]}
|
|
368
371
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.plan_first Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.plan_first`: "}]}
|
|
369
372
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.publish_usage Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.publish_usage`: "}]}
|
|
370
373
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.quiet_debug_tui! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.quiet_debug_tui!`: "}]}
|
|
371
374
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.recall_target Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.recall_target`: "}]}
|
|
372
375
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.record_metrics Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.record_metrics`: "}]}
|
|
376
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.repair_tool_history! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.repair_tool_history!`: "}]}
|
|
373
377
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.request_intent Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.request_intent`: "}]}
|
|
374
378
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.request_need Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.request_need`: "}]}
|
|
375
379
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.request_unsatisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.request_unsatisfied?`: "}]}
|
|
@@ -670,6 +674,7 @@
|
|
|
670
674
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.handoff_to_next? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.handoff_to_next?`: "}]}
|
|
671
675
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.help`: "}]}
|
|
672
676
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.heuristic_decompose Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.heuristic_decompose`: Supported Method Parameters\n\ntasks = PWN::AI::Agent::TaskSummarizer.heuristic_decompose(\n\ngoal: 'required - user goal string'\n\n)\n"}]}
|
|
677
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.host_shaped_task? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.host_shaped_task?`: "}]}
|
|
673
678
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.howto_goal? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.howto_goal?`: "}]}
|
|
674
679
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.immutable_request_line Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.immutable_request_line`: "}]}
|
|
675
680
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.intent_phase Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.intent_phase`: "}]}
|