pwn 0.5.794 → 0.5.795
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/lib/pwn/version.rb +1 -1
- data/spec/integration/spec_sandbox_isolation_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +25 -6
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +33 -4
- data/spec/support/sandbox.rb +3 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b3dcfffe5307553d194d351676ac3eaf1691d7342fc6464b95e85703cdbbeb4
|
|
4
|
+
data.tar.gz: 1401f535f0537335601e31705528478e4e4db31ffa8a174aadc8daa79c8add88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1bd1c31a8df2a4e9c4ced9164dd404aa434d24a1858f037a7f4cbd44a2c50859504fd509776333f99834caa103f4e4f4f90b63cabf117643ea63ab3db6c715f
|
|
7
|
+
data.tar.gz: c229d034c61e8b5ad91e6082d4a3831fbe9f7323b396296e58ceab991fadb15996d624a29d446949d65d2ffa50fe8056806911cb9385b5d3f69b34267c2b197b
|
data/Gemfile
CHANGED
data/lib/pwn/version.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'offline spec mission isolation' do
|
|
6
|
+
before do
|
|
7
|
+
@outside = Dir.mktmpdir('pwn-outside-mission-')
|
|
8
|
+
stub_const('PWN::AI::Agent::Mission::ROOT', File.join(@outside, 'missions'))
|
|
9
|
+
stub_const('PWN::AI::Agent::OpenGoal::GOAL_FILE', File.join(@outside, 'open_goal.json'))
|
|
10
|
+
PWN::AI::Agent::Mission.begin!(id: 'outside', request: 'Unrelated active mission fixture', unattended: true)
|
|
11
|
+
PWN::AI::Agent::Mission.note_job!(id: 'outside', job_id: 'outside-job', command: 'fixture')
|
|
12
|
+
PWN::AI::Agent::OpenGoal.begin!(request: 'Unrelated active mission fixture', mission_id: 'outside')
|
|
13
|
+
@outside_files = Dir.glob(File.join(@outside, '**', '*')).select { |path| File.file?(path) }.to_h { |path| [path, File.binread(path)] }
|
|
14
|
+
allow(PWN::Plugins::Jobs).to receive(:status).and_call_original
|
|
15
|
+
allow(PWN::Plugins::Jobs).to receive(:status).with(id: 'outside-job').and_return(id: 'outside-job', status: 'RUNNING')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after { FileUtils.remove_entry(@outside) if @outside && Dir.exist?(@outside) }
|
|
19
|
+
|
|
20
|
+
context 'with the shared persistence sandbox' do
|
|
21
|
+
include_context 'pwn tmp sandbox'
|
|
22
|
+
|
|
23
|
+
it 'answers through the provider without consulting or modifying an outside live mission' do
|
|
24
|
+
allow(Dir).to receive(:home).and_return(@tmp)
|
|
25
|
+
allow(PWN::AI::Agent::PromptBuilder).to receive(:build).and_return('Offline fixture')
|
|
26
|
+
allow(PWN::AI::Agent::TaskSummarizer).to receive(:enabled?).and_return(false)
|
|
27
|
+
allow(PWN::AI::Agent::Loop).to receive(:maybe_refresh_extro_snapshot!)
|
|
28
|
+
allow(PWN::AI::Agent::Loop).to receive(:may_finalize?).and_return(true)
|
|
29
|
+
allow(PWN::AI::Agent::Loop).to receive(:should_auto_introspect?).and_return(false)
|
|
30
|
+
PWN::Env[:ai][:active] = :openai
|
|
31
|
+
PWN::Env[:ai][:openai] = { model: 'offline-fixture' }
|
|
32
|
+
allow(PWN::AI::OpenAI).to receive(:chat_with_tools).and_return(choices: [{ message: { role: 'assistant', content: 'Fixture answer.', tool_calls: [] } }])
|
|
33
|
+
sid = PWN::Sessions.create(title: 'Isolation fixture')[:id]
|
|
34
|
+
answer = PWN::AI::Agent::Loop.run(request: 'what color is a lemon?', session_id: sid, enabled_toolsets: [])
|
|
35
|
+
expect(answer).to eq('Fixture answer.')
|
|
36
|
+
expect(PWN::AI::OpenAI).to have_received(:chat_with_tools).once
|
|
37
|
+
expect(PWN::AI::Agent::Mission::ROOT).to eq(File.join(@tmp, 'missions'))
|
|
38
|
+
expect(PWN::AI::Agent::OpenGoal::GOAL_FILE).to eq(File.join(@tmp, 'open_goal.json'))
|
|
39
|
+
expect(PWN::Plugins::Jobs).not_to have_received(:status).with(id: 'outside-job')
|
|
40
|
+
@outside_files.each { |path, bytes| expect(File.binread(path)).to eq(bytes) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -530,7 +530,9 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
530
530
|
PWN::Plugins::REPL::AIConsole.run(pry: @pry, input: input, output: output, curses: screen, getch: reader)
|
|
531
531
|
end
|
|
532
532
|
rescue expired
|
|
533
|
-
|
|
533
|
+
timeline = @console&.instance_variable_get(:@timeline).to_a.last(8).map { |type, text| [type, text.to_s[0, 400]] }
|
|
534
|
+
paint = @paint.last(120).join
|
|
535
|
+
raise expired, "execution expired phase=#{@launch_phase.inspect} session=#{@pry.config.pwn_ai_session_id} timeline=#{timeline.inspect} paint=#{paint[-240, 240] || paint}"
|
|
534
536
|
end
|
|
535
537
|
expect([$stdin, $stdout]).to eq(previous.take(2))
|
|
536
538
|
expect(PWN::Plugins::Log.raw_stderr).to equal(previous[2])
|
|
@@ -2678,34 +2680,51 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
2678
2680
|
PWN::Sessions.append(session_id: sid, role: 'user', content: 'Previous fixture question')
|
|
2679
2681
|
PWN::Sessions.append(session_id: sid, role: 'assistant', content: 'Previous fixture answer')
|
|
2680
2682
|
allow(PWN::Plugins::REPL).to receive(:persist_ai_selection).and_return(false)
|
|
2683
|
+
seen = File.join(@tmp, 'curses-provider-call.json')
|
|
2684
|
+
recorded = false
|
|
2681
2685
|
allow(PWN::AI::OpenAI).to receive(:chat_with_tools) do |opts|
|
|
2682
|
-
|
|
2683
|
-
|
|
2686
|
+
unless recorded
|
|
2687
|
+
recorded = true
|
|
2688
|
+
messages = Array(opts[:messages]).map { |row| { role: row[:role] || row['role'], content: (row[:content] || row['content']).to_s } }
|
|
2689
|
+
File.write(seen, JSON.generate(model: PWN::Env.dig(:ai, :openai, :model), call_model: opts[:model], messages: messages))
|
|
2690
|
+
end
|
|
2684
2691
|
{ choices: [{ message: { role: 'assistant', content: 'Selected route.', tool_calls: [] } }] }
|
|
2685
2692
|
end
|
|
2693
|
+
allow(PWN::AI::Router).to receive(:summarize).and_return('{"title":"Resumed console","summary":"Fixture summary."}')
|
|
2686
2694
|
keys = "/model openai console-fixture\n".chars
|
|
2687
2695
|
phase = :model
|
|
2688
2696
|
launch(keys) do
|
|
2689
2697
|
@launch_phase = phase
|
|
2690
2698
|
idle = @console && !@console.send(:busy?)
|
|
2699
|
+
prompt = @console&.instance_variable_get(:@model_prompt)
|
|
2691
2700
|
case phase
|
|
2692
2701
|
when :model
|
|
2693
|
-
if idle && @paint.join.include?('model=console-fixture')
|
|
2702
|
+
if idle && !prompt && @paint.join.include?('model=console-fixture') && PWN::Env.dig(:ai, :openai, :model) == 'console-fixture'
|
|
2694
2703
|
phase = :resume
|
|
2695
2704
|
keys.concat("/sessions resume resumed console\n".chars)
|
|
2696
2705
|
end
|
|
2697
2706
|
nil
|
|
2698
2707
|
when :resume
|
|
2699
|
-
if idle && @pry.config.pwn_ai_session_id == sid
|
|
2708
|
+
if idle && !prompt && @pry.config.pwn_ai_session_id == sid && PWN::Env.dig(:ai, :openai, :model) == 'console-fixture'
|
|
2700
2709
|
phase = :ask
|
|
2701
2710
|
keys.concat("what color is a lemon?\n".chars)
|
|
2702
2711
|
end
|
|
2703
2712
|
nil
|
|
2704
2713
|
else
|
|
2705
|
-
"\u0004" if
|
|
2714
|
+
"\u0004" if idle
|
|
2706
2715
|
end
|
|
2707
2716
|
end
|
|
2708
2717
|
expect(@pry.config.pwn_ai_session_id).to eq(sid)
|
|
2718
|
+
expect(File).to exist(seen), "Mission finished without calling the provider: #{@console.instance_variable_get(:@timeline).inspect}"
|
|
2719
|
+
observed = JSON.parse(File.read(seen), symbolize_names: true)
|
|
2720
|
+
expect(observed[:model]).to eq('console-fixture')
|
|
2721
|
+
expect(observed[:call_model]).to eq('console-fixture')
|
|
2722
|
+
expect(observed[:messages]).to include(
|
|
2723
|
+
hash_including(role: 'user', content: 'Previous fixture question'),
|
|
2724
|
+
hash_including(role: 'assistant', content: 'Previous fixture answer'),
|
|
2725
|
+
hash_including(role: 'user', content: 'what color is a lemon?')
|
|
2726
|
+
)
|
|
2727
|
+
expect(@paint.join).to include('Selected route.')
|
|
2709
2728
|
expect(PWN::Sessions.load(session_id: sid).map { |row| row[:content] }.join).to include('Selected route.')
|
|
2710
2729
|
end
|
|
2711
2730
|
|
|
@@ -27,6 +27,37 @@ describe 'regex-ReAct mission containment' do
|
|
|
27
27
|
Pry.config.hooks.get_hook(:after_read, :pwn_ai_hook).call(request, pry)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def wait_for_descendant_pid(marker)
|
|
31
|
+
Timeout.timeout(10) do
|
|
32
|
+
loop do
|
|
33
|
+
text = File.read(marker) if File.file?(marker)
|
|
34
|
+
pid = text.to_s[/^NSpid:[ \t]+([1-9]\d*)(?:[ \t]+\d+)+\n/, 1]
|
|
35
|
+
return pid.to_i if pid
|
|
36
|
+
|
|
37
|
+
sleep 0.01
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'waits for the descendant PID contents rather than an empty readiness file' do
|
|
43
|
+
marker = File.join(@tmp, 'partial-ready')
|
|
44
|
+
File.write(marker, '')
|
|
45
|
+
read_empty = Queue.new
|
|
46
|
+
writer = Thread.new do
|
|
47
|
+
read_empty.pop
|
|
48
|
+
File.write(marker, "NSpid:\t12345\t1\n")
|
|
49
|
+
end
|
|
50
|
+
allow(File).to receive(:read).and_call_original
|
|
51
|
+
allow(File).to receive(:read).with(marker).and_wrap_original do |original, path|
|
|
52
|
+
text = original.call(path)
|
|
53
|
+
read_empty << true if text.empty?
|
|
54
|
+
text
|
|
55
|
+
end
|
|
56
|
+
expect(wait_for_descendant_pid(marker)).to eq(12_345)
|
|
57
|
+
ensure
|
|
58
|
+
writer&.kill&.join
|
|
59
|
+
end
|
|
60
|
+
|
|
30
61
|
it 'fails closed before calling a provider when mission containment is unavailable' do
|
|
31
62
|
allow(PWN::Plugins::AISandbox::MissionProcess).to receive(:new).and_raise(PWN::Plugins::AISandbox::NotStarted, 'enable user/PID namespaces')
|
|
32
63
|
expect(PWN::AI::Ollama).not_to receive(:chat)
|
|
@@ -137,8 +168,7 @@ describe 'regex-ReAct mission containment' do
|
|
|
137
168
|
nil
|
|
138
169
|
end
|
|
139
170
|
marker = File.join(@tmp, '.pwn', 'workspaces', 'runtime', 'pty-ready')
|
|
140
|
-
|
|
141
|
-
host_pid = File.read(marker)[/^NSpid:\s+(\d+)/, 1].to_i
|
|
171
|
+
host_pid = wait_for_descendant_pid(marker)
|
|
142
172
|
writer.write("\x03")
|
|
143
173
|
Timeout.timeout(10) { sleep 0.01 until output.include?('CANCELLED READY') }
|
|
144
174
|
expect { Process.kill(0, host_pid) }.to raise_error(Errno::ESRCH)
|
|
@@ -179,8 +209,7 @@ describe 'regex-ReAct mission containment' do
|
|
|
179
209
|
rescue Interrupt
|
|
180
210
|
:cancelled
|
|
181
211
|
end
|
|
182
|
-
|
|
183
|
-
host_pid = File.read(marker)[/^NSpid:\s+(\d+)/, 1].to_i
|
|
212
|
+
host_pid = wait_for_descendant_pid(marker)
|
|
184
213
|
worker.raise(Interrupt)
|
|
185
214
|
expect(Timeout.timeout(10) { worker.value }).to eq(:cancelled)
|
|
186
215
|
expect(repl).to have_received(:ready_tty!)
|
data/spec/support/sandbox.rb
CHANGED
|
@@ -47,7 +47,9 @@ RSpec.shared_context 'pwn tmp sandbox' do
|
|
|
47
47
|
'PWN::AI::Agent::Policy::TRAJECTORY_FILE' => 'policy_traj.jsonl',
|
|
48
48
|
'PWN::AI::Agent::Extrospection::EXTRO_FILE' => 'extrospection.json',
|
|
49
49
|
'PWN::AI::Agent::Swarm::AGENTS_FILE' => 'agents.yml',
|
|
50
|
-
'PWN::AI::Agent::Swarm::SWARM_ROOT' => 'swarm'
|
|
50
|
+
'PWN::AI::Agent::Swarm::SWARM_ROOT' => 'swarm',
|
|
51
|
+
'PWN::AI::Agent::Mission::ROOT' => 'missions',
|
|
52
|
+
'PWN::AI::Agent::OpenGoal::GOAL_FILE' => 'open_goal.json'
|
|
51
53
|
}.each do |const, rel|
|
|
52
54
|
owner = const.split('::')[0..-2].join('::')
|
|
53
55
|
stub_const(const, File.join(@tmp, rel)) if Object.const_defined?(owner)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.795
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -169,14 +169,14 @@ dependencies:
|
|
|
169
169
|
requirements:
|
|
170
170
|
- - '='
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
|
-
version: 3.
|
|
172
|
+
version: 3.6.0
|
|
173
173
|
type: :runtime
|
|
174
174
|
prerelease: false
|
|
175
175
|
version_requirements: !ruby/object:Gem::Requirement
|
|
176
176
|
requirements:
|
|
177
177
|
- - '='
|
|
178
178
|
- !ruby/object:Gem::Version
|
|
179
|
-
version: 3.
|
|
179
|
+
version: 3.6.0
|
|
180
180
|
- !ruby/object:Gem::Dependency
|
|
181
181
|
name: colorize
|
|
182
182
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -6714,6 +6714,7 @@ files:
|
|
|
6714
6714
|
- spec/integration/reports_spec.rb
|
|
6715
6715
|
- spec/integration/result_condition_spec.rb
|
|
6716
6716
|
- spec/integration/sast_functional_spec.rb
|
|
6717
|
+
- spec/integration/spec_sandbox_isolation_spec.rb
|
|
6717
6718
|
- spec/integration/tool_registry_spec.rb
|
|
6718
6719
|
- spec/integration/web_cache_deception_yubikey_spec.rb
|
|
6719
6720
|
- spec/lib/pwn/ai/agent/assembly_spec.rb
|