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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d7f95442e7e518256ffa6e9f9ce4e6856559430d475621446b07192f22088ad
4
- data.tar.gz: 3030f3616da6d651f08eae290b4a979633074904d001c3bfd16c6ddd81c20de5
3
+ metadata.gz: 7b3dcfffe5307553d194d351676ac3eaf1691d7342fc6464b95e85703cdbbeb4
4
+ data.tar.gz: 1401f535f0537335601e31705528478e4e4db31ffa8a174aadc8daa79c8add88
5
5
  SHA512:
6
- metadata.gz: ed97966a4b047588627e1e3864471171a565e8095fc2c9091b418756d8dcb02ead69db214d18d8d4db2b470ac6e1455dc9307c37ffddcc0b4f36feb45ae93a0e
7
- data.tar.gz: 1c349c364aeed6a41a3ec8b25f21bc4cbadd100e5312df2fc9c0b5ed8916c3410602ad4f5d2ebe6a3edb3dcd8e51aa8ab0d58782679f7822c32a8b498351bfb5
6
+ metadata.gz: e1bd1c31a8df2a4e9c4ced9164dd404aa434d24a1858f037a7f4cbd44a2c50859504fd509776333f99834caa103f4e4f4f90b63cabf117643ea63ab3db6c715f
7
+ data.tar.gz: c229d034c61e8b5ad91e6082d4a3831fbe9f7323b396296e58ceab991fadb15996d624a29d446949d65d2ffa50fe8056806911cb9385b5d3f69b34267c2b197b
data/Gemfile CHANGED
@@ -22,7 +22,7 @@ gem 'brakeman', '8.1.0'
22
22
  gem 'bson', '5.2.0'
23
23
  gem 'bundler', '>=4.0.24'
24
24
  gem 'bundler-audit', '>=0.9.3'
25
- gem 'bunny', '3.5.0'
25
+ gem 'bunny', '3.6.0'
26
26
  gem 'colorize', '1.1.0'
27
27
  gem 'credit_card_validations', '10.0.0'
28
28
  gem 'curses', '1.7.0'
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.794'
4
+ VERSION = '0.5.795'
5
5
  end
@@ -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
- raise expired, "execution expired phase=#{@launch_phase.inspect} session=#{@pry.config.pwn_ai_session_id} paint=#{@paint.join[-240]}"
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
- expect(PWN::Env.dig(:ai, :openai, :model)).to eq('console-fixture')
2683
- expect(opts[:messages]).to include(hash_including(role: 'user', content: 'Previous fixture question'), hash_including(role: 'assistant', content: 'Previous fixture answer'))
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 @paint.join.include?('Selected route.')
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
- Timeout.timeout(10) { sleep 0.01 until File.exist?(marker) }
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
- Timeout.timeout(10) { sleep 0.01 until File.exist?(marker) }
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!)
@@ -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.794
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.5.0
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.5.0
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