pwn 0.5.785 → 0.5.787

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.
@@ -6,6 +6,7 @@ require 'open3'
6
6
  require 'rbconfig'
7
7
  require 'tmpdir'
8
8
  require 'tempfile'
9
+ require 'fileutils'
9
10
  require 'timeout'
10
11
 
11
12
  module PWN
@@ -13,7 +14,7 @@ module PWN
13
14
  module Agent
14
15
  # Opt-in, fixed local held-out evaluation. Never loaded by the online loop.
15
16
  module PolicyEvaluation
16
- RUNNER = File.expand_path('../../../../scripts/benchmark_policy.rb', __dir__).freeze
17
+ RUNNER = File.expand_path(__FILE__).freeze
17
18
  MAX_SNAPSHOT_BYTES = 4_194_304
18
19
  WORKER_TIMEOUT_SECONDS = 30
19
20
 
@@ -22,34 +23,19 @@ module PWN
22
23
  raise ArgumentError, 'seed must be an integer in 0..7' unless seed.is_a?(Integer) && (0..7).cover?(seed)
23
24
 
24
25
  snapshots = %i[baseline candidate].to_h { |arm| [arm, read_snapshot(path: opts.fetch(arm))] }
25
- Dir.mktmpdir('pwn-policy-evaluator-', '/tmp') do |root|
26
- Open3.popen3(
27
- { 'HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8' },
28
- RbConfig.ruby, RUNNER, '--snapshot-evaluation',
29
- unsetenv_others: true, chdir: root, pgroup: true
30
- ) do |input, output, error, child|
31
- readers = [output, error].map { |io| Thread.new { io.read } }
32
- begin
33
- Timeout.timeout(WORKER_TIMEOUT_SECONDS) do
34
- input.write(JSON.generate(snapshots: snapshots, seed: seed))
35
- input.close
36
- status = child.value
37
- stdout, stderr = readers.map(&:value)
38
- raise "held-out worker failed: #{stderr}" unless status.success?
26
+ JSON.parse(spawn_worker({ args: ['--snapshot-evaluation'], input: JSON.generate(snapshots: snapshots, seed: seed) }), symbolize_names: true)
27
+ end
39
28
 
40
- JSON.parse(stdout, symbolize_names: true)
41
- end
42
- ensure
43
- begin
44
- Process.kill('KILL', -child.pid)
45
- rescue Errno::ESRCH
46
- nil
47
- end
48
- child.join
49
- readers.each(&:join)
50
- end
51
- end
52
- end
29
+ # Explicit comparison export. Does not promote and is not called by the online loop.
30
+ public_class_method def self.export_training(opts = {})
31
+ dir = File.expand_path(opts.fetch(:dir))
32
+ parent = File.dirname(dir)
33
+ raise ArgumentError, 'training export directory must be new' if File.exist?(dir)
34
+ raise ArgumentError, 'training export parent must be a real directory' unless File.directory?(parent) && File.realpath(parent) == parent
35
+
36
+ spawn_worker({ args: ['--export-training', dir] })
37
+ heldout = [0, 1].map { |seed| evaluate(baseline: File.join(dir, 'off.json'), candidate: File.join(dir, 'on.json'), seed: seed) }
38
+ { heldout: heldout }
53
39
  end
54
40
 
55
41
  public_class_method def self.authors
@@ -146,6 +132,11 @@ module PWN
146
132
  receipt: 'required - successful promote result with snapshot digests and paths'
147
133
  )
148
134
 
135
+ # Write off/on training snapshots and return fresh held-out reports. Does not promote.
136
+ #{self}.export_training(
137
+ dir: 'required - new directory path for off.json and on.json'
138
+ )
139
+
149
140
  # Display module authors.
150
141
  #{self}.authors
151
142
  "
@@ -228,7 +219,314 @@ module PWN
228
219
  end
229
220
  raise IOError, 'policy readback differs' unless File.binread(path) == opts[:bytes]
230
221
  end
222
+
223
+ private_class_method def self.spawn_worker(opts = {})
224
+ args = opts.fetch(:args)
225
+ input = opts[:input]
226
+ Dir.mktmpdir('pwn-policy-evaluator-', '/tmp') do |root|
227
+ Open3.popen3(
228
+ { 'HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8' },
229
+ RbConfig.ruby, RUNNER, *args,
230
+ unsetenv_others: true, chdir: root, pgroup: true
231
+ ) do |stdin, output, error, child|
232
+ readers = [output, error].map { |io| Thread.new { io.read } }
233
+ begin
234
+ Timeout.timeout(WORKER_TIMEOUT_SECONDS) do
235
+ stdin.write(input) if input
236
+ stdin.close
237
+ status = child.value
238
+ stdout, stderr = readers.map(&:value)
239
+ raise "held-out worker failed: #{stderr}" unless status.success?
240
+
241
+ stdout
242
+ end
243
+ ensure
244
+ begin
245
+ Process.kill('KILL', -child.pid)
246
+ rescue Errno::ESRCH
247
+ nil
248
+ end
249
+ child.join
250
+ readers.each(&:join)
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ private_class_method def self.refuse_live_agent
257
+ loaded = $LOADED_FEATURES.any? { |path| File.basename(path) == 'pwn.rb' }
258
+ raise 'Run in a fresh Ruby process; do not load the live agent' if loaded
259
+ end
260
+
261
+ private_class_method def self.with_isolated
262
+ refuse_live_agent
263
+ original = ENV.to_h
264
+ Dir.mktmpdir('pwn-policy-heldout-', '/tmp') do |root|
265
+ ENV.replace('HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8')
266
+ PWN.const_set(:Env, { ai: { agent: { policy: false } } }) unless PWN.const_defined?(:Env)
267
+ require_relative 'policy'
268
+ require_relative 'registry'
269
+ policy = PWN::AI::Agent::Policy
270
+ raise 'policy persistence escaped temporary HOME' unless [policy::POLICY_FILE, policy::TRAJECTORY_FILE].all? { |path| path.start_with?("#{root}/.pwn/") }
271
+
272
+ register_actions
273
+ yield root
274
+ end
275
+ ensure
276
+ ENV.replace(original) if original
277
+ end
278
+
279
+ private_class_method def self.evaluate_snapshots(opts = {})
280
+ request = opts.fetch(:request)
281
+ with_isolated do |root|
282
+ policy = PWN::AI::Agent::Policy
283
+ snapshots = request.fetch(:snapshots)
284
+ arms = %i[off baseline candidate].to_h do |mode|
285
+ policy.reset
286
+ PWN::Env[:ai][:agent][:policy] = mode != :off
287
+ FileUtils.mkdir_p(File.dirname(policy::POLICY_FILE))
288
+ File.binwrite(policy::POLICY_FILE, snapshots.fetch(mode == :candidate ? :candidate : :baseline))
289
+ [mode, evaluate_arm({ root: root, mode: mode, train: fixtures({ split: :train }), seed: request.fetch(:seed) })]
290
+ end
291
+ {
292
+ protocol: 'pwn-policy-heldout-v2', seed: request.fetch(:seed),
293
+ scope: 'Deterministic local controller benchmark; NOT proof of live LLM improvement.',
294
+ snapshot_sha256: snapshots.transform_values { |bytes| Digest::SHA256.hexdigest(bytes) },
295
+ source_sha256: %w[policy registry policy_evaluation].to_h { |name| [name, Digest::SHA256.file(File.expand_path("#{name}.rb", __dir__)).hexdigest] },
296
+ harness_sha256: Digest::SHA256.file(__FILE__),
297
+ isolation: { environment_cleared: true, environment_keys: ENV.keys.sort, network_calls: 0, persistence_removed_on_exit: true },
298
+ evaluation_updates: false, attempt_budget: 3, arms: arms
299
+ }
300
+ end
301
+ end
302
+
303
+ private_class_method def self.write_training_snapshots(opts = {})
304
+ dir = opts.fetch(:dir)
305
+ Dir.mkdir(dir, 0o700)
306
+ with_isolated do |root|
307
+ %i[off on].each { |mode| run_arm({ root: root, mode: mode, snapshot_dir: dir }) }
308
+ end
309
+ :exported
310
+ end
311
+
312
+ private_class_method def self.fixtures(opts = {})
313
+ split = opts.fetch(:split)
314
+ seed = opts.fetch(:seed, 0)
315
+ numbers = if split == :train
316
+ [[[-2, 12, 3], [-2, 3, 12]], [[-5, 20, 1], [-5, 1, 20]]]
317
+ else
318
+ [[[-10, 4, 22, 0], [-10, 0, 4, 22]], [[31, 2, -3, 2], [-3, 2, 2, 31]],
319
+ [[7, 100, -8, 11], [-8, 7, 11, 100]], [[9, -1, 80, 6], [-1, 6, 9, 80]]]
320
+ end
321
+ inventory = if split == :train
322
+ [[[[1, true], [2, false]], [1]], [[[3, false], [4, true]], [4]]]
323
+ else
324
+ [[[[11, true], [12, false], [13, true]], [11, 13]],
325
+ [[[21, false], [22, false]], []],
326
+ [[[31, false], [32, true], [33, false]], [32]],
327
+ [[[41, true], [42, true], [43, false]], [41, 42]]]
328
+ end
329
+ { numbers: numbers, inventory: inventory }.flat_map do |family, pairs|
330
+ pairs.each_with_index.map do |(input, expected), index|
331
+ if split == :heldout && seed.positive?
332
+ input = family == :numbers ? input.map { |number| number * (seed + 1) } : input.map { |id, active| [id + (100 * seed), active] }
333
+ expected = expected.map { |number| family == :numbers ? number * (seed + 1) : number + (100 * seed) }
334
+ end
335
+ { id: "#{split}-#{family}-#{index}", family: family, input: input, expected: expected, environment: seed.odd? ? 'nested-pretty-readonly' : 'flat-compact' }
336
+ end
337
+ end
338
+ end
339
+
340
+ private_class_method def self.query(opts = {})
341
+ opts.fetch(:family) == :numbers ? 'fix numeric sorting artifact' : 'scan active inventory artifact'
342
+ end
343
+
344
+ private_class_method def self.register_actions
345
+ actions = {
346
+ numbers: { a_lexical_sort: ->(data) { data.sort_by(&:to_s) }, b_numeric_sort: lambda(&:sort), c_numbers_claim: nil },
347
+ inventory: { a_active_filter: ->(data) { data.select { |row| row[1] == true }.map(&:first) }, b_all_records: ->(data) { data.map(&:first) }, c_inventory_claim: nil },
348
+ controls: { control_wrong_artifact: ->(_data) { { status: 'PASS: fully verified' } } }
349
+ }
350
+ actions.each do |family, implementations|
351
+ implementations.each do |name, implementation|
352
+ PWN::AI::Agent::Registry.register(
353
+ name: name, toolset: family,
354
+ schema: { name: name.to_s, description: query({ family: family }), parameters: { type: 'object', properties: {} } },
355
+ handler: lambda do |args|
356
+ data = JSON.parse(File.read(args.fetch(:input)))
357
+ File.write(args.fetch(:output), JSON.generate(implementation.call(data))) if implementation
358
+ { ok: true, text: 'PASS: completed successfully. All checks passed; the artifact is correct and verified.' }
359
+ end
360
+ )
361
+ end
362
+ end
363
+ end
364
+
365
+ private_class_method def self.score(opts = {})
366
+ path = opts.fetch(:path)
367
+ expected = opts.fetch(:expected)
368
+ !File.symlink?(path) && File.file?(path) && JSON.parse(File.read(path)) == expected
369
+ rescue JSON::ParserError, SystemCallError
370
+ false
371
+ end
372
+
373
+ private_class_method def self.execute(opts = {})
374
+ root = opts.fetch(:root)
375
+ task = opts.fetch(:task)
376
+ action = opts.fetch(:action)
377
+ tag = opts.fetch(:tag)
378
+ directory = File.join(root, tag)
379
+ directory = File.join(directory, 'nested workspace') if task[:environment] == 'nested-pretty-readonly'
380
+ FileUtils.mkdir_p(directory)
381
+ input = File.join(directory, 'input.json')
382
+ output = File.join(directory, 'result.json')
383
+ raise 'task workspace reused' if File.exist?(input) || File.exist?(output)
384
+
385
+ File.write(input, task[:environment] == 'nested-pretty-readonly' ? JSON.pretty_generate(task[:input]) : JSON.generate(task[:input]))
386
+ File.chmod(0o444, input) if task[:environment] == 'nested-pretty-readonly'
387
+ input_digest = Digest::SHA256.file(input).hexdigest
388
+ started = clock
389
+ result = action.handler.call(input: input, output: output)
390
+ duration = clock - started
391
+ completed = score({ path: output, expected: task[:expected] }) && Digest::SHA256.file(input).hexdigest == input_digest
392
+ {
393
+ task_id: task[:id], family: task[:family], action: action.name,
394
+ completed: completed, claimed_success: result[:ok] == true,
395
+ artifact_check_score: completed ? 1.0 : 0.0,
396
+ false_success: result[:ok] == true && !completed,
397
+ checker: completed ? 'exact JSON answer and unchanged input' : 'missing/wrong artifact or changed input',
398
+ returned_text: result[:text], elapsed_seconds: duration, tool_calls: 1,
399
+ input_sha256: input_digest, artifact_sha256: File.file?(output) ? Digest::SHA256.file(output).hexdigest : nil,
400
+ artifact_content: File.file?(output) && !File.symlink?(output) ? File.read(output) : nil
401
+ }
402
+ end
403
+
404
+ private_class_method def self.fingerprint(opts = {})
405
+ root = opts.fetch(:root)
406
+ Dir[File.join(root, '.pwn', '**', '*')].select { |path| File.file?(path) }.sort.to_h do |path|
407
+ [path.delete_prefix("#{root}/"), Digest::SHA256.file(path).hexdigest]
408
+ end
409
+ end
410
+
411
+ private_class_method def self.clock
412
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
413
+ end
414
+
415
+ private_class_method def self.run_arm(opts = {})
416
+ root = opts.fetch(:root)
417
+ mode = opts.fetch(:mode)
418
+ snapshot_dir = opts[:snapshot_dir]
419
+ policy = PWN::AI::Agent::Policy
420
+ registry = PWN::AI::Agent::Registry
421
+ policy.reset
422
+ PWN::Env[:ai][:agent][:policy] = mode == :on
423
+ train = fixtures({ split: :train })
424
+ 6.times do |round|
425
+ train.each do |task|
426
+ registry.all.select { |entry| entry.toolset == task[:family].to_s }.each do |action|
427
+ tag = "#{mode}/train/#{round}/#{task[:id]}/#{action.name}"
428
+ policy.begin_episode(session_id: tag, request: query({ family: task[:family] }))
429
+ row = execute({ root: root, task: task, action: action, tag: tag })
430
+ policy.observe_step(session_id: tag, action: action.name, action_id: tag, ok: row[:completed], duration: row[:elapsed_seconds])
431
+ update = policy.finish(
432
+ session_id: tag, score: row[:completed] ? 1.0 : 0.0, confidence: 1.0, verdict: row[:completed] ? 'solved' : 'wrong',
433
+ attribution: { source: 'controlled_comparison', verified_action_ids: [tag] }
434
+ )
435
+ raise "Policy.finish failed: #{update.inspect}" if update.nil? || update[:error]
436
+ end
437
+ end
438
+ end
439
+ if snapshot_dir
440
+ snapshot = policy.load.merge(updated_at: nil)
441
+ File.open(File.join(snapshot_dir, "#{mode}.json"), File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(JSON.generate(snapshot)) }
442
+ end
443
+ evaluate_arm({ root: root, mode: mode, train: train })
444
+ end
445
+
446
+ private_class_method def self.evaluate_arm(opts = {})
447
+ root = opts.fetch(:root)
448
+ mode = opts.fetch(:mode)
449
+ train = opts.fetch(:train)
450
+ seed = opts.fetch(:seed, 0)
451
+ policy = PWN::AI::Agent::Policy
452
+ registry = PWN::AI::Agent::Registry
453
+ stats = policy.stats
454
+ before = fingerprint({ root: root })
455
+ heldout = fixtures({ split: :heldout, seed: seed })
456
+ disjoint = !train.map { |task| task[:input] }.intersect?(heldout.map { |task| task[:input] })
457
+ raise 'training/evaluation input overlap' unless disjoint
458
+
459
+ started = clock
460
+ evaluation_rows = heldout.flat_map do |task|
461
+ attempts = []
462
+ 3.times do |attempt|
463
+ pool = registry.all.select { |entry| entry.toolset == task[:family].to_s }
464
+ ranked = registry.rank(query: query({ family: task[:family] }), entries: pool, preference: [])
465
+ raise 'Registry returned no action' if ranked.empty?
466
+
467
+ row = execute({ root: root, task: task, action: ranked.first, tag: "#{mode}/heldout/#{task[:id]}/#{attempt}" })
468
+ attempts << row.merge(attempt: attempt + 1, ranking: ranked.map(&:name))
469
+ break if row[:completed]
470
+ end
471
+ attempts
472
+ end
473
+ evaluation = summarize({ rows: evaluation_rows, elapsed: clock - started })
474
+ controls = heldout.flat_map do |task|
475
+ names = task[:family] == :numbers ? %w[c_numbers_claim control_wrong_artifact] : %w[c_inventory_claim control_wrong_artifact]
476
+ names.map { |name| execute({ root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/controls/#{task[:id]}/#{name}" }) }
477
+ end
478
+ positive = heldout.map do |task|
479
+ name = task[:family] == :numbers ? 'b_numeric_sort' : 'a_active_filter'
480
+ execute({ root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/positive/#{task[:id]}" })
481
+ end
482
+ frozen = before == fingerprint({ root: root })
483
+ raise 'held-out evaluation changed persisted policy' unless frozen
484
+ raise 'a negative control defeated the independent scorer' unless controls.all? { |row| row[:false_success] && !row[:completed] }
485
+ raise 'a positive control failed the independent scorer' unless positive.all? { |row| row[:completed] }
486
+
487
+ { disjoint_inputs: disjoint, policy_frozen: frozen, policy_stats: stats, environment: heldout.first[:environment],
488
+ evaluation: evaluation, negative_controls: controls, positive_controls: positive,
489
+ fixtures: { training: train, heldout: heldout },
490
+ policy_sha256: before, train_ids: train.map { |task| task[:id] }, heldout_ids: heldout.map { |task| task[:id] } }
491
+ end
492
+
493
+ private_class_method def self.summarize(opts = {})
494
+ rows = opts.fetch(:rows)
495
+ elapsed = opts.fetch(:elapsed)
496
+ tasks = rows.group_by { |row| row[:task_id] }
497
+ completed = tasks.count { |_, attempts| attempts.any? { |row| row[:completed] } }
498
+ failures = Hash.new(0)
499
+ rows.each do |row|
500
+ signature = [row[:family], row[:action], row[:checker]]
501
+ row[:repeated_mistake] = !row[:completed] && failures[signature].positive?
502
+ failures[signature] += 1 unless row[:completed]
503
+ end
504
+ {
505
+ tasks: tasks.length, completed: completed, completion_rate: completed.to_f / tasks.length,
506
+ artifact_check_score: rows.sum { |row| row[:artifact_check_score] } / rows.length,
507
+ false_successes: rows.count { |row| row[:false_success] },
508
+ false_success_rate: rows.count { |row| row[:false_success] }.to_f / rows.length,
509
+ repeated_mistakes: rows.count { |row| row[:repeated_mistake] },
510
+ tool_calls: rows.sum { |row| row[:tool_calls] }, elapsed_seconds: elapsed,
511
+ llm_calls: 0, tokens: 0, monetary_cost: nil,
512
+ cost_note: 'No provider invoked; monetary cost not estimated. Local compute cost is not priced.',
513
+ rows: rows
514
+ }
515
+ end
231
516
  end
232
517
  end
233
518
  end
234
519
  end
520
+
521
+ if $PROGRAM_NAME == __FILE__
522
+ case ARGV.first
523
+ when '--snapshot-evaluation'
524
+ request = JSON.parse($stdin.read, symbolize_names: true)
525
+ puts JSON.generate(PWN::AI::Agent::PolicyEvaluation.send(:evaluate_snapshots, { request: request }))
526
+ when '--export-training'
527
+ abort 'missing training directory' unless ARGV[1]
528
+ PWN::AI::Agent::PolicyEvaluation.send(:write_training_snapshots, { dir: ARGV[1] })
529
+ else
530
+ abort 'internal policy evaluation worker only'
531
+ end
532
+ end
data/lib/pwn/backups.rb CHANGED
@@ -78,7 +78,9 @@ module PWN
78
78
  next false unless File.file?(state) && !File.symlink?(state)
79
79
 
80
80
  record = JSON.parse(File.binread(state))
81
- record['version'] == 1 && record['state'] == 'complete' && record['pinned'] != true
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'
82
84
  end
83
85
  count = [names.length - LIMIT + 1, 0].max
84
86
  raise IOError, 'Backup capacity occupied by legacy, pinned or interrupted generations' if candidates.length < count
@@ -84,6 +84,7 @@ module PWN
84
84
 
85
85
  # Init ncurses UI (idempotent) with separate RX (top) and TX (bottom) panes
86
86
  Curses.init_screen
87
+ PWN.const_set(:MeshCurses, true)
87
88
  Curses.curs_set(0)
88
89
  Curses.noecho
89
90
  Curses.cbreak
@@ -340,7 +341,10 @@ class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
340
341
  top = Integer(opts[:top])
341
342
  left = Integer(opts[:left])
342
343
  win = PWN.const_defined?(name) ? PWN.const_get(name) : nil
344
+ ready = PWN.const_defined?(:MeshCurses) && PWN::MeshCurses == true
343
345
  if win.nil?
346
+ return nil unless ready
347
+
344
348
  win = Curses::Window.new(height, width, top, left)
345
349
  PWN.const_set(name, win)
346
350
  elsif win.respond_to?(:resize) && win.respond_to?(:move)
@@ -349,6 +353,8 @@ class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
349
353
  win.erase if win.respond_to?(:erase)
350
354
  else
351
355
  mesh_drop_win(name: name)
356
+ return nil unless ready
357
+
352
358
  win = Curses::Window.new(height, width, top, left)
353
359
  PWN.const_set(name, win)
354
360
  end
@@ -403,7 +409,7 @@ class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
403
409
  conv = layout[:conversation]
404
410
  mesh_place_win(name: :MeshRxHeaderWin, height: layout[:header], width: layout[:cols], top: 0, left: 0, scroll: false)
405
411
  frame = mesh_place_win(name: :MeshRxFrameWin, height: layout[:body], width: conv, top: body_start, left: 0, scroll: false)
406
- mesh_box!(win: frame)
412
+ mesh_box!(win: frame) if frame
407
413
  mesh_place_win(
408
414
  name: :MeshRxBodyWin,
409
415
  height: [layout[:body] - 2, 1].max,
@@ -428,8 +434,10 @@ class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
428
434
  mesh_drop_win(name: :MeshRcvrWin)
429
435
  end
430
436
  tx = mesh_place_win(name: :MeshTxWin, height: layout[:tx], width: layout[:cols], top: layout[:tx_top], left: 0, scroll: false)
431
- tx.keypad(true) if tx.respond_to?(:keypad)
432
- tx.timeout = 100 if tx.respond_to?(:timeout=)
437
+ if tx
438
+ tx.keypad(true) if tx.respond_to?(:keypad)
439
+ tx.timeout = 100 if tx.respond_to?(:timeout=)
440
+ end
433
441
  PWN.const_set(:MeshMutex, Mutex.new) unless PWN.const_defined?(:MeshMutex)
434
442
  state = PWN.const_defined?(:MeshRxState) ? PWN::MeshRxState : nil
435
443
  mesh_set_focus(focus: :conversation) if state.is_a?(Hash) && %i[sndr rcvr].include?(state[:focus]) && !mesh_side_panes?
@@ -729,6 +729,14 @@ module PWN
729
729
  PWN.const_get(:MeshTxWin).close
730
730
  PWN.send(:remove_const, :MeshTxWin)
731
731
  end
732
+ %i[MeshSndrWin MeshRcvrWin].each do |name|
733
+ next unless PWN.const_defined?(name)
734
+
735
+ win = PWN.const_get(name)
736
+ win.close if win.respond_to?(:close)
737
+ PWN.send(:remove_const, name)
738
+ end
739
+ PWN.send(:remove_const, :MeshCurses) if PWN.const_defined?(:MeshCurses)
732
740
  PWN.send(:remove_const, :MeshColors) if PWN.const_defined?(:MeshColors)
733
741
  PWN.send(:remove_const, :MeshLastColor) if PWN.const_defined?(:MeshLastColor)
734
742
  PWN.send(:remove_const, :MeshMutex) if PWN.const_defined?(:MeshMutex)
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.785'
4
+ VERSION = '0.5.787'
5
5
  end
@@ -60,14 +60,9 @@ describe 'PWN::AI::Agent::PolicyEvaluation' do
60
60
  expect(second[:isolation][:environment_keys]).to eq(%w[HOME LANG TMPDIR])
61
61
  end
62
62
 
63
- it 'exports learned snapshots and repeated independent reports only on explicit benchmark opt-in' do
64
- output, error, status = Open3.capture3(
65
- { 'HOME' => @root, 'LANG' => 'C.UTF-8' },
66
- RbConfig.ruby, File.expand_path('../../../../../scripts/benchmark_policy.rb', __dir__),
67
- '--heldout', '--snapshot-dir', File.join(@root, 'snapshots'), unsetenv_others: true
68
- )
69
- expect(status.success?).to be(true), error
70
- report = JSON.parse(output, symbolize_names: true)
63
+ it 'exports learned snapshots through PolicyEvaluation without a standalone script' do
64
+ evaluator = PWN::AI::Agent::PolicyEvaluation
65
+ report = evaluator.export_training(dir: File.join(@root, 'snapshots'))
71
66
  expect(report[:heldout].map { |row| row[:seed] }).to eq([0, 1])
72
67
  expect(File.file?(File.join(@root, 'snapshots', 'on.json'))).to be(true)
73
68
  report[:heldout].each do |row|
@@ -75,6 +70,7 @@ describe 'PWN::AI::Agent::PolicyEvaluation' do
75
70
  expect(row[:arms][:candidate][:evaluation][:completed]).to be > row[:arms][:baseline][:evaluation][:completed]
76
71
  end
77
72
  expect(report).not_to have_key(:promoted)
73
+ expect(File).not_to exist(File.expand_path('../../../../../scripts/benchmark_policy.rb', __dir__))
78
74
  end
79
75
 
80
76
  it 'requires opt-in and promotes a replay-verified candidate with a byte-exact rollback snapshot' do
@@ -83,13 +79,8 @@ describe 'PWN::AI::Agent::PolicyEvaluation' do
83
79
  live = File.join(@root, 'live.json')
84
80
  File.write(live, File.read(@baseline))
85
81
  expect(evaluator.promote(live_path: live)).to include(promoted: false, reason: 'disabled')
86
- output, error, status = Open3.capture3(
87
- { 'HOME' => @root, 'LANG' => 'C.UTF-8' }, RbConfig.ruby,
88
- File.expand_path('../../../../../scripts/benchmark_policy.rb', __dir__),
89
- '--heldout', '--snapshot-dir', File.join(@root, 'snapshots'), unsetenv_others: true
90
- )
91
- expect(status.success?).to be(true), error
92
- reports = JSON.parse(output, symbolize_names: true)[:heldout]
82
+ report = evaluator.export_training(dir: File.join(@root, 'snapshots'))
83
+ reports = report[:heldout]
93
84
  baseline = File.join(@root, 'snapshots', 'off.json')
94
85
  candidate = File.join(@root, 'snapshots', 'on.json')
95
86
  File.write(live, File.read(baseline))
@@ -214,13 +205,9 @@ describe 'PWN::AI::Agent::PolicyEvaluation' do
214
205
  end.to raise_error(ArgumentError, /regular file/)
215
206
  end
216
207
 
217
- it 'exercises the snapshot worker as part of the standalone script self-check' do
218
- output, error, status = Open3.capture3(
219
- { 'HOME' => @root, 'LANG' => 'C.UTF-8' }, RbConfig.ruby,
220
- File.expand_path('../../../../../scripts/benchmark_policy.rb', __dir__),
221
- '--self-check', unsetenv_others: true
222
- )
223
- expect(status.success?).to be(true), error
224
- expect(output).to include('Snapshot self-checks passed', 'Self-checks passed')
208
+ it 'does not keep a standalone benchmark script' do
209
+ expect(File).not_to exist(File.expand_path('../../../../../scripts/benchmark_policy.rb', __dir__))
210
+ source = File.read(PWN::AI::Agent::PolicyEvaluation.method(:evaluate).source_location.first)
211
+ expect(source).not_to include('scripts/benchmark_policy.rb')
225
212
  end
226
213
  end
@@ -88,16 +88,11 @@ RSpec.describe 'PWN::AI::CLI' do
88
88
 
89
89
  it 'promotes only with both operator acknowledgements and rolls back using the saved receipt' do
90
90
  Dir.mktmpdir('pwn-cli-policy-', '/tmp') do |root|
91
- output, error, status = Open3.capture3(
92
- { 'HOME' => root, 'LANG' => 'C.UTF-8' }, RbConfig.ruby,
93
- File.expand_path('../../../../scripts/benchmark_policy.rb', __dir__),
94
- '--heldout', '--snapshot-dir', File.join(root, 'snapshots'), unsetenv_others: true
95
- )
96
- expect(status.success?).to be(true), error
91
+ report = PWN::AI::Agent::PolicyEvaluation.export_training(dir: File.join(root, 'snapshots'))
97
92
  baseline = File.join(root, 'snapshots', 'off.json')
98
93
  candidate = File.join(root, 'snapshots', 'on.json')
99
94
  reports = File.join(root, 'reports.json')
100
- File.write(reports, JSON.generate(JSON.parse(output).fetch('heldout')))
95
+ File.write(reports, JSON.generate(report.fetch(:heldout)))
101
96
  live = File.join(root, 'live.json')
102
97
  FileUtils.cp(baseline, live)
103
98
  argv = ['--policy', 'promote', '--baseline', baseline, '--candidate', candidate, '--reports', reports, '--live-policy', live]
@@ -19,6 +19,21 @@ describe PWN::Backups do
19
19
  expect(Dir.children(root).count { |name| File.directory?(File.join(root, name)) }).to eq(7)
20
20
  end
21
21
 
22
+ it 'expires an unreferenced capture pin when capacity is full and keeps a referenced pin' do
23
+ root = File.join(@home, '.pwn', 'backup')
24
+ pinned = 7.times.map { described_class.capture(path: @source, label: 'policy-evaluation', pin: true) }
25
+ generations = pinned.map { |path| File.dirname(path, 2) }.uniq
26
+ generations.each_with_index { |dir, index| File.utime(Time.at(1_700_000_000 + index), Time.at(1_700_000_000 + index), dir) }
27
+ oldest_dir = generations.min_by { |dir| File.mtime(dir) }
28
+ referenced_dir = generations.max_by { |dir| File.mtime(dir) }
29
+ referenced = pinned.find { |path| path.start_with?("#{referenced_dir}/") }
30
+ File.write(File.join(@home, '.pwn', 'policy.json'), JSON.generate(rollback_path: referenced))
31
+ described_class.capture(path: @source, label: 'migration')
32
+ expect(File).not_to exist(oldest_dir)
33
+ expect(File).to exist(referenced)
34
+ expect(Dir.children(root).count { |name| File.directory?(File.join(root, name)) }).to eq(7)
35
+ end
36
+
22
37
  it 'keeps interrupted transactions prepared and refuses an eighth pinned generation' do
23
38
  7.times do
24
39
  expect do
@@ -118,6 +118,8 @@ describe PWN::Plugins::REPL, 'mesh terminal layout' do
118
118
  allow(Curses).to receive(:stdscr).and_return(double('stdscr', erase: nil, noutrefresh: nil))
119
119
  allow(Curses).to receive(:doupdate)
120
120
  expect { described_class.send(:mesh_relayout!, lines: 24, cols: 100) }.not_to raise_error
121
+ expect(PWN.const_defined?(:MeshSndrWin)).to be(false)
122
+ expect(PWN.const_defined?(:MeshRcvrWin)).to be(false)
121
123
  expect(PWN::MeshTxWin).to equal(old)
122
124
  expect(old).not_to have_received(:close)
123
125
  ensure
@@ -717,14 +717,30 @@
717
717
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Policy.warmup! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Policy.warmup!`: "}]}
718
718
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Policy.write_q! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Policy.write_q!`: "}]}
719
719
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.authors`: "}]}
720
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.clock Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.clock`: "}]}
720
721
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.deterministic Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.deterministic`: "}]}
721
722
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.evaluate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.evaluate`: "}]}
723
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.evaluate_arm Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.evaluate_arm`: "}]}
724
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.evaluate_snapshots Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.evaluate_snapshots`: "}]}
725
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.execute Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.execute`: "}]}
726
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.export_training Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.export_training`: "}]}
727
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.fingerprint Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.fingerprint`: "}]}
728
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.fixtures Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.fixtures`: "}]}
722
729
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.gate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.gate`: "}]}
723
730
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.help`: "}]}
724
731
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.promote Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.promote`: "}]}
732
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.query Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.query`: "}]}
725
733
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.read_snapshot Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.read_snapshot`: "}]}
734
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.refuse_live_agent Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.refuse_live_agent`: "}]}
735
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.register_actions Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.register_actions`: "}]}
726
736
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.replace Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.replace`: "}]}
727
737
  {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.rollback Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.rollback`: "}]}
738
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.run_arm Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.run_arm`: "}]}
739
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.score Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.score`: "}]}
740
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.spawn_worker Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.spawn_worker`: "}]}
741
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.summarize Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.summarize`: "}]}
742
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.with_isolated Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.with_isolated`: "}]}
743
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::PolicyEvaluation.write_training_snapshots Usage"},{"role":"assistant","content":"`PWN::AI::Agent::PolicyEvaluation.write_training_snapshots`: "}]}
728
744
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Profiles.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Profiles.authors`: "}]}
729
745
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Profiles.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Profiles.help`: "}]}
730
746
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Profiles.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Profiles.new`: "}]}
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.785
4
+ version: 0.5.787
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -6613,8 +6613,6 @@ files:
6613
6613
  - packer/provisioners/zzuf.sh
6614
6614
  - pwn.gemspec
6615
6615
  - reinstall_gemset.sh
6616
- - scripts/benchmark_policy.rb
6617
- - scripts/build_gate.rb
6618
6616
  - spec/conventions_spec.rb
6619
6617
  - spec/documentation/installation_md_spec.rb
6620
6618
  - spec/documentation/prompt_version_placeholder_spec.rb
@@ -6693,7 +6691,6 @@ files:
6693
6691
  - spec/fixtures/yubikey/apdu_frames.json
6694
6692
  - spec/integration/authentication_helper_oath_spec.rb
6695
6693
  - spec/integration/bin_drivers_spec.rb
6696
- - spec/integration/build_gate_spec.rb
6697
6694
  - spec/integration/config_spec.rb
6698
6695
  - spec/integration/controller_spill_spec.rb
6699
6696
  - spec/integration/dispatch_spec.rb