pwn 0.5.698 → 0.5.699
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/lib/pwn/ai/agent/loop.rb +8 -4
- data/lib/pwn/ai/agent/task_summarizer.rb +11 -0
- 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 +27 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +8 -0
- data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +10 -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: 5d7edeea1940ad56143190285565ba9bd76f4d78a47294238eafaad1d002686f
|
|
4
|
+
data.tar.gz: c3a594906ca8d239b8f5dfc3fe3343ec718bf1a71e3c85334b3b6db7e852b5fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fee1edba85d565609b592498feff5eaf38bc01a4771af2a2e22f3719d1eac7faaf54c8f8772837358cf5cbf18db59736aee90fe4a7114c1e6353be423d42e2e8
|
|
7
|
+
data.tar.gz: 4ad32cb15b7bd78d0fa150cf63379ecf61184b84a902426a3581a558b48b17b0a7bc23048a2d2576155b538403e9472a1ebe542a8154eece95a36e4106e352f9
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -1427,8 +1427,10 @@ module PWN
|
|
|
1427
1427
|
messages = opts[:messages]
|
|
1428
1428
|
return messages unless messages.is_a?(Array) && messages.length > 12
|
|
1429
1429
|
|
|
1430
|
-
keep_pairs =
|
|
1431
|
-
|
|
1430
|
+
keep_pairs = opts[:keep_pairs]
|
|
1431
|
+
keep_pairs = (agent_flag(key: :history_keep_tool_pairs, default: 6) || 6).to_i if keep_pairs.nil?
|
|
1432
|
+
max_chars = opts[:max_chars]
|
|
1433
|
+
max_chars = (agent_flag(key: :history_tool_max_chars, default: 2_000) || 2_000).to_i if max_chars.nil?
|
|
1432
1434
|
|
|
1433
1435
|
head = []
|
|
1434
1436
|
rest = messages.dup
|
|
@@ -2491,8 +2493,9 @@ module PWN
|
|
|
2491
2493
|
i = 0
|
|
2492
2494
|
loop do
|
|
2493
2495
|
i += 1
|
|
2494
|
-
# 3.1 — compact
|
|
2495
|
-
|
|
2496
|
+
# 3.1 — compact fat tool dumps so remote ReadTimeout hops do not
|
|
2497
|
+
# retry the same 70-message payload (R1 201215 Anthropic 180s×5).
|
|
2498
|
+
compact_history!(messages: messages)
|
|
2496
2499
|
# English-task-as-primary: when plan_idx advanced, tell the model
|
|
2497
2500
|
# which plain-English task is active before the next tool batch.
|
|
2498
2501
|
inject_task_focus!(messages: messages, state: ts_state, request: request) unless skip_compass
|
|
@@ -2510,6 +2513,7 @@ module PWN
|
|
|
2510
2513
|
final_chars = txt.length
|
|
2511
2514
|
return txt
|
|
2512
2515
|
end
|
|
2516
|
+
compact_history!(messages: messages, keep_pairs: 3, max_chars: 800)
|
|
2513
2517
|
messages << {
|
|
2514
2518
|
role: 'user',
|
|
2515
2519
|
content: '[pwn-ai] engine hop failed (transient). Keep calling CORE_TOOLS. ' \
|
|
@@ -661,6 +661,17 @@ module PWN
|
|
|
661
661
|
]
|
|
662
662
|
end
|
|
663
663
|
|
|
664
|
+
artifact = goal_text[%r{(?:/(?:tmp|var|home|opt|usr)/\S+\.(?:pdf|html|md|json|txt|csv)|~/\S+\.(?:pdf|html|md|json|txt|csv))}i]
|
|
665
|
+
if artifact && goal_lc.match?(/\b(analy[sz]e|test|scan|report|generat|store|write|export)\b/)
|
|
666
|
+
tasks << 'Carry out the requested analysis using the named skills and live evidence'
|
|
667
|
+
tasks << "Write the requested report to #{artifact}"
|
|
668
|
+
if goal_lc.match?(/\b(json|ya?ml|table|csv|tsv)\b/)
|
|
669
|
+
fmt = goal_lc[/\b(json|ya?ml|table|csv|tsv)\b/]
|
|
670
|
+
tasks << "Present the results in #{fmt} format"
|
|
671
|
+
end
|
|
672
|
+
return tasks
|
|
673
|
+
end
|
|
674
|
+
|
|
664
675
|
tasks << "Understand the request: #{truncate_goal(goal: goal_text)}"
|
|
665
676
|
tasks << "Carry out the core work for: #{truncate_goal(goal: goal_text)}"
|
|
666
677
|
|
|
@@ -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,33 @@ 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
|
+
|
|
437
464
|
it 'records a timeout increment mistake instead of treating success:true as ok' do
|
|
438
465
|
tmp = Dir.mktmpdir
|
|
439
466
|
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)
|