pwn 0.5.787 → 0.5.792

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: ce00a627ad7114375ec35258f45bd003f3fb0799851af992d5a1031d59d1a2d6
4
- data.tar.gz: 6687afae44eb7e95191584266cc65c09033c68cc42c0b649091dcc6e7ff269d4
3
+ metadata.gz: 0d9faf1e37903f9f0bf4c98030427b5f62fae266b50e02a85bf70f7732515abf
4
+ data.tar.gz: cffff5cf764a901a9ebabe6c840daeaa64f52e286185d48856c0166ea268457f
5
5
  SHA512:
6
- metadata.gz: 23d19fc265c256a615ec04fc1f6772f7c3924ffad70e94b8725afadba0651319610e4cf2fbfb57803122e3ca7e69cbd01f9517292414b9294538661f8d84fe58
7
- data.tar.gz: 476d7566a80d68b207d3c2e947bcba724251ad4adee393a5dca8992e52ed8953f8dc8f4f9b7ad7358c7eb8514db4c787739e25786d1b3206bf036e76ba31cc12
6
+ metadata.gz: a08a110a62e07958a85431b858c4590c41af6d49c42bd3f70dbbacd37231bb4eee363a1279139dfb7b7679f676bde6062ce99ccca19f02f715075510aa8896f3
7
+ data.tar.gz: 9293669aaef1c48e4d48cf67fccf5f237b4517ca2da3e93956053510be5887b5b91ddfed01fa271ceb221db45668a1c59d5c4ea52e6f0a73969ba8cc34da3f3d
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ gemspec
11
11
  # In some circumstances custom flags are passed to gems in order
12
12
  # to build appropriately. Defer to ./reinstall_pwn_gemset.sh
13
13
  # to review these custom flags (e.g. pg, serialport, etc).
14
- gem 'activesupport', '<8.1.4'
14
+ gem 'activesupport', '<8.1.4.1'
15
15
  gem 'anemone', '0.7.2'
16
16
  gem 'authy', '3.0.1'
17
17
  gem 'aws-sdk', '3.3.0'
@@ -34,12 +34,16 @@ Maintenance still defers when the Learning writer is busy.
34
34
 
35
35
  Before a new folder is admitted, oldest completed generations may be expired
36
36
  to make room. That includes a capture pin that is not listed in the retention
37
- policy and not named by current evidence. Prepared/interrupted generations,
38
- retention-policy pins and current literal evidence references remain protected.
39
- Malformed reference data fails closed when expiry would be needed. If those
40
- protected folders occupy the seven-folder limit, the next producer fails before
41
- modifying its source. Unknown legacy folders count toward capacity but are not
42
- silently certified or expired.
37
+ policy and not named by current evidence. It also includes a published
38
+ pre-generation snapshot named `YYYYMMDD_HHMMSS` or `YYYYMMDD_HHMMSS_<8 hex>`
39
+ that has no interrupted generation record, and an unpublished `.partial-`
40
+ copy that was never renamed into a restore name. Those are not certified as
41
+ complete; they are only eligible so one verified snapshot can be published.
42
+ Prepared/interrupted generations, retention-policy pins and current literal
43
+ evidence references remain protected. Malformed reference data fails closed
44
+ when expiry would be needed. If those protected folders occupy the seven-folder
45
+ limit, the next producer fails before modifying its source. Unknown folder
46
+ names still count toward capacity and are not silently expired.
43
47
 
44
48
  Transactions publish `prepared` before payload/source changes and `complete`
45
49
  only after successful return. Exceptions, interrupted/blocked results and failed
data/lib/pwn/backups.rb CHANGED
@@ -12,6 +12,10 @@ module PWN
12
12
  module Backups
13
13
  LIMIT = 7
14
14
  GENERATION = /\A\d{8}_\d{6}_[a-f0-9]{8}\z/
15
+ # Published Migrate.backup! names from before .generation.json. Not an unknown folder.
16
+ LEGACY_SNAPSHOT = /\A\d{8}_\d{6}\z/
17
+ # Unpublished housekeeping-era copy. Rename was the publish step, so this is not a restore name.
18
+ PARTIAL_SNAPSHOT = /\A\.partial-\d{8}_\d{6}(?:_[a-f0-9]{8})?\z/
15
19
 
16
20
  # Supported Method Parameters::
17
21
  # PWN::Backups.active?
@@ -69,23 +73,13 @@ module PWN
69
73
  pins = names.length >= LIMIT ? reference_pins(root: root) : []
70
74
  raise IOError, 'Pinned backup missing' unless (pins - names).empty?
71
75
 
72
- candidates = names.select do |name|
73
- next false unless name.match?(GENERATION) && !pins.include?(name)
74
-
75
- path = File.join(root, name)
76
- safe_directory(path: path)
77
- state = File.join(path, '.generation.json')
78
- next false unless File.file?(state) && !File.symlink?(state)
79
-
80
- record = JSON.parse(File.binread(state))
81
- # Capture pins that nothing references may be expired. Policy pins and
82
- # evidence references are already in `pins`. Interrupted generations are not.
83
- record['version'] == 1 && record['state'] == 'complete'
84
- end
76
+ candidates = names.select { |name| expirable_name?(name: name, root: root, pins: pins) }
85
77
  count = [names.length - LIMIT + 1, 0].max
86
78
  raise IOError, 'Backup capacity occupied by legacy, pinned or interrupted generations' if candidates.length < count
87
79
 
88
- candidates.sort_by { |name| File.mtime(File.join(root, name)).to_r }.first(count).each { |name| FileUtils.remove_entry_secure(File.join(root, name)) }
80
+ candidates.sort_by { |name| [File.mtime(File.join(root, name)).to_r, name] }.first(count).each do |name|
81
+ FileUtils.remove_entry_secure(File.join(root, name))
82
+ end
89
83
  context[:directory] = File.join(root, "#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(4)}")
90
84
  Dir.mkdir(context[:directory], 0o700)
91
85
  write_state(context: context, state: 'prepared')
@@ -285,6 +279,44 @@ module PWN
285
279
  raise IOError, "Unreadable backup references: #{e.class}"
286
280
  end
287
281
 
282
+ # Unreferenced complete generations, published pre-generation snapshots, and
283
+ # unpublished partials may be expired. Policy pins, evidence references,
284
+ # interrupted generations, and unknown folder names may not.
285
+ private_class_method def self.expirable_name?(opts = {})
286
+ name = opts[:name].to_s
287
+ root = opts[:root]
288
+ return false if opts[:pins].include?(name)
289
+
290
+ path = File.join(root, name)
291
+ return false unless owned_directory?(path: path)
292
+ return true if name.match?(PARTIAL_SNAPSHOT)
293
+ return false unless name.match?(GENERATION) || name.match?(LEGACY_SNAPSHOT)
294
+
295
+ state = File.join(path, '.generation.json')
296
+ return true unless File.exist?(state) || File.symlink?(state)
297
+ return false unless File.file?(state) && !File.symlink?(state)
298
+
299
+ record = JSON.parse(File.binread(state))
300
+ record.is_a?(Hash) && record['version'] == 1 && record['state'] == 'complete'
301
+ rescue JSON::ParserError
302
+ false
303
+ end
304
+
305
+ private_class_method def self.owned_directory?(opts = {})
306
+ path = opts[:path]
307
+ ancestor = path
308
+ loop do
309
+ return false if File.symlink?(ancestor)
310
+ break if File.dirname(ancestor) == ancestor
311
+
312
+ ancestor = File.dirname(ancestor)
313
+ end
314
+ stat = File.stat(path)
315
+ stat.directory? && stat.uid == Process.uid
316
+ rescue SystemCallError
317
+ false
318
+ end
319
+
288
320
  private_class_method def self.safe_directory(opts = {})
289
321
  path = opts[:path]
290
322
  ancestor = path
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.787'
4
+ VERSION = '0.5.792'
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'open3'
4
5
  require 'tmpdir'
5
6
 
6
7
  RSpec.describe 'PWN::AI::Agent::MissionNamespace compiler-free process ownership' do
@@ -12,6 +13,16 @@ RSpec.describe 'PWN::AI::Agent::MissionNamespace compiler-free process ownership
12
13
  PWN::AI::Agent::MissionNamespace.force_exec = previous
13
14
  end
14
15
 
16
+ it 'does not ask newuidmap for a host uid range starting at 0' do
17
+ source = File.read(File.expand_path('../../../../support/mission_namespace.rb', __dir__))
18
+ pid_token = ['#', '{pid}'].join
19
+ expect(source).not_to include('--map-users=0:0:65536')
20
+ expect(source).to include("/proc/#{pid_token}/uid_map")
21
+ expect(source.index('Process.groups = []')).to be < source.index("/proc/#{pid_token}/setgroups")
22
+ expect(source).to include('exit! status')
23
+ expect(source).not_to match(/^\s*exit status\b/)
24
+ end
25
+
15
26
  it 'loads through root and direct requires without suppressing sibling autoloads' do
16
27
  %w[pwn pwn/ai/agent/mission_namespace].each do |entry|
17
28
  code = <<~RUBY
@@ -49,6 +49,9 @@ describe PWN::Backups do
49
49
 
50
50
  it 'pins actual evidence paths but not quoted inventory prose' do
51
51
  copies = 7.times.map { described_class.capture(path: @source) }
52
+ generations = copies.map { |path| File.dirname(path, 2) }
53
+ generations.each_with_index { |dir, index| File.utime(Time.at(1_700_000_100 + index), Time.at(1_700_000_100 + index), dir) }
54
+ File.utime(Time.at(1_700_000_000), Time.at(1_700_000_000), generations[1])
52
55
  File.write(File.join(@home, '.pwn', 'learning.jsonl'), JSON.generate(evidence_path: copies.first, prose: "old path #{copies[1]}"))
53
56
  described_class.capture(path: @source)
54
57
  expect(File).to exist(copies.first)
@@ -119,6 +122,67 @@ describe PWN::Backups do
119
122
  expect(File.read(result[:moved].fetch(legacy))).to eq('legacy bytes')
120
123
  end
121
124
 
125
+ it 'expires an unreferenced legacy timestamp snapshot and keeps a referenced one' do
126
+ root = File.join(@home, '.pwn', 'backup')
127
+ FileUtils.mkdir_p(root)
128
+ dirs = 7.times.map do |index|
129
+ dir = File.join(root, format('2020010%d_000000', index + 1))
130
+ FileUtils.mkdir_p(dir)
131
+ File.write(File.join(dir, 'learning.jsonl'), "legacy-#{index}")
132
+ File.utime(Time.at(1_600_000_000 + index), Time.at(1_600_000_000 + index), dir)
133
+ dir
134
+ end
135
+ File.write(File.join(@home, '.pwn', 'learning.jsonl'), JSON.generate(evidence_path: "#{dirs.last}/learning.jsonl"))
136
+ described_class.capture(path: @source, label: 'migration')
137
+ expect(File).not_to exist(dirs.first)
138
+ expect(File).to exist(dirs.last)
139
+ expect(Dir.children(root).count { |name| File.directory?(File.join(root, name)) }).to eq(7)
140
+ end
141
+
142
+ it 'expires an unreferenced pre-generation hex snapshot that has no generation record' do
143
+ root = File.join(@home, '.pwn', 'backup')
144
+ FileUtils.mkdir_p(root)
145
+ dirs = 7.times.map do |index|
146
+ hex = (index + 1).to_s(16).rjust(8, '0')
147
+ dir = File.join(root, "2020020#{index + 1}_000000_#{hex}")
148
+ FileUtils.mkdir_p(dir)
149
+ File.utime(Time.at(1_600_000_100 + index), Time.at(1_600_000_100 + index), dir)
150
+ dir
151
+ end
152
+ described_class.capture(path: @source, label: 'migration')
153
+ expect(File).not_to exist(dirs.first)
154
+ expect(File).to exist(dirs.last)
155
+ end
156
+
157
+ it 'expires an unpublished partial copy that was never renamed into a restore name' do
158
+ root = File.join(@home, '.pwn', 'backup')
159
+ FileUtils.mkdir_p(root)
160
+ dirs = 7.times.map do |index|
161
+ dir = File.join(root, ".partial-2020040#{index + 1}_000000_abcd1234")
162
+ FileUtils.mkdir_p(dir)
163
+ File.utime(Time.at(1_600_000_200 + index), Time.at(1_600_000_200 + index), dir)
164
+ dir
165
+ end
166
+ described_class.capture(path: @source, label: 'migration')
167
+ expect(File).not_to exist(dirs.first)
168
+ expect(Dir.children(root).count { |name| File.directory?(File.join(root, name)) }).to eq(7)
169
+ end
170
+
171
+ it 'does not expire unknown folders or an interrupted legacy snapshot to free capacity' do
172
+ root = File.join(@home, '.pwn', 'backup')
173
+ unknown = 6.times.map do |index|
174
+ dir = File.join(root, "unknown-#{index}")
175
+ FileUtils.mkdir_p(dir)
176
+ dir
177
+ end
178
+ interrupted = File.join(root, '20200301_000000')
179
+ FileUtils.mkdir_p(interrupted)
180
+ File.write(File.join(interrupted, '.generation.json'), JSON.generate(version: 1, state: 'prepared', pinned: false))
181
+ expect { described_class.capture(path: @source) }.to raise_error(IOError, /capacity/)
182
+ expect(unknown.all? { |dir| File.directory?(dir) }).to be(true)
183
+ expect(File).to exist(interrupted)
184
+ end
185
+
122
186
  it 'tightens an owned group-writable backup directory before writing' do
123
187
  root = File.join(@home, '.pwn', 'backup')
124
188
  FileUtils.mkdir_p(root)
@@ -381,8 +381,42 @@ describe PWN::Migrate, 'backup housekeeping' do
381
381
  result = described_class.prune_backups(dry_run: false, keep: 3, expire_history: true)
382
382
  expect(result).to include(keep: 7, scope: 'global', deleted: [])
383
383
  expect(Dir.children(root).length).to eq(8)
384
- expect { described_class.send(:backup!) }.to raise_error(IOError)
385
- expect(Dir.children(root).grep(/\A\d/).length).to eq(8)
384
+ end
385
+
386
+ it 'expires oldest unreferenced legacy snapshots so migration can publish one verified generation' do
387
+ root = described_class::BACKUP_ROOT
388
+ dirs = 8.times.map do |i|
389
+ dir = File.join(root, format('2026010%d_000000', i + 1))
390
+ FileUtils.mkdir_p(dir)
391
+ File.write(File.join(dir, 'learning.jsonl'), "unique-#{i}")
392
+ File.utime(Time.at(1_700_000_000 + i), Time.at(1_700_000_000 + i), dir)
393
+ dir
394
+ end
395
+ path = described_class.send(:backup!)
396
+ expect(File).not_to exist(dirs[0])
397
+ expect(File).not_to exist(dirs[1])
398
+ expect(File).to exist(dirs.last)
399
+ expect(JSON.parse(File.binread(File.join(path, '.snapshot-manifest.json')))['complete']).to be(true)
400
+ expect(Dir.children(root).grep(/\A\d/).length).to eq(7)
401
+ end
402
+
403
+ it 'admits a schema migration snapshot when seven legacy timestamp folders already fill the store' do
404
+ root = described_class::BACKUP_ROOT
405
+ FileUtils.mkdir_p(root)
406
+ dirs = 7.times.map do |i|
407
+ dir = File.join(root, format('2025010%d_000000', i + 1))
408
+ FileUtils.mkdir_p(dir)
409
+ File.write(File.join(dir, 'learning.jsonl'), "legacy-#{i}")
410
+ File.utime(Time.at(1_700_100_000 + i), Time.at(1_700_100_000 + i), dir)
411
+ dir
412
+ end
413
+ File.write(described_class::SCHEMA_FILE, JSON.generate(schema: 11))
414
+ result = described_class.run(io: io)
415
+ expect(result[:applied_migrations]).to eq([12])
416
+ expect(result[:backup]).to be_a(String)
417
+ expect(File).not_to exist(dirs.first)
418
+ expect(File).to exist(File.join(result[:backup], '.snapshot-manifest.json'))
419
+ expect(Dir.children(root).count { |name| File.directory?(File.join(root, name)) }).to eq(7)
386
420
  end
387
421
 
388
422
  it 'publishes a verified scoped snapshot and fails closed on a copy failure' do
@@ -101,7 +101,15 @@ describe 'Native capability broker daemon' do
101
101
  Process.setpgrp
102
102
  begin
103
103
  if account
104
- Process.groups = []
104
+ begin
105
+ Process.groups = []
106
+ rescue Errno::EPERM
107
+ # The root suite bootstrap denies setgroups before writing gid_map.
108
+ # That parent already cleared supplementary groups, so an empty set
109
+ # is success. A nonempty set is still a failed drop.
110
+ denied = File.file?('/proc/self/setgroups') && File.read('/proc/self/setgroups').strip == 'deny'
111
+ raise unless denied && Process.groups.empty?
112
+ end
105
113
  Process::GID.change_privilege(account.gid)
106
114
  Process::UID.change_privilege(account.uid)
107
115
  expect(Process.groups).to eq([])
@@ -515,13 +515,22 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
515
515
  @screen = screen
516
516
  output = double('terminal', tty?: true)
517
517
  previous = [$stdin, $stdout, $stderr]
518
- Timeout.timeout(60, Class.new(Exception)) do # rubocop:disable Lint/InheritException -- must escape Console rescue StandardError
519
- reader = lambda do
520
- key = keys.empty? ? tick.call : keys.shift
521
- Thread.pass if key.nil?
522
- key
518
+ @console = nil
519
+ allow(PWN::Plugins::REPL::AIConsole::Console).to receive(:new).and_wrap_original do |original, *args, **kwargs|
520
+ @console = original.call(*args, **kwargs)
521
+ end
522
+ expired = Class.new(Exception) # rubocop:disable Lint/InheritException -- must escape Console rescue StandardError
523
+ begin
524
+ Timeout.timeout(60, expired) do
525
+ reader = lambda do
526
+ key = keys.empty? ? tick.call : keys.shift
527
+ Thread.pass if key.nil?
528
+ key
529
+ end
530
+ PWN::Plugins::REPL::AIConsole.run(pry: @pry, input: input, output: output, curses: screen, getch: reader)
523
531
  end
524
- PWN::Plugins::REPL::AIConsole.run(pry: @pry, input: input, output: output, curses: screen, getch: reader)
532
+ rescue expired
533
+ raise expired, "execution expired phase=#{@launch_phase.inspect} session=#{@pry.config.pwn_ai_session_id} paint=#{@paint.join[-240]}"
525
534
  end
526
535
  expect([$stdin, $stdout]).to eq(previous.take(2))
527
536
  expect(PWN::Plugins::Log.raw_stderr).to equal(previous[2])
@@ -2674,15 +2683,26 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
2674
2683
  expect(opts[:messages]).to include(hash_including(role: 'user', content: 'Previous fixture question'), hash_including(role: 'assistant', content: 'Previous fixture answer'))
2675
2684
  { choices: [{ message: { role: 'assistant', content: 'Selected route.', tool_calls: [] } }] }
2676
2685
  end
2677
- keys = "/model openai console-fixture\n/sessions resume resumed console\n".chars
2678
- submitted = false
2686
+ keys = "/model openai console-fixture\n".chars
2687
+ phase = :model
2679
2688
  launch(keys) do
2680
- if !submitted && @pry.config.pwn_ai_session_id == sid
2681
- submitted = true
2682
- keys.concat("what color is a lemon?\n".chars)
2689
+ @launch_phase = phase
2690
+ idle = @console && !@console.send(:busy?)
2691
+ case phase
2692
+ when :model
2693
+ if idle && @paint.join.include?('model=console-fixture')
2694
+ phase = :resume
2695
+ keys.concat("/sessions resume resumed console\n".chars)
2696
+ end
2683
2697
  nil
2684
- elsif @paint.any? { |row| row.include?('Selected route.') }
2685
- "\u0004"
2698
+ when :resume
2699
+ if idle && @pry.config.pwn_ai_session_id == sid
2700
+ phase = :ask
2701
+ keys.concat("what color is a lemon?\n".chars)
2702
+ end
2703
+ nil
2704
+ else
2705
+ "\u0004" if @paint.join.include?('Selected route.')
2686
2706
  end
2687
2707
  end
2688
2708
  expect(@pry.config.pwn_ai_session_id).to eq(sid)
@@ -1,14 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
3
6
  # Unit fixtures contain Ruby closures/mocks that cannot cross exec. Bootstrap
4
7
  # ONLY the test runner into a real user namespace; every mission still creates
5
8
  # and reaps its own PID namespace. Exec integration fixtures force the production
6
9
  # descriptor path separately. No test flag disables kernel containment.
7
10
  # A root runner must map nobody. --map-current-user leaves only uid 0, and
8
11
  # chown/setuid to an unmapped account then fails with EINVAL.
9
- unless PWN::AI::Agent::MissionNamespace.prepared?
10
- args = ['unshare', '--user', '--keep-caps']
11
- args.push(Process.uid.zero? ? '--map-users=0:0:65536' : '--map-current-user')
12
- args.push('--map-groups=0:0:65536') if Process.uid.zero?
13
- exec(*args, RbConfig.ruby, '-I', $LOAD_PATH.join(File::PATH_SEPARATOR), $PROGRAM_NAME, *ARGV)
12
+ # Do not ask unshare to map host uids 0-65535. That flag calls newuidmap, which
13
+ # refuses the range unless /etc/subuid grants it. Root still has
14
+ # CAP_SETUID in the parent namespace and writes the map itself.
15
+ module PwnSpecNamespace
16
+ module_function
17
+
18
+ def bootstrap!
19
+ return if PWN::AI::Agent::MissionNamespace.prepared?
20
+
21
+ if Process.uid.zero?
22
+ bootstrap_root!
23
+ else
24
+ exec('unshare', '--user', '--keep-caps', '--map-current-user', RbConfig.ruby, '-I', $LOAD_PATH.join(File::PATH_SEPARATOR), $PROGRAM_NAME, *ARGV)
25
+ end
26
+ end
27
+
28
+ def bootstrap_root!
29
+ dir = nil
30
+ pid = nil
31
+ # setgroups deny is required before gid_map and then makes Process.groups=
32
+ # return EPERM for every process in the suite. Clear supplementary groups
33
+ # while this parent can still call setgroups. The unshare child inherits
34
+ # the empty set, which is the cleared state the smoke drop expects.
35
+ Process.groups = []
36
+ dir = Dir.mktmpdir('pwn-userns-')
37
+ File.chmod(0o1777, dir)
38
+ ready = File.join(dir, 'ready')
39
+ go = File.join(dir, 'go')
40
+ pid = Process.spawn(
41
+ { 'PWN_NS_READY' => ready, 'PWN_NS_GO' => go },
42
+ 'unshare', '--user', '--keep-caps', '/bin/sh', '-c',
43
+ 'touch "$PWN_NS_READY"; while [ ! -f "$PWN_NS_GO" ]; do sleep 0.05; done; exec "$@"',
44
+ 'sh', RbConfig.ruby, '-I', $LOAD_PATH.join(File::PATH_SEPARATOR), $PROGRAM_NAME, *ARGV
45
+ )
46
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
47
+ sleep 0.02 until File.exist?(ready) || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
48
+ raise 'root user-namespace bootstrap timed out before unshare' unless File.exist?(ready)
49
+
50
+ File.write("/proc/#{pid}/setgroups", 'deny')
51
+ File.write("/proc/#{pid}/gid_map", "0 0 65536\n")
52
+ File.write("/proc/#{pid}/uid_map", "0 0 65536\n")
53
+ File.write(go, "go\n")
54
+ Process.wait(pid)
55
+ status = Process.last_status.exitstatus || 1
56
+ pid = nil
57
+ # Kernel#exit raises SystemExit. RSpec rescues that while loading a spec
58
+ # and reports it as a load failure. exit! ends this parent with the child's
59
+ # status and does not raise. Clean the sync dir first; exit! skips ensure.
60
+ FileUtils.rm_rf(dir) if dir
61
+ dir = nil
62
+ exit! status
63
+ rescue StandardError
64
+ if pid
65
+ Process.kill('KILL', pid)
66
+ Process.wait(pid)
67
+ end
68
+ raise
69
+ ensure
70
+ FileUtils.rm_rf(dir) if dir && File.exist?(dir)
71
+ end
14
72
  end
73
+
74
+ PwnSpecNamespace.bootstrap!
@@ -1900,9 +1900,11 @@
1900
1900
  {"messages":[{"role":"user","content":"PWN::Backups.capture Usage"},{"role":"assistant","content":"`PWN::Backups.capture`: Supported Method Parameters\n\nPWN::Backups.capture(path: ‘/path/to/source’, label: ‘learning’)\n"}]}
1901
1901
  {"messages":[{"role":"user","content":"PWN::Backups.configure Usage"},{"role":"assistant","content":"`PWN::Backups.configure`: Supported Method Parameters\n\nPWN::Backups.configure(root: nil)\n"}]}
1902
1902
  {"messages":[{"role":"user","content":"PWN::Backups.directory Usage"},{"role":"assistant","content":"`PWN::Backups.directory`: Supported Method Parameters\n\nPWN::Backups.directory(label: ‘maintenance’) Caller must hold transaction through the final source mutation and ledger.\n"}]}
1903
+ {"messages":[{"role":"user","content":"PWN::Backups.expirable_name? Usage"},{"role":"assistant","content":"`PWN::Backups.expirable_name?`: "}]}
1903
1904
  {"messages":[{"role":"user","content":"PWN::Backups.help Usage"},{"role":"assistant","content":"`PWN::Backups.help`: "}]}
1904
1905
  {"messages":[{"role":"user","content":"PWN::Backups.legacy_inventory Usage"},{"role":"assistant","content":"`PWN::Backups.legacy_inventory`: "}]}
1905
1906
  {"messages":[{"role":"user","content":"PWN::Backups.legacy_unreferenced Usage"},{"role":"assistant","content":"`PWN::Backups.legacy_unreferenced`: "}]}
1907
+ {"messages":[{"role":"user","content":"PWN::Backups.owned_directory? Usage"},{"role":"assistant","content":"`PWN::Backups.owned_directory?`: "}]}
1906
1908
  {"messages":[{"role":"user","content":"PWN::Backups.reconcile Usage"},{"role":"assistant","content":"`PWN::Backups.reconcile`: Supported Method Parameters\n\nPWN::Backups.reconcile(generation: ‘name’, expected_sha256: ‘digest’, confirmed: true)\n"}]}
1907
1909
  {"messages":[{"role":"user","content":"PWN::Backups.reference_pins Usage"},{"role":"assistant","content":"`PWN::Backups.reference_pins`: "}]}
1908
1910
  {"messages":[{"role":"user","content":"PWN::Backups.relocate_legacy Usage"},{"role":"assistant","content":"`PWN::Backups.relocate_legacy`: Supported Method Parameters\n\nPWN::Backups.relocate_legacy(paths: [], dry_run: true, writers_stopped: false)\n"}]}
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.787
4
+ version: 0.5.792
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "<"
17
17
  - !ruby/object:Gem::Version
18
- version: 8.1.4
18
+ version: 8.1.4.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "<"
24
24
  - !ruby/object:Gem::Version
25
- version: 8.1.4
25
+ version: 8.1.4.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: anemone
28
28
  requirement: !ruby/object:Gem::Requirement