pwn 0.5.635 → 0.5.636

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: 347b9c864507e706e465e27933acee3d03b8fca35529e3c63bfb374a50763061
4
- data.tar.gz: 98764623bb85939b3dc681c1703ad4f5e50bc9450caef5773c39855b3b4b469e
3
+ metadata.gz: 41a8a5c5d750508d4f3904d7a05151719e2c86fa4cf20001a3aab0985910963d
4
+ data.tar.gz: 7af0757b53bdfa6c97d86a9415a2cbf41ca9447b3f9e438b95b2424f5d835cbf
5
5
  SHA512:
6
- metadata.gz: 3fe90d0d5abb8876bcb55d3be503725f7819426b2b42ea1e10c756c3c7c3f4d4060bace652e8617ba5d74af1078dab2d6250c5c5c57e0a3ca52eaf3a9410492b
7
- data.tar.gz: d778fc2e42b1276b2731546fc84f893bf7174f903d8c3880a75c1930509d3a4e55e387e0c38ed61cfb43ad19e9e9e824c0f336b6e559d409d8fd6604da00ddfe
6
+ metadata.gz: 8053f31ef3138368e50c1b6920fa7804945f508daabc31191ba2a9578f3c31f7e6629e1ca901a409ec82e4cefdf3a824fa48bcfe4fdcd8ab4a1a9da0f168e1da
7
+ data.tar.gz: 592bfb24c2c2a610813fcd5b8431478d4774ef312405f446393db9cbb305c2c223b129a0714b9b464adc9976e682b1754a4e5d760d0ca28b00ba99778df21d7a
data/Rakefile CHANGED
@@ -16,7 +16,13 @@ if defined?(RDoc::Task)
16
16
  RDoc::Task.new do |rdoc|
17
17
  rdoc.rdoc_files.include('lib/**/*.rb')
18
18
  rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.options << '--quiet'
19
20
  end
21
+ # RDoc::Task's default :clobber_rdoc uses Rake's verbose FileUtils, which
22
+ # echoes "rm -r rdoc" to STDOUT on every `rake` (via :rerdoc). Replace it
23
+ # with a silent rm_rf so the default task emits only spec/rubocop output.
24
+ Rake::Task[:clobber_rdoc].clear_actions
25
+ task(:clobber_rdoc) { FileUtils.rm_rf('rdoc') }
20
26
  end
21
27
 
22
28
  default_tasks = %i[spec rubocop]
data/bin/pwn_setup CHANGED
@@ -29,6 +29,16 @@ pwn_driver = PWN::Driver::Parser.new do |options|
29
29
  opts[:list] = o
30
30
  end
31
31
 
32
+ options.on('-m', '--migrate',
33
+ '<Optional - Verify ~/.pwn state files are compatible with this pwn version; apply schema migrations>') do |o|
34
+ opts[:migrate] = o
35
+ end
36
+
37
+ options.on('-f', '--fix',
38
+ '<Optional - With --migrate: also autofix incompatible files (backup taken first)>') do |o|
39
+ opts[:fix] = o
40
+ end
41
+
32
42
  options.on('-y', '--yes',
33
43
  '<Optional - Assume yes; non-interactive (CI-friendly)>') do |o|
34
44
  opts[:yes] = o
@@ -45,6 +55,12 @@ pwn_driver.parse!
45
55
  begin
46
56
  if opts[:list]
47
57
  PWN::Setup.list_profiles
58
+ elsif opts[:migrate]
59
+ r = PWN::Setup.migrate(
60
+ fix: opts[:fix] || opts[:yes],
61
+ dry_run: opts[:dry_run]
62
+ )
63
+ exit 1 unless r && r[:after] && r[:after][:incompatible].empty?
48
64
  elsif opts[:deps] || opts[:profile]
49
65
  PWN::Setup.deps(
50
66
  profile: opts[:profile],
@@ -17,7 +17,7 @@
17
17
  | [What is PWN](What-is-PWN.md) | One-paragraph elevator pitch |
18
18
  | [Why PWN](Why-PWN.md) | Design philosophy - why another framework |
19
19
  | [How PWN Works](How-PWN-Works.md) | The five layers, with the architecture diagram |
20
- | [Installation](Installation.md) | `gem install pwn` → `pwn setup` doctor/provisioner · capability profiles |
20
+ | [Installation](Installation.md) | `gem install pwn` → `pwn setup` doctor/provisioner · capability profiles · `--migrate` state doctor |
21
21
  | [General Usage](General-PWN-Usage.md) | Day-one cheat sheet |
22
22
  | [Configuration](Configuration.md) | `~/.pwn/pwn.yaml` (encrypted) - engines, keys, agent options · `pwn-vault` |
23
23
  | **[All Data-Flow Diagrams](Diagrams.md)** | **27 SVGs** in one scrollable page |
@@ -36,13 +36,14 @@
36
36
  | | |
37
37
  |---|---|
38
38
  | [AI / LLM Integration](AI-Integration.md) | OpenAI · Anthropic · Grok (OAuth) · Gemini · Ollama |
39
- | [Agent Tool Registry](Agent-Tool-Registry.md) | 10 toolsets · 61 LLM-callable tools |
39
+ | [Agent Tool Registry](Agent-Tool-Registry.md) | 10 toolsets · **71** LLM-callable tools |
40
40
  | [Memory · Skills · Learning](Skills-Memory-Learning.md) | Introspection - the self-improvement loop |
41
41
  | [Mistakes](Mistakes.md) | **Negative feedback** - fingerprint failures · do-NOT-repeat · `[REPEATING]`/`[REGRESSED]` · inline self-correction |
42
- | [Extrospection](Extrospection.md) | World-awareness - snapshot · drift · intel · **watch** · **verify** · **rf_tune** · correlate · **rf** · **web** |
42
+ | [Reinforcement Learning](Reinforcement-Learning.md) | **`Reward` + `Curriculum`** - ORM/PRM judge · sentinel · HER · self-play · DPO export · **regression-gated LoRA** |
43
+ | [Extrospection](Extrospection.md) | World-awareness - snapshot · drift · intel · **watch** · **verify** · **rf_tune** · **osint** · serial · telecomm · packet · vision · voice · correlate |
43
44
  | [Swarm (Multi-Agent)](Swarm.md) | Personas · ask · debate · broadcast · shared bus |
44
45
  | [Sessions](Sessions.md) | Transcript persistence + reflection |
45
- | [Cron](Cron.md) | Scheduled autonomous jobs |
46
+ | [Cron](Cron.md) | Scheduled autonomous jobs (nightly self-play + weekly weight-loop seeded by default) |
46
47
 
47
48
  ## 🧩 Capability Namespaces (`lib/pwn/*`)
48
49
 
@@ -62,14 +63,14 @@
62
63
  | [Blockchain](Blockchain.md) | BTC · ETH helpers |
63
64
  | [Bounty](Bounty.md) | Lifecycle / auth-replay tooling |
64
65
  | [Reports](Reporting.md) | HTML/JSON output + DefectDojo/Jira |
65
- | [FFI](FFI.md) | Native DSP/RF backends (Volk · Liquid · FFTW · HackRF · RTL-SDR · SoapySDR) |
66
+ | [FFI](FFI.md) | Native DSP/RF backends (Volk · Liquid · FFTW · HackRF · RTL-SDR · **AdalmPluto** · SoapySDR · Stdio) |
66
67
  | [Banner](Banner.md) | 15 startup banners |
67
68
 
68
69
  ## 🛠️ Meta
69
70
 
70
71
  | | |
71
72
  |---|---|
72
- | [`~/.pwn/` Filesystem Map](Persistence.md) | Every file PWN writes and why |
73
+ | [`~/.pwn/` Filesystem Map](Persistence.md) | Every file PWN writes and why · `PWN::Migrate` state doctor |
73
74
  | [Troubleshooting](Troubleshooting.md) | Common errors and fixes |
74
75
  | [Contributing](Contributing.md) | Conventions, RuboCop, spec rules |
75
76
 
@@ -208,7 +208,8 @@ row as `MISSING` until a key is set.
208
208
  Everything above is a thin CLI over one autoloaded module:
209
209
 
210
210
  ```ruby
211
- PWN::Setup.check # → { ok:, native_gems_missing:, toolchain_missing:, pkg_manager:, os:, arch: }
211
+ PWN::Setup.check # → { ok:, native_gems_missing:, toolchain_missing:, state:, pkg_manager:, os:, arch: }
212
+ PWN::Setup.migrate(fix: true) # verify + autofix ~/.pwn state files (see PWN::Migrate)
212
213
  PWN::Setup.deps(profile: :web, yes: true) # install, then re-check
213
214
  PWN::Setup.list_profiles
214
215
  PWN::Setup.pkg_manager # → { key: :apt, install: 'sudo apt-get install -y', sudo: true }
@@ -237,3 +238,27 @@ pwn[CURRENT_VERSION]:004 >>> pwn-ai # launches agent TUI
237
238
  **Next:** [Configuration](Configuration.md) · [General Usage](General-PWN-Usage.md)
238
239
 
239
240
  [← Home](Home.md)
241
+
242
+ ---
243
+
244
+ ## Upgrading — `~/.pwn` state migration
245
+
246
+ After `gem update pwn`, run the state doctor to verify (and autofix)
247
+ every persisted file under `~/.pwn` against the new release:
248
+
249
+ ```bash
250
+ pwn setup --migrate # verify + apply schema migrations (backup taken)
251
+ pwn setup --migrate --fix # also autofix incompatible files
252
+ pwn setup --migrate --dry-run # report only, write nothing
253
+ ```
254
+
255
+ `PWN::Migrate` (autoloaded) stamps `~/.pwn/.schema` with the release
256
+ schema number, checks each state file (`memory.json`, `metrics.json`,
257
+ `mistakes.json`, `learning.jsonl`, `agents.yml`, `cron/jobs.yml`,
258
+ `skills/`, …) against its owning module's loader, and — with `--fix` —
259
+ deep-merges any keys the current `PWN::Config.env_template` added into
260
+ your encrypted `~/.pwn/pwn.yaml` **without overwriting your values**.
261
+ Every run takes a timestamped backup under `~/.pwn/backup/<ts>/` first.
262
+
263
+ The plain `pwn` launcher will also print a one-line drift warning on
264
+ startup whenever `~/.pwn/.schema` predates the running gem.
data/lib/pwn/config.rb CHANGED
@@ -7,26 +7,16 @@ module PWN
7
7
  # Used to manage PWN configuration settings within PWN drivers.
8
8
  module Config
9
9
  # Supported Method Parameters::
10
- # env = PWN::Config.default_env(
11
- # pwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml'
12
- # )
13
- public_class_method def self.default_env(opts = {})
14
- pwn_env_path = opts[:pwn_env_path]
15
- pwn_dec_path = "#{pwn_env_path}.decryptor"
16
-
17
- puts "
18
- [*] NOTICE:
19
- 1. Writing minimal PWN::Env to:
20
- #{pwn_env_path}
21
- 2. Your decryptor file will be written to:
22
- #{pwn_dec_path}
23
- 3. Use the pwn-vault command in the pwn prototyping driver to update:
24
- #{pwn_env_path}
25
- 4. For optimal security, it's recommended to move:
26
- #{pwn_dec_path}
27
- to a secure location and use the --pwn-dec parameter for PWN drivers.
28
- "
29
- env = {
10
+ # tmpl = PWN::Config.env_template
11
+ #
12
+ # The canonical current-release ~/.pwn/pwn.yaml shape as a pure Hash
13
+ # (no I/O, no vault write, no puts). Single source of truth used by:
14
+ # * PWN::Config.default_env seed a fresh ~/.pwn/pwn.yaml
15
+ # * PWN::Migrate.vault_drift — diff a user vault against this release
16
+ # * PWN::Migrate.backfill_vault — deep-merge missing keys UNDER the
17
+ # user values on `pwn setup --migrate --fix`
18
+ public_class_method def self.env_template
19
+ {
30
20
  ai: {
31
21
  active: 'grok',
32
22
  module_reflection: false,
@@ -174,6 +164,31 @@ module PWN
174
164
  provider: 'yaml'
175
165
  }
176
166
  }
167
+ rescue StandardError => e
168
+ raise e
169
+ end
170
+
171
+ # Supported Method Parameters::
172
+ # env = PWN::Config.default_env(
173
+ # pwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml'
174
+ # )
175
+ public_class_method def self.default_env(opts = {})
176
+ pwn_env_path = opts[:pwn_env_path]
177
+ pwn_dec_path = "#{pwn_env_path}.decryptor"
178
+
179
+ puts "
180
+ [*] NOTICE:
181
+ 1. Writing minimal PWN::Env to:
182
+ #{pwn_env_path}
183
+ 2. Your decryptor file will be written to:
184
+ #{pwn_dec_path}
185
+ 3. Use the pwn-vault command in the pwn prototyping driver to update:
186
+ #{pwn_env_path}
187
+ 4. For optimal security, it's recommended to move:
188
+ #{pwn_dec_path}
189
+ to a secure location and use the --pwn-dec parameter for PWN drivers.
190
+ "
191
+ env = env_template
177
192
 
178
193
  # Remove beginning colon from key names
179
194
 
@@ -393,6 +408,13 @@ module PWN
393
408
  Pry.config.refresh_pwn_env = false if defined?(Pry)
394
409
 
395
410
  puts "[*] PWN::Env loaded via: #{pwn_env_path}\n"
411
+
412
+ # Upgrade drift — cheap schema-stamp check only (no per-file probes).
413
+ if defined?(PWN::Migrate) && PWN::Migrate.needed?
414
+ puts "[!] ~/.pwn state predates pwn #{PWN::VERSION} (schema " \
415
+ "#{PWN::Migrate.installed_schema} < #{PWN::Migrate::SCHEMA_VERSION}). " \
416
+ 'Run `pwn setup --migrate --fix` to autofix (backup taken first).'
417
+ end
396
418
  rescue StandardError => e
397
419
  raise e
398
420
  end
@@ -0,0 +1,716 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'fileutils'
6
+ require 'time'
7
+
8
+ module PWN
9
+ # PWN::Migrate — the ~/.pwn state **doctor** and **auto-migrator**.
10
+ #
11
+ # PWN persists a growing set of files under `~/.pwn` (encrypted config,
12
+ # memory, learning outcomes, metrics, mistakes, extrospection, cron
13
+ # jobs, agents, skills, sessions, swarm buses, …). Each file is owned
14
+ # by a different module and each release can add keys or change shape.
15
+ # A user upgrading `gem install pwn` between two versions could
16
+ # therefore hit `KeyError`, `NoMethodError for nil`, or a silent
17
+ # empty-fallback because their on-disk state predates the new loader.
18
+ #
19
+ # `PWN::Migrate` closes that gap. It is called from:
20
+ #
21
+ # * `pwn setup --migrate` — explicit doctor + autofix
22
+ # * `PWN::Setup.check` — read-only ~/.pwn state section
23
+ # * `PWN::Config.refresh_env` — one-line drift warning on every launch
24
+ #
25
+ # It works by:
26
+ #
27
+ # 1. Stamping `~/.pwn/.schema` with `{ schema:, pwn_version:, at: }`
28
+ # the first time it runs. Any release that changes the on-disk
29
+ # shape of a `~/.pwn` file bumps `SCHEMA_VERSION` here and appends
30
+ # an idempotent lambda to `MIGRATIONS`.
31
+ # 2. Declaratively verifying every state file against its OWNING
32
+ # module's own loader (`STATE_FILES`) — no re-implemented parsers.
33
+ # If the raw file has bytes but the owner returned its
34
+ # empty-fallback, the file is flagged incompatible.
35
+ # 3. Autofixing: timestamped backup → ordered schema migrations →
36
+ # per-file repair (quarantine corrupt / re-seed missing) →
37
+ # `pwn.yaml` deep-key backfill (missing keys from the current
38
+ # `PWN::Config.env_template` are merged in-place under the user's
39
+ # values, then the vault is re-encrypted with the SAME key/iv).
40
+ #
41
+ # Everything is idempotent, dry-run capable, and never overwrites a
42
+ # user-set value.
43
+ module Migrate
44
+ PWN_ROOT = File.join(Dir.home, '.pwn')
45
+ SCHEMA_FILE = File.join(PWN_ROOT, '.schema')
46
+ BACKUP_ROOT = File.join(PWN_ROOT, 'backup')
47
+ QUARANTINE_DIR = File.join(PWN_ROOT, 'quarantine')
48
+
49
+ # Bump this whenever the shape of any file under ~/.pwn changes in a
50
+ # way that requires a one-time transform. Add the transform as an
51
+ # entry in MIGRATIONS keyed by the NEW schema number.
52
+ SCHEMA_VERSION = 1
53
+
54
+ OK = "\e[32mok\e[0m"
55
+ BAD = "\e[31mFAIL\e[0m"
56
+ WARN = "\e[33mdrift\e[0m"
57
+
58
+ # ──────────────────────────────────────────────────────────────────
59
+ # Declarative registry of every persisted ~/.pwn artefact.
60
+ #
61
+ # Each entry knows how to (a) locate itself, (b) ask its owning
62
+ # module to load it, and (c) decide whether the loaded shape is what
63
+ # this pwn version expects. Verifiers are intentionally SHALLOW —
64
+ # they detect "loader gave up and returned the fallback" rather than
65
+ # re-validating every key, so ownership stays with the module.
66
+ #
67
+ # :fix values:
68
+ # :quarantine — move the file aside; owner re-seeds on next write
69
+ # :vault — deep-merge missing keys from PWN::Config.env_template
70
+ # :seed — call the owner's idempotent seeder (dirs / defaults)
71
+ # :jsonl — strip unparsable lines only (keeps good history)
72
+ # :skills — PWN::Config.migrate_legacy_skills
73
+ # ──────────────────────────────────────────────────────────────────
74
+ STATE_FILES = {
75
+ 'pwn.yaml' => {
76
+ owner: 'PWN::Config',
77
+ kind: :vault,
78
+ fix: :vault,
79
+ verify: lambda { |p, _raw|
80
+ return { ok: false, why: 'missing' } unless File.file?(p)
81
+
82
+ enc = PWN::Plugins::Vault.file_encrypted?(file: p)
83
+ # Shape (missing keys vs env_template) is checked separately in
84
+ # .vault_drift because it requires the decryptor.
85
+ { ok: enc, why: enc ? nil : 'not encrypted (run PWN::Plugins::Vault.create)' }
86
+ }
87
+ },
88
+ 'memory.json' => {
89
+ owner: 'PWN::Memory',
90
+ kind: :json,
91
+ fix: :quarantine,
92
+ verify: lambda { |_p, raw|
93
+ m = PWN::Memory.load
94
+ bad = m.reject { |_k, v| v.is_a?(Hash) && v.key?(:value) }
95
+ { ok: !(m.empty? && raw.to_s.length > 4) && bad.empty?,
96
+ why: bad.any? ? "#{bad.length} entries missing :value" : nil }
97
+ }
98
+ },
99
+ 'memory.idx' => {
100
+ owner: 'PWN::MemoryIndex',
101
+ kind: :blob,
102
+ fix: :quarantine,
103
+ verify: ->(_p, _raw) { { ok: true } }
104
+ },
105
+ 'learning.jsonl' => {
106
+ owner: 'PWN::AI::Agent::Learning',
107
+ kind: :jsonl,
108
+ fix: :jsonl,
109
+ verify: :jsonl
110
+ },
111
+ 'preferences.jsonl' => {
112
+ owner: 'PWN::AI::Agent::Reward',
113
+ kind: :jsonl,
114
+ fix: :jsonl,
115
+ verify: :jsonl
116
+ },
117
+ 'metrics.json' => {
118
+ owner: 'PWN::AI::Agent::Metrics',
119
+ kind: :json,
120
+ fix: :quarantine,
121
+ verify: lambda { |_p, raw|
122
+ m = PWN::AI::Agent::Metrics.load
123
+ fell_back = m == { tools: {}, updated_at: nil } && raw.to_s.length > 4
124
+ { ok: m.is_a?(Hash) && m.key?(:tools) && !fell_back,
125
+ why: fell_back ? 'loader fell back to empty (unparsable / wrong shape)' : nil }
126
+ }
127
+ },
128
+ 'mistakes.json' => {
129
+ owner: 'PWN::AI::Agent::Mistakes',
130
+ kind: :json,
131
+ fix: :quarantine,
132
+ verify: lambda { |_p, raw|
133
+ m = PWN::AI::Agent::Mistakes.load
134
+ fell_back = m.empty? && raw.to_s.length > 4
135
+ { ok: m.is_a?(Hash) && !fell_back, why: fell_back ? 'loader fell back to {}' : nil }
136
+ }
137
+ },
138
+ 'extrospection.json' => {
139
+ owner: 'PWN::AI::Agent::Extrospection',
140
+ kind: :json,
141
+ fix: :quarantine,
142
+ verify: lambda { |_p, raw|
143
+ m = PWN::AI::Agent::Extrospection.load
144
+ shape = m.is_a?(Hash) && m.key?(:observations) && m[:observations].is_a?(Array)
145
+ fell_back = shape && m[:snapshot].to_h.empty? && m[:observations].empty? && raw.to_s.length > 8
146
+ { ok: shape && !fell_back, why: shape ? nil : 'missing :observations array' }
147
+ }
148
+ },
149
+ 'reward_sentinel.json' => {
150
+ owner: 'PWN::AI::Agent::Reward',
151
+ kind: :json,
152
+ fix: :quarantine,
153
+ verify: ->(_p, _raw) { { ok: true } }
154
+ },
155
+ 'agents.yml' => {
156
+ owner: 'PWN::AI::Agent::Swarm',
157
+ kind: :yaml,
158
+ fix: :quarantine,
159
+ verify: lambda { |_p, raw|
160
+ m = PWN::AI::Agent::Swarm.personas
161
+ fell_back = m.empty? && raw.to_s.strip.length > 4
162
+ bad = m.reject { |_k, v| v.is_a?(Hash) && v[:role] }
163
+ { ok: !fell_back && bad.empty?,
164
+ why: (fell_back && 'loader fell back to {}') || (bad.any? && "#{bad.length} personas missing :role") || nil }
165
+ }
166
+ },
167
+ 'cron/jobs.yml' => {
168
+ owner: 'PWN::Cron',
169
+ kind: :yaml,
170
+ fix: :seed,
171
+ verify: lambda { |_p, raw|
172
+ j = PWN::Cron.list
173
+ fell_back = j.empty? && raw.to_s.strip.length > 8
174
+ bad = j.reject { |_id, v| v.is_a?(Hash) && v[:schedule] }
175
+ { ok: !fell_back && bad.empty?,
176
+ why: (fell_back && 'loader fell back to {}') || (bad.any? && "#{bad.length} jobs missing :schedule") || nil }
177
+ }
178
+ },
179
+ 'skills/' => {
180
+ owner: 'PWN::Config',
181
+ kind: :dir,
182
+ fix: :skills,
183
+ verify: lambda { |p, _raw|
184
+ return { ok: true } unless File.directory?(p)
185
+
186
+ legacy = Dir.glob(File.join(p, '*.{rb,md,txt,skill,yml,yaml}')).length
187
+ { ok: legacy.zero?, why: legacy.zero? ? nil : "#{legacy} legacy flat skill file(s)" }
188
+ }
189
+ },
190
+ 'sessions/' => { owner: 'PWN::Sessions', kind: :dir, fix: :seed, verify: ->(_p, _r) { { ok: true } } },
191
+ 'swarm/' => { owner: 'PWN::AI::Agent::Swarm', kind: :dir, fix: :seed, verify: ->(_p, _r) { { ok: true } } },
192
+ 'curriculum/' => { owner: 'PWN::AI::Agent::Curriculum', kind: :dir, fix: :seed, verify: ->(_p, _r) { { ok: true } } },
193
+ 'finetune/' => { owner: 'PWN::AI::Agent::Curriculum', kind: :dir, fix: :seed, verify: ->(_p, _r) { { ok: true } } },
194
+ 'extrospection/' => { owner: 'PWN::AI::Agent::Extrospection', kind: :dir, fix: :seed, verify: ->(_p, _r) { { ok: true } } }
195
+ }.freeze
196
+
197
+ # Ordered, idempotent one-shot transforms. Key == the schema number
198
+ # a ~/.pwn tree is AT after the lambda runs. Add a new entry every
199
+ # time SCHEMA_VERSION is bumped. Each lambda receives (root, io).
200
+ MIGRATIONS = {
201
+ 1 => lambda { |root, io|
202
+ # v0 → v1 : agentskills.io directory layout + seeded RL cron jobs
203
+ # + ensure all state subdirs exist.
204
+ %w[skills sessions swarm cron curriculum finetune extrospection].each do |d|
205
+ FileUtils.mkdir_p(File.join(root, d))
206
+ end
207
+ r = begin
208
+ PWN::Config.migrate_legacy_skills
209
+ rescue StandardError
210
+ { migrated: 0 }
211
+ end
212
+ io.puts " · migrate_legacy_skills → #{r[:migrated]} converted" if r[:migrated].to_i.positive?
213
+ PWN::Cron.install_defaults if defined?(PWN::Cron)
214
+ }
215
+ }.freeze
216
+
217
+ # ──────────────────────────────────────────────────────────────────
218
+ # public api
219
+ # ──────────────────────────────────────────────────────────────────
220
+
221
+ # Supported Method Parameters::
222
+ # schema = PWN::Migrate.installed_schema
223
+ #
224
+ # → Integer schema number stamped in ~/.pwn/.schema (0 when absent).
225
+
226
+ public_class_method def self.installed_schema
227
+ return 0 unless File.file?(SCHEMA_FILE)
228
+
229
+ JSON.parse(File.read(SCHEMA_FILE))['schema'].to_i
230
+ rescue StandardError
231
+ 0
232
+ end
233
+
234
+ # Supported Method Parameters::
235
+ # bool = PWN::Migrate.needed?
236
+ #
237
+ # Cheap predicate for PWN::Config.refresh_env — schema stamp only,
238
+ # no per-file probes, so it adds ~0 to launch time.
239
+
240
+ public_class_method def self.needed?
241
+ File.directory?(PWN_ROOT) && installed_schema < SCHEMA_VERSION
242
+ rescue StandardError
243
+ false
244
+ end
245
+
246
+ # Supported Method Parameters::
247
+ # rows = PWN::Migrate.status
248
+ #
249
+ # Per-file compatibility report — the machine-readable half of
250
+ # `.check`. Never writes. Never raises (each verifier is rescued).
251
+
252
+ public_class_method def self.status
253
+ files = STATE_FILES.map do |rel, meta|
254
+ path = File.join(PWN_ROOT, rel)
255
+ exists = meta[:kind] == :dir ? File.directory?(path) : File.file?(path)
256
+ raw = exists && meta[:kind] != :dir ? safe_read(path: path) : nil
257
+ v = verify_one(path: path, raw: raw, meta: meta)
258
+ {
259
+ rel: rel, path: path, owner: meta[:owner], kind: meta[:kind],
260
+ exists: exists, size: exists ? File.size?(path) : nil,
261
+ ok: v[:ok], why: v[:why], fix: meta[:fix]
262
+ }
263
+ end
264
+ {
265
+ root: PWN_ROOT,
266
+ schema_installed: installed_schema,
267
+ schema_current: SCHEMA_VERSION,
268
+ pwn_version: (defined?(PWN::VERSION) ? PWN::VERSION : nil),
269
+ needs_migration: needed?,
270
+ files: files,
271
+ incompatible: files.reject { |f| f[:ok] }
272
+ }
273
+ rescue StandardError => e
274
+ raise e
275
+ end
276
+
277
+ # Supported Method Parameters::
278
+ # PWN::Migrate.check(
279
+ # io: 'optional - IO to write the report to (default $stdout)'
280
+ # )
281
+ #
282
+ # Human-readable ~/.pwn state doctor. Read-only. Called from
283
+ # PWN::Setup.check so `pwn setup` shows this alongside the
284
+ # native-gem / toolchain report.
285
+
286
+ public_class_method def self.check(opts = {})
287
+ io = opts[:io] || $stdout
288
+ st = status
289
+
290
+ io.puts "~/.pwn state schema #{st[:schema_installed]} → #{st[:schema_current]} " \
291
+ "#{st[:needs_migration] ? "(#{WARN} — run `pwn setup --migrate`)" : "(#{OK})"}"
292
+ st[:files].each do |f|
293
+ mark = f[:ok] ? OK : BAD
294
+ note = f[:ok] ? '' : "→ #{f[:why]} [fix: #{f[:fix]}]"
295
+ sz = f[:exists] ? human_size(bytes: f[:size]) : '—'
296
+ io.puts " #{f[:rel].ljust(22)} #{mark} #{sz.to_s.ljust(8)} #{f[:owner].to_s.ljust(30)} #{note}"
297
+ end
298
+ unless st[:incompatible].empty?
299
+ io.puts
300
+ io.puts "#{st[:incompatible].length} file(s) incompatible with pwn #{st[:pwn_version]} — " \
301
+ 'run `pwn setup --migrate --fix` to autofix (a timestamped backup is taken first).'
302
+ end
303
+ st
304
+ rescue StandardError => e
305
+ raise e
306
+ end
307
+
308
+ # Supported Method Parameters::
309
+ # report = PWN::Migrate.run(
310
+ # fix: 'optional - also autofix incompatible files (default: schema-migrations only)',
311
+ # backup: 'optional - take timestamped backup of ~/.pwn first (default true)',
312
+ # dry_run: 'optional - report what WOULD happen, write nothing (default false)',
313
+ # key: 'optional - vault key (default: read ~/.pwn/pwn.yaml.decryptor)',
314
+ # iv: 'optional - vault iv (default: read ~/.pwn/pwn.yaml.decryptor)',
315
+ # io: 'optional - IO to write to (default $stdout)'
316
+ # )
317
+ #
318
+ # 1. backup 2. schema migrations installed→current 3. per-file
319
+ # autofix (when fix:true) 4. pwn.yaml key backfill (when fix:true
320
+ # and decryptor available) 5. stamp .schema.
321
+
322
+ public_class_method def self.run(opts = {})
323
+ fix = opts[:fix] ? true : false
324
+ dry_run = opts[:dry_run] ? true : false
325
+ backup = opts.fetch(:backup, true)
326
+ io = opts[:io] || $stdout
327
+
328
+ FileUtils.mkdir_p(PWN_ROOT)
329
+ before = status
330
+ io.puts "PWN::Migrate — pwn #{before[:pwn_version]} · ~/.pwn schema " \
331
+ "#{before[:schema_installed]} → #{before[:schema_current]}" \
332
+ "#{' (dry-run)' if dry_run}"
333
+
334
+ bpath = nil
335
+ if backup && !dry_run && File.directory?(PWN_ROOT)
336
+ bpath = backup!
337
+ io.puts " backup : #{bpath}"
338
+ end
339
+
340
+ # ── ordered schema migrations ────────────────────────────────────
341
+ applied = []
342
+ ((before[:schema_installed] + 1)..SCHEMA_VERSION).each do |n|
343
+ m = MIGRATIONS[n]
344
+ next unless m
345
+
346
+ io.puts " migrate : v#{n - 1} → v#{n}"
347
+ m.call(PWN_ROOT, io) unless dry_run
348
+ applied << n
349
+ end
350
+
351
+ # ── per-file autofix ─────────────────────────────────────────────
352
+ fixed = []
353
+ if fix
354
+ before[:incompatible].each do |f|
355
+ io.puts " fix : #{f[:rel].ljust(22)} [#{f[:fix]}] #{f[:why]}"
356
+ next if dry_run
357
+
358
+ fixed << f.merge(result: apply_fix(file: f))
359
+ end
360
+
361
+ # pwn.yaml key backfill runs even when the vault verifier passed —
362
+ # "encrypted" ≠ "has every key this release added".
363
+ vd = backfill_vault(key: opts[:key], iv: opts[:iv], dry_run: dry_run, io: io)
364
+ fixed << vd if vd && vd[:added].to_i.positive?
365
+ end
366
+
367
+ stamp! unless dry_run
368
+
369
+ after = status
370
+ io.puts
371
+ io.puts " result : #{after[:incompatible].length} incompatible remaining " \
372
+ "(was #{before[:incompatible].length}) · schema now #{after[:schema_installed]}"
373
+
374
+ { backup: bpath, applied_migrations: applied, fixed: fixed,
375
+ before: before, after: after, dry_run: dry_run }
376
+ rescue StandardError => e
377
+ raise e
378
+ end
379
+
380
+ # Supported Method Parameters::
381
+ # drift = PWN::Migrate.vault_drift(
382
+ # key: 'optional - vault key', iv: 'optional - vault iv'
383
+ # )
384
+ #
385
+ # Deep-diff the user's decrypted pwn.yaml against the current
386
+ # PWN::Config.env_template. → { missing: [dotted.paths], user: {…} }
387
+ # Returns nil when the decryptor is unavailable (never prompts).
388
+
389
+ public_class_method def self.vault_drift(opts = {})
390
+ key, iv = decryptor(opts)
391
+ return nil unless key && iv
392
+
393
+ yaml = File.join(PWN_ROOT, 'pwn.yaml')
394
+ return nil unless File.file?(yaml) && PWN::Plugins::Vault.file_encrypted?(file: yaml)
395
+
396
+ user = PWN::Plugins::Vault.dump(file: yaml, key: key, iv: iv)
397
+ tmpl = env_template
398
+ { missing: missing_paths(tmpl: tmpl, user: user), user: user, template: tmpl }
399
+ rescue StandardError
400
+ nil
401
+ end
402
+
403
+ # Supported Method Parameters::
404
+ # r = PWN::Migrate.backfill_vault(
405
+ # key: 'optional', iv: 'optional', dry_run: false, io: $stdout
406
+ # )
407
+ #
408
+ # Decrypt → deep-merge template UNDER user (never overwrites) →
409
+ # re-encrypt with the SAME key/iv. This is the fix for the #1
410
+ # upgrade break: `NoMethodError: undefined method '[]' for nil` when
411
+ # a release added `env[:ai][:new_engine][:…]` and the user's vault
412
+ # predates it.
413
+
414
+ public_class_method def self.backfill_vault(opts = {})
415
+ io = opts[:io] || $stdout
416
+ dry_run = opts[:dry_run] ? true : false
417
+ d = vault_drift(key: opts[:key], iv: opts[:iv])
418
+ unless d
419
+ io.puts ' vault : skipped (no decryptor available — run with --pwn-dec or set key:/iv:)'
420
+ return nil
421
+ end
422
+ if d[:missing].empty?
423
+ io.puts " vault : #{OK} (no missing keys vs PWN::Config.env_template)"
424
+ return { rel: 'pwn.yaml', added: 0, missing: [] }
425
+ end
426
+
427
+ io.puts " vault : #{WARN} #{d[:missing].length} missing key(s) — backfilling under user values"
428
+ d[:missing].first(8).each { |p| io.puts " + #{p}" }
429
+ io.puts " … (+#{d[:missing].length - 8} more)" if d[:missing].length > 8
430
+ return { rel: 'pwn.yaml', added: d[:missing].length, missing: d[:missing], dry_run: true } if dry_run
431
+
432
+ merged = deep_defaults(user: d[:user], tmpl: d[:template])
433
+ yaml = File.join(PWN_ROOT, 'pwn.yaml')
434
+ key, iv = decryptor(opts)
435
+ # Same colon-stripping default_env uses so the file stays diff-clean.
436
+ File.write(yaml, YAML.dump(merged).gsub(/^(\s*):/, '\1'))
437
+ File.chmod(0o600, yaml)
438
+ PWN::Plugins::Vault.encrypt(file: yaml, key: key, iv: iv)
439
+
440
+ { rel: 'pwn.yaml', added: d[:missing].length, missing: d[:missing] }
441
+ rescue StandardError => e
442
+ io.puts " vault : #{BAD} #{e.class}: #{e.message}"
443
+ nil
444
+ end
445
+
446
+ # Supported Method Parameters::
447
+ # tmpl = PWN::Migrate.env_template
448
+ #
449
+ # The canonical current-release pwn.yaml shape, WITHOUT the I/O
450
+ # side-effects of PWN::Config.default_env. Prefers
451
+ # PWN::Config.env_template when that refactor is present.
452
+
453
+ public_class_method def self.env_template
454
+ return PWN::Config.env_template if PWN::Config.respond_to?(:env_template)
455
+
456
+ # Fallback: intercept default_env's write path. Never reached once
457
+ # PWN::Config.env_template ships, but keeps Migrate self-sufficient.
458
+ {}
459
+ end
460
+
461
+ # ──────────────────────────────────────────────────────────────────
462
+ # internals
463
+ # ──────────────────────────────────────────────────────────────────
464
+
465
+ private_class_method def self.verify_one(opts = {})
466
+ meta = opts[:meta]
467
+ path = opts[:path]
468
+ raw = opts[:raw]
469
+ # Nonexistent leaf files are fine — owners lazily seed on first
470
+ # write. A missing pwn.yaml IS a problem though (kind :vault).
471
+ return { ok: true } if !File.exist?(path) && meta[:kind] != :vault
472
+
473
+ # Raw parseability first (owners rescue → fallback so we'd never see it).
474
+ case meta[:kind]
475
+ when :json
476
+ JSON.parse(raw.to_s) if raw
477
+ when :yaml
478
+ YAML.safe_load(raw.to_s, permitted_classes: [Symbol, Time, Date], aliases: true) if raw
479
+ end
480
+
481
+ v = meta[:verify]
482
+ return jsonl_verify(path: path) if v == :jsonl
483
+
484
+ v.call(path, raw)
485
+ rescue JSON::ParserError, Psych::SyntaxError => e
486
+ { ok: false, why: "unparsable #{meta[:kind]}: #{e.message[0, 60]}" }
487
+ rescue StandardError => e
488
+ { ok: false, why: "#{e.class}: #{e.message[0, 80]}" }
489
+ end
490
+
491
+ private_class_method def self.jsonl_verify(opts = {})
492
+ path = opts[:path]
493
+ total = 0
494
+ bad = 0
495
+ File.foreach(path) do |l|
496
+ next if l.strip.empty?
497
+
498
+ total += 1
499
+ JSON.parse(l)
500
+ rescue StandardError
501
+ bad += 1
502
+ end
503
+ { ok: bad.zero?, why: bad.zero? ? nil : "#{bad}/#{total} lines unparsable" }
504
+ end
505
+
506
+ private_class_method def self.apply_fix(opts = {})
507
+ f = opts[:file]
508
+ case f[:fix]
509
+ when :quarantine
510
+ quarantine!(rel: f[:rel])
511
+ when :jsonl
512
+ strip_bad_jsonl!(rel: f[:rel])
513
+ when :skills
514
+ PWN::Config.migrate_legacy_skills
515
+ when :seed
516
+ FileUtils.mkdir_p(File.join(PWN_ROOT, f[:rel])) if f[:kind] == :dir
517
+ PWN::Cron.install_defaults if f[:rel].start_with?('cron/') && defined?(PWN::Cron)
518
+ :seeded
519
+ when :vault
520
+ :deferred_to_backfill
521
+ else
522
+ :noop
523
+ end
524
+ rescue StandardError => e
525
+ "#{e.class}: #{e.message}"
526
+ end
527
+
528
+ private_class_method def self.quarantine!(opts = {})
529
+ src = File.join(PWN_ROOT, opts[:rel])
530
+ return :absent unless File.exist?(src)
531
+
532
+ dst = File.join(QUARANTINE_DIR, Time.now.utc.strftime('%Y%m%d_%H%M%S'))
533
+ FileUtils.mkdir_p(dst)
534
+ FileUtils.mv(src, File.join(dst, File.basename(opts[:rel])))
535
+ "quarantined → #{dst}"
536
+ end
537
+
538
+ private_class_method def self.strip_bad_jsonl!(opts = {})
539
+ path = File.join(PWN_ROOT, opts[:rel])
540
+ return :absent unless File.file?(path)
541
+
542
+ good = []
543
+ bad = 0
544
+ File.foreach(path) do |l|
545
+ next if l.strip.empty?
546
+
547
+ JSON.parse(l)
548
+ good << l
549
+ rescue StandardError
550
+ bad += 1
551
+ end
552
+ File.write(path, good.join)
553
+ "dropped #{bad} bad line(s), kept #{good.length}"
554
+ end
555
+
556
+ private_class_method def self.backup!
557
+ ts = Time.now.utc.strftime('%Y%m%d_%H%M%S')
558
+ dst = File.join(BACKUP_ROOT, ts)
559
+ FileUtils.mkdir_p(dst)
560
+ Dir.children(PWN_ROOT).each do |c|
561
+ next if %w[backup quarantine sessions swarm].include?(c) # large / regenerable
562
+
563
+ src = File.join(PWN_ROOT, c)
564
+ FileUtils.cp_r(src, dst)
565
+ rescue StandardError
566
+ next
567
+ end
568
+ dst
569
+ end
570
+
571
+ private_class_method def self.stamp!
572
+ FileUtils.mkdir_p(PWN_ROOT)
573
+ File.write(
574
+ SCHEMA_FILE,
575
+ JSON.pretty_generate(
576
+ schema: SCHEMA_VERSION,
577
+ pwn_version: (defined?(PWN::VERSION) ? PWN::VERSION : nil),
578
+ at: Time.now.utc.iso8601
579
+ )
580
+ )
581
+ SCHEMA_VERSION
582
+ end
583
+
584
+ private_class_method def self.decryptor(opts = {})
585
+ key = opts[:key]
586
+ iv = opts[:iv]
587
+ return [key, iv] if key && iv
588
+
589
+ dec = File.join(PWN_ROOT, 'pwn.yaml.decryptor')
590
+ return [nil, nil] unless File.file?(dec)
591
+
592
+ d = YAML.load_file(dec, symbolize_names: true)
593
+ [d[:key], d[:iv]]
594
+ rescue StandardError
595
+ [nil, nil]
596
+ end
597
+
598
+ private_class_method def self.safe_read(opts = {})
599
+ path = opts[:path]
600
+ File.size(path) > 5_000_000 ? nil : File.read(path)
601
+ rescue StandardError
602
+ nil
603
+ end
604
+
605
+ private_class_method def self.human_size(opts = {})
606
+ bytes = opts[:bytes]
607
+ return '' if bytes.nil?
608
+
609
+ u = %w[B K M G]
610
+ i = 0
611
+ n = bytes.to_f
612
+ while n >= 1024 && i < u.length - 1
613
+ n /= 1024
614
+ i += 1
615
+ end
616
+ format('%<n>.4g%<u>s', n: n, u: u[i])
617
+ end
618
+
619
+ # template UNDER user — user values always win; template only fills
620
+ # holes. Arrays are treated as leaves (never element-merged).
621
+ private_class_method def self.deep_defaults(opts = {})
622
+ user = opts[:user]
623
+ tmpl = opts[:tmpl]
624
+ return user unless tmpl.is_a?(Hash)
625
+ return tmpl unless user.is_a?(Hash)
626
+
627
+ out = user.dup
628
+ tmpl.each do |k, tv|
629
+ uv = out.key?(k) ? out[k] : out[k.to_s]
630
+ out[k] = if tv.is_a?(Hash)
631
+ deep_defaults(user: uv, tmpl: tv)
632
+ elsif uv.nil? && !out.key?(k) && !out.key?(k.to_s)
633
+ tv
634
+ else
635
+ uv
636
+ end
637
+ end
638
+ out
639
+ end
640
+
641
+ private_class_method def self.missing_paths(opts = {})
642
+ tmpl = opts[:tmpl]
643
+ user = opts[:user]
644
+ prefix = opts[:prefix] || ''
645
+ out = []
646
+ return out unless tmpl.is_a?(Hash)
647
+
648
+ tmpl.each do |k, tv|
649
+ dotted = prefix.empty? ? k.to_s : "#{prefix}.#{k}"
650
+ uv = nil
651
+ if user.is_a?(Hash)
652
+ uv = user[k]
653
+ uv = user[k.to_s] if uv.nil?
654
+ end
655
+ if tv.is_a?(Hash)
656
+ if uv.is_a?(Hash)
657
+ out.concat(missing_paths(tmpl: tv, user: uv, prefix: dotted))
658
+ else
659
+ out << dotted
660
+ end
661
+ elsif !user.is_a?(Hash) || (!user.key?(k) && !user.key?(k.to_s))
662
+ out << dotted
663
+ end
664
+ end
665
+ out
666
+ end
667
+
668
+ # Author(s):: 0day Inc. <support@0dayinc.com>
669
+
670
+ public_class_method def self.authors
671
+ "AUTHOR(S):
672
+ 0day Inc. <support@0dayinc.com>
673
+ "
674
+ end
675
+
676
+ # Display Usage for this Module
677
+
678
+ public_class_method def self.help
679
+ puts "USAGE:
680
+ # Is ~/.pwn older than this pwn release's schema?
681
+ #{self}.needed? # => true/false (cheap; used by refresh_env)
682
+
683
+ # Read-only per-file compatibility report.
684
+ #{self}.status # => { schema_installed:, schema_current:, files:[…], incompatible:[…] }
685
+ #{self}.check # human-readable to $stdout
686
+
687
+ # Apply schema migrations + (optionally) autofix incompatible files.
688
+ # Always takes a timestamped backup under ~/.pwn/backup/<ts>/ first.
689
+ #{self}.run(
690
+ fix: 'optional - also autofix per-file (default false)',
691
+ backup: 'optional - default true',
692
+ dry_run: 'optional - default false',
693
+ key: 'optional - vault key (default: ~/.pwn/pwn.yaml.decryptor)',
694
+ iv: 'optional - vault iv'
695
+ )
696
+
697
+ # pwn.yaml specifically — deep-merge missing keys from the
698
+ # current release template UNDER the user's values, re-encrypt.
699
+ #{self}.vault_drift # => { missing: [dotted.paths], … } or nil
700
+ #{self}.backfill_vault
701
+
702
+ # Data:
703
+ #{self}::SCHEMA_VERSION # bump when a ~/.pwn file shape changes
704
+ #{self}::STATE_FILES # rel-path → owner → verifier → fix
705
+ #{self}::MIGRATIONS # ordered idempotent transforms
706
+
707
+ # From the shell:
708
+ pwn setup --migrate # == #{self}.run
709
+ pwn setup --migrate --fix # == #{self}.run(fix: true)
710
+ pwn setup --migrate --dry-run
711
+
712
+ #{self}.authors
713
+ "
714
+ end
715
+ end
716
+ end
data/lib/pwn/setup.rb CHANGED
@@ -372,6 +372,13 @@ module PWN
372
372
  end
373
373
  io.puts
374
374
 
375
+ # ~/.pwn state ----------------------------------------------------
376
+ migrate = nil
377
+ if defined?(PWN::Migrate)
378
+ migrate = PWN::Migrate.check(io: io)
379
+ io.puts
380
+ end
381
+
375
382
  total = NATIVE_GEMS.size + TOOLCHAIN.size
376
383
  missing = gem_missing.size + bin_missing.size
377
384
  io.puts "#{total - missing} / #{total} capabilities usable · #{missing} degraded"
@@ -381,8 +388,9 @@ module PWN
381
388
  io.puts ' `pwn setup --profile <name>` for a subset. See `pwn setup --list-profiles`.'
382
389
  end
383
390
 
384
- { ok: missing.zero?, native_gems_missing: gem_missing, toolchain_missing: bin_missing,
385
- pkg_manager: pm[:key], os: os, arch: arch }
391
+ { ok: missing.zero? && Array(migrate && migrate[:incompatible]).empty?,
392
+ native_gems_missing: gem_missing, toolchain_missing: bin_missing,
393
+ state: migrate, pkg_manager: pm[:key], os: os, arch: arch }
386
394
  rescue StandardError => e
387
395
  raise e
388
396
  end
@@ -493,6 +501,28 @@ module PWN
493
501
  raise e
494
502
  end
495
503
 
504
+ # Supported Method Parameters::
505
+ # PWN::Setup.migrate(
506
+ # fix: 'optional - also autofix incompatible ~/.pwn files (default false)',
507
+ # dry_run: 'optional - print what WOULD happen (default false)',
508
+ # yes: 'optional - alias for fix:true (CI-friendly)',
509
+ # io: 'optional - IO to write to (default $stdout)'
510
+ # )
511
+ #
512
+ # Delegate to PWN::Migrate.run — verify every ~/.pwn state file is
513
+ # compatible with THIS pwn release and (optionally) autofix it. A
514
+ # timestamped backup of ~/.pwn is taken first. See `PWN::Migrate.help`.
515
+
516
+ public_class_method def self.migrate(opts = {})
517
+ PWN::Migrate.run(
518
+ fix: opts[:fix] || opts[:yes],
519
+ dry_run: opts[:dry_run],
520
+ io: opts[:io] || $stdout
521
+ )
522
+ rescue StandardError => e
523
+ raise e
524
+ end
525
+
496
526
  # ------------------------------------------------------------------
497
527
 
498
528
  private_class_method def self.detect_os_attr(opts = {})
@@ -585,6 +615,8 @@ module PWN
585
615
  pwn setup --profile sdr --yes
586
616
  pwn setup --list-profiles
587
617
  pwn setup --dry-run --profile net
618
+ pwn setup --migrate # verify + upgrade ~/.pwn schema
619
+ pwn setup --migrate --fix # also autofix incompatible files
588
620
 
589
621
  #{self}.authors
590
622
  "
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.635'
4
+ VERSION = '0.5.636'
5
5
  end
data/lib/pwn.rb CHANGED
@@ -19,6 +19,7 @@ module PWN
19
19
  autoload :Cron, 'pwn/cron'
20
20
  autoload :Memory, 'pwn/memory'
21
21
  autoload :MemoryIndex, 'pwn/memory_index'
22
+ autoload :Migrate, 'pwn/migrate'
22
23
  autoload :Sessions, 'pwn/sessions'
23
24
  autoload :Setup, 'pwn/setup'
24
25
  autoload :Driver, 'pwn/driver'
@@ -57,9 +57,9 @@ RSpec.describe 'documentation/Installation.md' do
57
57
  end
58
58
 
59
59
  it 'programmatic API section is truthful' do
60
- expect(PWN::Setup).to respond_to(:check, :deps, :list_profiles, :pkg_manager)
60
+ expect(PWN::Setup).to respond_to(:check, :deps, :list_profiles, :pkg_manager, :migrate)
61
61
  expect(PWN::Setup.check(io: StringIO.new).keys)
62
- .to eq(%i[ok native_gems_missing toolchain_missing pkg_manager os arch])
62
+ .to eq(%i[ok native_gems_missing toolchain_missing state pkg_manager os arch])
63
63
  expect(PWN::Setup.pkg_manager.keys).to eq(%i[key install sudo])
64
64
  expect(PWN::Setup.constants).to include(:NATIVE_GEMS, :TOOLCHAIN, :PROFILES)
65
65
  end
@@ -15,6 +15,8 @@ describe PWN::AI::Agent::Mistakes do
15
15
 
16
16
  it 'fingerprints, counts and resolves recurring failures' do
17
17
  stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(Dir.mktmpdir, 'mistakes.json'))
18
+ stub_const('PWN::AI::Agent::Reward::PREFERENCES_FILE', File.join(Dir.mktmpdir, 'prefs.jsonl')) if defined?(PWN::AI::Agent::Reward)
19
+ stub_const('PWN::Memory::MEMORY_FILE', File.join(Dir.mktmpdir, 'memory.json')) if defined?(PWN::Memory)
18
20
  PWN::AI::Agent::Mistakes.reset
19
21
  a = PWN::AI::Agent::Mistakes.record(tool: 'shell', error: 'nmpa: command not found at /tmp/x:42')
20
22
  b = PWN::AI::Agent::Mistakes.record(tool: 'shell', error: 'nmpa: command not found at /var/y:99')
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Migrate do
6
+ it 'should display information for authors' do
7
+ authors_response = PWN::Migrate
8
+ expect(authors_response).to respond_to :authors
9
+ end
10
+
11
+ it 'should display information for existing help method' do
12
+ help_response = PWN::Migrate
13
+ expect(help_response).to respond_to :help
14
+ end
15
+
16
+ it 'exposes the schema / state-file data tables' do
17
+ expect(PWN::Migrate::SCHEMA_VERSION).to be_a(Integer)
18
+ expect(PWN::Migrate::STATE_FILES).to be_a(Hash)
19
+ expect(PWN::Migrate::STATE_FILES).to have_key('pwn.yaml')
20
+ expect(PWN::Migrate::MIGRATIONS).to be_a(Hash)
21
+ expect(PWN::Migrate::MIGRATIONS.keys.max).to eq(PWN::Migrate::SCHEMA_VERSION)
22
+ end
23
+
24
+ it 'env_template is the pure hash used for vault backfill' do
25
+ t = PWN::Config.env_template
26
+ expect(t).to be_a(Hash)
27
+ expect(t).to have_key(:ai)
28
+ expect(t[:ai]).to have_key(:agent)
29
+ expect(PWN::Migrate.env_template).to eq(t)
30
+ end
31
+
32
+ # ────────────────────────────────────────────────────────────────────
33
+ # functional round-trip in a tmp sandbox — never touches real ~/.pwn
34
+ # ────────────────────────────────────────────────────────────────────
35
+ context 'sandboxed ~/.pwn', :aggregate_failures do
36
+ include_context 'pwn tmp sandbox'
37
+
38
+ let(:io) { StringIO.new }
39
+
40
+ before do
41
+ # Skills verifier globs the REAL PWN::Config.pwn_skills_path unless
42
+ # redirected — keep it inside the sandbox too.
43
+ allow(PWN::Config).to receive(:pwn_skills_path).and_return(File.join(@tmp, 'skills'))
44
+ end
45
+
46
+ it '.needed? / .status on a fresh (empty) root' do
47
+ expect(PWN::Migrate.installed_schema).to eq(0)
48
+ expect(PWN::Migrate.needed?).to be true
49
+
50
+ st = PWN::Migrate.status
51
+ expect(st[:schema_installed]).to eq(0)
52
+ expect(st[:schema_current]).to eq(PWN::Migrate::SCHEMA_VERSION)
53
+ expect(st[:files]).to be_an(Array)
54
+ # only pwn.yaml is REQUIRED to exist; every other absent file is ok:true
55
+ missing_vault = st[:files].find { |f| f[:rel] == 'pwn.yaml' }
56
+ expect(missing_vault[:ok]).to be false
57
+ end
58
+
59
+ it '.run applies migrations, stamps .schema, and clears needed?' do
60
+ r = PWN::Migrate.run(fix: false, backup: false, io: io)
61
+ expect(r[:applied_migrations]).to include(1)
62
+ expect(File).to exist(File.join(@tmp, '.schema'))
63
+ expect(PWN::Migrate.installed_schema).to eq(PWN::Migrate::SCHEMA_VERSION)
64
+ expect(PWN::Migrate.needed?).to be false
65
+ # migration 1 seeds the state subdirs
66
+ %w[skills sessions swarm cron curriculum finetune extrospection].each do |d|
67
+ expect(File).to be_directory(File.join(@tmp, d))
68
+ end
69
+ end
70
+
71
+ it 'detects & autofixes a corrupt JSON store (quarantine)' do
72
+ File.write(File.join(@tmp, 'metrics.json'), '{{{ not json')
73
+ st = PWN::Migrate.status
74
+ row = st[:files].find { |f| f[:rel] == 'metrics.json' }
75
+ expect(row[:ok]).to be false
76
+ expect(row[:why]).to match(/unparsable/)
77
+
78
+ PWN::Migrate.run(fix: true, backup: false, io: io)
79
+ expect(File).not_to exist(File.join(@tmp, 'metrics.json'))
80
+ expect(Dir.glob(File.join(@tmp, 'quarantine', '*', 'metrics.json'))).not_to be_empty
81
+ # owner falls back cleanly after quarantine
82
+ expect(PWN::AI::Agent::Metrics.load).to eq(tools: {}, updated_at: nil)
83
+ end
84
+
85
+ it 'detects & autofixes bad JSONL lines without losing good history' do
86
+ path = File.join(@tmp, 'learning.jsonl')
87
+ File.write(path, %({"task":"a","success":true}\nNOT JSON\n{"task":"b","success":false}\n))
88
+ row = PWN::Migrate.status[:files].find { |f| f[:rel] == 'learning.jsonl' }
89
+ expect(row[:ok]).to be false
90
+ expect(row[:why]).to match(%r{1/3})
91
+
92
+ PWN::Migrate.run(fix: true, backup: false, io: io)
93
+ kept = File.readlines(path)
94
+ expect(kept.length).to eq(2)
95
+ kept.each { |l| expect { JSON.parse(l) }.not_to raise_error }
96
+ end
97
+
98
+ it 'detects legacy flat skills and converts them (fix: :skills)' do
99
+ sroot = File.join(@tmp, 'skills')
100
+ FileUtils.mkdir_p(sroot)
101
+ File.write(File.join(sroot, 'legacy_probe.md'), "# legacy\n\nBody line.\n")
102
+ row = PWN::Migrate.status[:files].find { |f| f[:rel] == 'skills/' }
103
+ expect(row[:ok]).to be false
104
+
105
+ PWN::Migrate.run(fix: true, backup: false, io: io)
106
+ expect(File).to exist(File.join(sroot, 'legacy-probe', 'SKILL.md'))
107
+ expect(Dir.glob(File.join(sroot, '*.md'))).to be_empty
108
+ end
109
+
110
+ it 'deep_defaults never overwrites a user value' do
111
+ user = { ai: { active: 'anthropic', anthropic: { key: 'sk-LIVE' } } }
112
+ tmpl = PWN::Config.env_template
113
+ merged = PWN::Migrate.send(:deep_defaults, user: user, tmpl: tmpl)
114
+ expect(merged[:ai][:active]).to eq('anthropic') # user wins
115
+ expect(merged[:ai][:anthropic][:key]).to eq('sk-LIVE') # user wins
116
+ expect(merged[:ai][:anthropic]).to have_key(:model) # backfilled
117
+ expect(merged[:ai]).to have_key(:agent) # whole subtree backfilled
118
+ expect(merged).to have_key(:cron) # top-level backfilled
119
+ end
120
+
121
+ it 'missing_paths reports dotted keys absent from a stale user vault' do
122
+ stale = { ai: { active: 'grok', grok: { key: 'x' } } }
123
+ miss = PWN::Migrate.send(:missing_paths, tmpl: PWN::Config.env_template, user: stale)
124
+ expect(miss).to include('ai.agent')
125
+ expect(miss).to include('memory')
126
+ expect(miss).not_to include('ai.active')
127
+ end
128
+
129
+ it 'dry_run writes nothing' do
130
+ r = PWN::Migrate.run(fix: true, backup: true, dry_run: true, io: io)
131
+ expect(r[:dry_run]).to be true
132
+ expect(File).not_to exist(File.join(@tmp, '.schema'))
133
+ expect(Dir.glob(File.join(@tmp, 'backup', '*'))).to be_empty
134
+ end
135
+ end
136
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pwn'
4
+ require 'stringio'
4
5
 
5
6
  Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
7
+
8
+ # ─────────────────────────────────────────────────────────────────────────────
9
+ # Keep `rake` output clean.
10
+ #
11
+ # RSpec binds its formatter to $stdout at configure-time (before any example
12
+ # runs), so redirecting $stdout / $stderr *inside* each example swallows any
13
+ # incidental `puts` / `print` / `p` / `warn` emitted by the code under test
14
+ # WITHOUT affecting the progress dots or failure reports.
15
+ #
16
+ # Escape hatches:
17
+ # - `PWN_SPEC_VERBOSE=1 rake spec` → no redirection at all
18
+ # - `it '...', :stdout do ... end` → per-example opt-out
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ RSpec.configure do |config|
21
+ next if ENV['PWN_SPEC_VERBOSE']
22
+
23
+ original_stdout = $stdout
24
+ original_stderr = $stderr
25
+ devnull = StringIO.new
26
+
27
+ config.before(:each) do |example|
28
+ next if example.metadata[:stdout]
29
+
30
+ devnull.truncate(0)
31
+ devnull.rewind
32
+ $stdout = devnull
33
+ $stderr = devnull
34
+ end
35
+
36
+ config.after(:each) do
37
+ $stdout = original_stdout
38
+ $stderr = original_stderr
39
+ end
40
+ end
@@ -49,6 +49,12 @@ RSpec.shared_context 'pwn tmp sandbox' do
49
49
  stub_const(const, File.join(@tmp, rel)) if Object.const_defined?(owner)
50
50
  end
51
51
  stub_const('PWN::MemoryIndex::INDEX_FILE', File.join(@tmp, 'memory.idx')) if defined?(PWN::MemoryIndex)
52
+ if defined?(PWN::Migrate)
53
+ stub_const('PWN::Migrate::PWN_ROOT', @tmp)
54
+ stub_const('PWN::Migrate::SCHEMA_FILE', File.join(@tmp, '.schema'))
55
+ stub_const('PWN::Migrate::BACKUP_ROOT', File.join(@tmp, 'backup'))
56
+ stub_const('PWN::Migrate::QUARANTINE_DIR', File.join(@tmp, 'quarantine'))
57
+ end
52
58
 
53
59
  # Controllable env — module_reflection OFF so nothing hits an engine.
54
60
  @agent_cfg = {}
@@ -908,6 +908,7 @@
908
908
  {"messages":[{"role":"user","content":"PWN::Bounty::LifecycleAuthzReplay.write_yaml Usage"},{"role":"assistant","content":"`PWN::Bounty::LifecycleAuthzReplay.write_yaml`: "}]}
909
909
  {"messages":[{"role":"user","content":"PWN::Config.authors Usage"},{"role":"assistant","content":"`PWN::Config.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
910
910
  {"messages":[{"role":"user","content":"PWN::Config.default_env Usage"},{"role":"assistant","content":"`PWN::Config.default_env`: Supported Method Parameters\n\nenv = PWN::Config.default_env(\n\npwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml'\n\n)\n"}]}
911
+ {"messages":[{"role":"user","content":"PWN::Config.env_template Usage"},{"role":"assistant","content":"`PWN::Config.env_template`: Supported Method Parameters\n\ntmpl = PWN::Config.env_template\n\nThe canonical current-release ~/.pwn/pwn.yaml shape as a pure Hash (no I/O, no vault write, no puts). Single source of truth used by:\n\n* PWN::Config.default_env — seed a fresh ~/.pwn/pwn.yaml\n* PWN::Migrate.vault_drift — diff a user vault against this release\n* PWN::Migrate.backfill_vault — deep-merge missing keys UNDER the\n user values on `pwn setup --migrate --fix`\n"}]}
911
912
  {"messages":[{"role":"user","content":"PWN::Config.help Usage"},{"role":"assistant","content":"`PWN::Config.help`: "}]}
912
913
  {"messages":[{"role":"user","content":"PWN::Config.init_driver_options Usage"},{"role":"assistant","content":"`PWN::Config.init_driver_options`: Supported Method Parameters\n\nenv = PWN::Config.init_driver_options\n"}]}
913
914
  {"messages":[{"role":"user","content":"PWN::Config.load_memory Usage"},{"role":"assistant","content":"`PWN::Config.load_memory`: Supported Method Parameters\n\nPWN::Config.load_memory\n"}]}
@@ -1047,6 +1048,28 @@
1047
1048
  {"messages":[{"role":"user","content":"PWN::MemoryIndex.save_index Usage"},{"role":"assistant","content":"`PWN::MemoryIndex.save_index`: "}]}
1048
1049
  {"messages":[{"role":"user","content":"PWN::MemoryIndex.sha Usage"},{"role":"assistant","content":"`PWN::MemoryIndex.sha`: "}]}
1049
1050
  {"messages":[{"role":"user","content":"PWN::MemoryIndex.to_context Usage"},{"role":"assistant","content":"`PWN::MemoryIndex.to_context`: Supported Method Parameters\n\nctx = PWN::MemoryIndex.to_context(query:, limit: 6)\n"}]}
1051
+ {"messages":[{"role":"user","content":"PWN::Migrate.apply_fix Usage"},{"role":"assistant","content":"`PWN::Migrate.apply_fix`: "}]}
1052
+ {"messages":[{"role":"user","content":"PWN::Migrate.authors Usage"},{"role":"assistant","content":"`PWN::Migrate.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1053
+ {"messages":[{"role":"user","content":"PWN::Migrate.backfill_vault Usage"},{"role":"assistant","content":"`PWN::Migrate.backfill_vault`: Supported Method Parameters\n\nr = PWN::Migrate.backfill_vault(\n\nkey: 'optional', iv: 'optional', dry_run: false, io: $stdout\n\n)\n\nDecrypt → deep-merge template UNDER user (never overwrites) →re-encrypt with the SAME key/iv. This is the fix for the #1 upgrade break: ‘NoMethodError: undefined method ’[]‘ for nil` when a release added `env[:new_engine]` and the user’s vault predates it.\n"}]}
1054
+ {"messages":[{"role":"user","content":"PWN::Migrate.backup! Usage"},{"role":"assistant","content":"`PWN::Migrate.backup!`: "}]}
1055
+ {"messages":[{"role":"user","content":"PWN::Migrate.check Usage"},{"role":"assistant","content":"`PWN::Migrate.check`: Supported Method Parameters\n\nPWN::Migrate.check(\n\nio: 'optional - IO to write the report to (default $stdout)'\n\n)\n\nHuman-readable ~/.pwn state doctor. Read-only. Called from PWN::Setup.check so ‘pwn setup` shows this alongside the native-gem / toolchain report.\n"}]}
1056
+ {"messages":[{"role":"user","content":"PWN::Migrate.decryptor Usage"},{"role":"assistant","content":"`PWN::Migrate.decryptor`: "}]}
1057
+ {"messages":[{"role":"user","content":"PWN::Migrate.deep_defaults Usage"},{"role":"assistant","content":"`PWN::Migrate.deep_defaults`: "}]}
1058
+ {"messages":[{"role":"user","content":"PWN::Migrate.env_template Usage"},{"role":"assistant","content":"`PWN::Migrate.env_template`: Supported Method Parameters\n\ntmpl = PWN::Migrate.env_template\n\nThe canonical current-release pwn.yaml shape, WITHOUT the I/O side-effects of PWN::Config.default_env. Prefers PWN::Config.env_template when that refactor is present.\n"}]}
1059
+ {"messages":[{"role":"user","content":"PWN::Migrate.help Usage"},{"role":"assistant","content":"`PWN::Migrate.help`: "}]}
1060
+ {"messages":[{"role":"user","content":"PWN::Migrate.human_size Usage"},{"role":"assistant","content":"`PWN::Migrate.human_size`: "}]}
1061
+ {"messages":[{"role":"user","content":"PWN::Migrate.installed_schema Usage"},{"role":"assistant","content":"`PWN::Migrate.installed_schema`: Supported Method Parameters\n\nschema = PWN::Migrate.installed_schema\n\n→ Integer schema number stamped in ~/.pwn/.schema (0 when absent).\n"}]}
1062
+ {"messages":[{"role":"user","content":"PWN::Migrate.jsonl_verify Usage"},{"role":"assistant","content":"`PWN::Migrate.jsonl_verify`: "}]}
1063
+ {"messages":[{"role":"user","content":"PWN::Migrate.missing_paths Usage"},{"role":"assistant","content":"`PWN::Migrate.missing_paths`: "}]}
1064
+ {"messages":[{"role":"user","content":"PWN::Migrate.needed? Usage"},{"role":"assistant","content":"`PWN::Migrate.needed?`: Supported Method Parameters\n\nbool = PWN::Migrate.needed?\n\nCheap predicate for PWN::Config.refresh_env — schema stamp only, no per-file probes, so it adds ~0 to launch time.\n"}]}
1065
+ {"messages":[{"role":"user","content":"PWN::Migrate.quarantine! Usage"},{"role":"assistant","content":"`PWN::Migrate.quarantine!`: "}]}
1066
+ {"messages":[{"role":"user","content":"PWN::Migrate.run Usage"},{"role":"assistant","content":"`PWN::Migrate.run`: Supported Method Parameters\n\nreport = PWN::Migrate.run(\n\nfix: 'optional - also autofix incompatible files (default: schema-migrations only)',\nbackup: 'optional - take timestamped backup of ~/.pwn first (default true)',\ndry_run: 'optional - report what WOULD happen, write nothing (default false)',\nkey: 'optional - vault key (default: read ~/.pwn/pwn.yaml.decryptor)',\niv: 'optional - vault iv (default: read ~/.pwn/pwn.yaml.decryptor)',\nio: 'optional - IO to write to (default $stdout)'\n\n)\n\nbackup 2. schema migrations installed→current 3. per-file\n\n\nautofix (when fix:true) 4. pwn.yaml key backfill (when fix:true and decryptor available) 5. stamp .schema.\n"}]}
1067
+ {"messages":[{"role":"user","content":"PWN::Migrate.safe_read Usage"},{"role":"assistant","content":"`PWN::Migrate.safe_read`: "}]}
1068
+ {"messages":[{"role":"user","content":"PWN::Migrate.stamp! Usage"},{"role":"assistant","content":"`PWN::Migrate.stamp!`: "}]}
1069
+ {"messages":[{"role":"user","content":"PWN::Migrate.status Usage"},{"role":"assistant","content":"`PWN::Migrate.status`: Supported Method Parameters\n\nrows = PWN::Migrate.status\n\nPer-file compatibility report — the machine-readable half of ‘.check`. Never writes. Never raises (each verifier is rescued).\n"}]}
1070
+ {"messages":[{"role":"user","content":"PWN::Migrate.strip_bad_jsonl! Usage"},{"role":"assistant","content":"`PWN::Migrate.strip_bad_jsonl!`: "}]}
1071
+ {"messages":[{"role":"user","content":"PWN::Migrate.vault_drift Usage"},{"role":"assistant","content":"`PWN::Migrate.vault_drift`: Supported Method Parameters\n\ndrift = PWN::Migrate.vault_drift(\n\nkey: 'optional - vault key', iv: 'optional - vault iv'\n\n)\n\nDeep-diff the user’s decrypted pwn.yaml against the current PWN::Config.env_template. → { missing: [dotted.paths], user: {…} } Returns nil when the decryptor is unavailable (never prompts).\n"}]}
1072
+ {"messages":[{"role":"user","content":"PWN::Migrate.verify_one Usage"},{"role":"assistant","content":"`PWN::Migrate.verify_one`: "}]}
1050
1073
  {"messages":[{"role":"user","content":"PWN::Plugins.authors Usage"},{"role":"assistant","content":"`PWN::Plugins.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1051
1074
  {"messages":[{"role":"user","content":"PWN::Plugins.help Usage"},{"role":"assistant","content":"`PWN::Plugins.help`: "}]}
1052
1075
  {"messages":[{"role":"user","content":"PWN::Plugins::Android.adb_net_connect Usage"},{"role":"assistant","content":"`PWN::Plugins::Android.adb_net_connect`: Supported Method Parameters\n\nPWN::Plugins::Android.adb_net_connect(\n\nadb_path: 'required - path to adb binary',\ntarget: 'required - target host or IP to connect',\nport: 'optional - defaults to tcp 5555'\n\n)\n"}]}
@@ -2149,6 +2172,7 @@
2149
2172
  {"messages":[{"role":"user","content":"PWN::Setup.gem_loadable? Usage"},{"role":"assistant","content":"`PWN::Setup.gem_loadable?`: "}]}
2150
2173
  {"messages":[{"role":"user","content":"PWN::Setup.help Usage"},{"role":"assistant","content":"`PWN::Setup.help`: "}]}
2151
2174
  {"messages":[{"role":"user","content":"PWN::Setup.list_profiles Usage"},{"role":"assistant","content":"`PWN::Setup.list_profiles`: Supported Method Parameters\n\nPWN::Setup.list_profiles(\n\nio: 'optional - IO to write to (default $stdout)'\n\n)\n"}]}
2175
+ {"messages":[{"role":"user","content":"PWN::Setup.migrate Usage"},{"role":"assistant","content":"`PWN::Setup.migrate`: Supported Method Parameters\n\nPWN::Setup.migrate(\n\nfix: 'optional - also autofix incompatible ~/.pwn files (default false)',\ndry_run: 'optional - print what WOULD happen (default false)',\nyes: 'optional - alias for fix:true (CI-friendly)',\nio: 'optional - IO to write to (default $stdout)'\n\n)\n\nDelegate to PWN::Migrate.run — verify every ~/.pwn state file is compatible with THIS pwn release and (optionally) autofix it. A timestamped backup of ~/.pwn is taken first. See ‘PWN::Migrate.help`.\n"}]}
2152
2176
  {"messages":[{"role":"user","content":"PWN::Setup.pkg_manager Usage"},{"role":"assistant","content":"`PWN::Setup.pkg_manager`: Supported Method Parameters\n\nPWN::Setup.pkg_manager\n"}]}
2153
2177
  {"messages":[{"role":"user","content":"PWN::Setup.which Usage"},{"role":"assistant","content":"`PWN::Setup.which`: "}]}
2154
2178
  {"messages":[{"role":"user","content":"PWN::WWW.authors Usage"},{"role":"assistant","content":"`PWN::WWW.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\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.635
4
+ version: 0.5.636
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -2040,6 +2040,7 @@ files:
2040
2040
  - lib/pwn/ffi/volk.rb
2041
2041
  - lib/pwn/memory.rb
2042
2042
  - lib/pwn/memory_index.rb
2043
+ - lib/pwn/migrate.rb
2043
2044
  - lib/pwn/plugins.rb
2044
2045
  - lib/pwn/plugins/android.rb
2045
2046
  - lib/pwn/plugins/assembly.rb
@@ -2489,6 +2490,7 @@ files:
2489
2490
  - spec/lib/pwn/ffi_spec.rb
2490
2491
  - spec/lib/pwn/memory_index_spec.rb
2491
2492
  - spec/lib/pwn/memory_spec.rb
2493
+ - spec/lib/pwn/migrate_spec.rb
2492
2494
  - spec/lib/pwn/plugins/android_spec.rb
2493
2495
  - spec/lib/pwn/plugins/assembly_spec.rb
2494
2496
  - spec/lib/pwn/plugins/authentication_helper_spec.rb