pwn 0.5.784 → 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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/learning.rb +14 -18
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +4 -0
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/setup.rb +114 -24
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +8 -16
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/migrate_spec.rb +29 -81
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
data/lib/pwn/migrate.rb
CHANGED
|
@@ -52,7 +52,7 @@ module PWN
|
|
|
52
52
|
# Bump this whenever the shape of any file under ~/.pwn changes in a
|
|
53
53
|
# way that requires a one-time transform. Add the transform as an
|
|
54
54
|
# entry in MIGRATIONS keyed by the NEW schema number.
|
|
55
|
-
SCHEMA_VERSION =
|
|
55
|
+
SCHEMA_VERSION = 12
|
|
56
56
|
|
|
57
57
|
OK = "\e[32mok\e[0m"
|
|
58
58
|
BAD = "\e[31mFAIL\e[0m"
|
|
@@ -84,7 +84,7 @@ module PWN
|
|
|
84
84
|
|
|
85
85
|
enc = PWN::Plugins::Vault.file_encrypted?(file: p)
|
|
86
86
|
# Shape (missing keys vs env_template) is checked separately in
|
|
87
|
-
# .vault_drift because it requires the
|
|
87
|
+
# .vault_drift because it requires access to the legacy key or YubiKey.
|
|
88
88
|
{ ok: enc, why: enc ? nil : 'not encrypted (run PWN::Plugins::Vault.create)' }
|
|
89
89
|
}
|
|
90
90
|
},
|
|
@@ -245,37 +245,41 @@ module PWN
|
|
|
245
245
|
File.write(path, YAML.dump(raw))
|
|
246
246
|
io.puts " · agents.yml added default model to #{patched} personas"
|
|
247
247
|
},
|
|
248
|
-
4 => lambda { |_root, io|
|
|
248
|
+
4 => lambda { |_root, io, vault_opts = {}|
|
|
249
249
|
io.puts ' · pwn.yaml ai_sandbox, ai_router, model_routes task classes'
|
|
250
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
250
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
251
251
|
},
|
|
252
|
-
5 => lambda { |_root, io|
|
|
252
|
+
5 => lambda { |_root, io, vault_opts = {}|
|
|
253
253
|
io.puts ' · pwn.yaml ai.agent.skill_review'
|
|
254
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
254
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
255
255
|
},
|
|
256
|
-
6 => lambda { |_root, io|
|
|
256
|
+
6 => lambda { |_root, io, vault_opts = {}|
|
|
257
257
|
io.puts ' · pwn.yaml ai.tui.theme'
|
|
258
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
258
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
259
259
|
},
|
|
260
|
-
7 => lambda { |_root, io|
|
|
260
|
+
7 => lambda { |_root, io, vault_opts = {}|
|
|
261
261
|
io.puts ' · pwn.yaml ai.tui.theme.title'
|
|
262
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
262
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
263
263
|
},
|
|
264
264
|
8 => lambda { |_root, io|
|
|
265
265
|
io.puts ' · guarded session_cleanup default cron payload and job'
|
|
266
266
|
PWN::Cron.install_defaults
|
|
267
267
|
},
|
|
268
|
-
9 => lambda { |_root, io|
|
|
268
|
+
9 => lambda { |_root, io, vault_opts = {}|
|
|
269
269
|
io.puts ' · pwn.yaml plugins.meshtastic.channel short_name'
|
|
270
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
270
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
271
271
|
},
|
|
272
|
-
10 => lambda { |_root, io|
|
|
272
|
+
10 => lambda { |_root, io, vault_opts = {}|
|
|
273
273
|
io.puts ' · pwn.yaml ai reasoning_effort for anthropic, gemini, ollama, openwebui'
|
|
274
|
-
PWN::Migrate.backfill_vault(io: io)
|
|
274
|
+
PWN::Migrate.backfill_vault(vault_opts.merge(io: io))
|
|
275
275
|
},
|
|
276
276
|
11 => lambda { |_root, io|
|
|
277
277
|
io.puts ' · versioned housekeeping policy and managed session_cleanup helper'
|
|
278
278
|
PWN::Cron.install_defaults
|
|
279
|
+
},
|
|
280
|
+
12 => lambda { |root, io|
|
|
281
|
+
io.puts ' · global seven-folder recovery policy; legacy relocation remains explicit'
|
|
282
|
+
PWN::Backups.configure(root: File.join(root, 'backup'))
|
|
279
283
|
}
|
|
280
284
|
}.freeze
|
|
281
285
|
|
|
@@ -309,14 +313,15 @@ module PWN
|
|
|
309
313
|
end
|
|
310
314
|
|
|
311
315
|
# Supported Method Parameters::
|
|
312
|
-
# rows = PWN::Migrate.status
|
|
316
|
+
# rows = PWN::Migrate.status(pwn_env_path: 'optional - Selected configuration vault path')
|
|
313
317
|
#
|
|
314
318
|
# Per-file compatibility report — the machine-readable half of
|
|
315
319
|
# `.check`. Never writes. Never raises (each verifier is rescued).
|
|
316
320
|
|
|
317
|
-
public_class_method def self.status
|
|
321
|
+
public_class_method def self.status(opts = {})
|
|
322
|
+
selected_vault = opts[:pwn_env_path] || vault_paths[:file]
|
|
318
323
|
files = STATE_FILES.map do |rel, meta|
|
|
319
|
-
path = File.join(PWN_ROOT, rel)
|
|
324
|
+
path = meta[:kind] == :vault ? selected_vault : File.join(PWN_ROOT, rel)
|
|
320
325
|
exists = meta[:kind] == :dir ? File.directory?(path) : File.file?(path)
|
|
321
326
|
raw = exists && meta[:kind] != :dir ? safe_read(path: path) : nil
|
|
322
327
|
v = verify_one(path: path, raw: raw, meta: meta)
|
|
@@ -385,18 +390,21 @@ module PWN
|
|
|
385
390
|
# and decryptor available) 5. stamp .schema.
|
|
386
391
|
|
|
387
392
|
public_class_method def self.run(opts = {})
|
|
393
|
+
return PWN::Backups.transaction(root: BACKUP_ROOT) { run(opts) } unless opts[:dry_run] || PWN::Backups.active?
|
|
394
|
+
|
|
388
395
|
fix = opts[:fix] ? true : false
|
|
389
396
|
dry_run = opts[:dry_run] ? true : false
|
|
390
397
|
backup = opts.fetch(:backup, true)
|
|
391
398
|
io = opts[:io] || $stdout
|
|
392
399
|
|
|
393
400
|
FileUtils.mkdir_p(PWN_ROOT) unless dry_run
|
|
394
|
-
before = status
|
|
401
|
+
before = status(pwn_env_path: opts[:pwn_env_path])
|
|
395
402
|
io.puts "PWN::Migrate — pwn #{before[:pwn_version]} · ~/.pwn schema " \
|
|
396
403
|
"#{before[:schema_installed]} → #{before[:schema_current]}" \
|
|
397
404
|
"#{' (dry-run)' if dry_run}"
|
|
398
405
|
|
|
399
|
-
|
|
406
|
+
vault_opts = { key: opts[:key], iv: opts[:iv], yubikey: opts[:yubikey], pwn_env_path: opts[:pwn_env_path], pwn_dec_path: opts[:pwn_dec_path] }
|
|
407
|
+
drift = fix ? vault_drift(vault_opts) : nil
|
|
400
408
|
changes = before[:schema_installed] < SCHEMA_VERSION ||
|
|
401
409
|
(fix && (!before[:incompatible].empty? || (drift && !drift[:missing].empty?)))
|
|
402
410
|
bpath = nil
|
|
@@ -412,7 +420,9 @@ module PWN
|
|
|
412
420
|
next unless m
|
|
413
421
|
|
|
414
422
|
io.puts " migrate : v#{n - 1} → v#{n}"
|
|
415
|
-
|
|
423
|
+
unless dry_run
|
|
424
|
+
m.arity == 2 ? m.call(PWN_ROOT, io) : m.call(PWN_ROOT, io, vault_opts)
|
|
425
|
+
end
|
|
416
426
|
applied << n
|
|
417
427
|
end
|
|
418
428
|
|
|
@@ -428,13 +438,13 @@ module PWN
|
|
|
428
438
|
|
|
429
439
|
# pwn.yaml key backfill runs even when the vault verifier passed —
|
|
430
440
|
# "encrypted" ≠ "has every key this release added".
|
|
431
|
-
vd = backfill_vault(
|
|
441
|
+
vd = backfill_vault(vault_opts.merge(dry_run: dry_run, io: io))
|
|
432
442
|
fixed << vd if vd && vd[:added].to_i.positive?
|
|
433
443
|
end
|
|
434
444
|
|
|
435
445
|
stamp! if !dry_run && changes
|
|
436
446
|
|
|
437
|
-
after = status
|
|
447
|
+
after = status(pwn_env_path: opts[:pwn_env_path])
|
|
438
448
|
io.puts
|
|
439
449
|
io.puts " result : #{after[:incompatible].length} incompatible remaining " \
|
|
440
450
|
"(was #{before[:incompatible].length}) · schema now #{after[:schema_installed]}"
|
|
@@ -452,16 +462,15 @@ module PWN
|
|
|
452
462
|
#
|
|
453
463
|
# Deep-diff the user's decrypted pwn.yaml against the current
|
|
454
464
|
# PWN::Config.env_template. → { missing: [dotted.paths], user: {…} }
|
|
455
|
-
# Returns nil when
|
|
465
|
+
# Returns nil when vault access is unavailable (never prompts).
|
|
456
466
|
|
|
457
467
|
public_class_method def self.vault_drift(opts = {})
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
yaml = File.join(PWN_ROOT, 'pwn.yaml')
|
|
468
|
+
paths = vault_paths(opts)
|
|
469
|
+
yaml = paths[:file]
|
|
462
470
|
return nil unless File.file?(yaml) && PWN::Plugins::Vault.file_encrypted?(file: yaml)
|
|
463
471
|
|
|
464
|
-
|
|
472
|
+
access = PWN::Plugins::Vault.access_options(paths.merge(key: opts[:key], iv: opts[:iv], yubikey: opts[:yubikey]))
|
|
473
|
+
user = PWN::Plugins::Vault.dump(access.merge(file: yaml))
|
|
465
474
|
tmpl = env_template
|
|
466
475
|
{ missing: missing_paths(tmpl: tmpl, user: user), user: user, template: tmpl }
|
|
467
476
|
rescue StandardError
|
|
@@ -474,7 +483,7 @@ module PWN
|
|
|
474
483
|
# )
|
|
475
484
|
#
|
|
476
485
|
# Decrypt → deep-merge template UNDER user (never overwrites) →
|
|
477
|
-
# re-encrypt with the SAME key/iv
|
|
486
|
+
# re-encrypt with the SAME legacy key/iv or hardware selector. This fixes the
|
|
478
487
|
# upgrade break: `NoMethodError: undefined method '[]' for nil` when
|
|
479
488
|
# a release added `env[:ai][:new_engine][:…]` and the user's vault
|
|
480
489
|
# predates it.
|
|
@@ -482,9 +491,9 @@ module PWN
|
|
|
482
491
|
public_class_method def self.backfill_vault(opts = {})
|
|
483
492
|
io = opts[:io] || $stdout
|
|
484
493
|
dry_run = opts[:dry_run] ? true : false
|
|
485
|
-
d = vault_drift(
|
|
494
|
+
d = vault_drift(opts)
|
|
486
495
|
unless d
|
|
487
|
-
io.puts ' vault : skipped (
|
|
496
|
+
io.puts ' vault : skipped (vault access unavailable — check YubiKey or legacy decryptor/key/IV)'
|
|
488
497
|
return nil
|
|
489
498
|
end
|
|
490
499
|
if d[:missing].empty?
|
|
@@ -497,13 +506,10 @@ module PWN
|
|
|
497
506
|
io.puts " … (+#{d[:missing].length - 8} more)" if d[:missing].length > 8
|
|
498
507
|
return { rel: 'pwn.yaml', added: d[:missing].length, missing: d[:missing], dry_run: true } if dry_run
|
|
499
508
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
File.write(yaml, YAML.dump(merged).gsub(/^(\s*):/, '\1'))
|
|
505
|
-
File.chmod(0o600, yaml)
|
|
506
|
-
PWN::Plugins::Vault.encrypt(file: yaml, key: key, iv: iv)
|
|
509
|
+
access = vault_paths(opts).merge(key: opts[:key], iv: opts[:iv], yubikey: opts[:yubikey])
|
|
510
|
+
PWN::Plugins::Vault.update(access) do |user|
|
|
511
|
+
user.replace(deep_defaults(user: user, tmpl: d[:template]))
|
|
512
|
+
end
|
|
507
513
|
|
|
508
514
|
{ rel: 'pwn.yaml', added: d[:missing].length, missing: d[:missing] }
|
|
509
515
|
rescue StandardError => e
|
|
@@ -623,213 +629,18 @@ module PWN
|
|
|
623
629
|
end
|
|
624
630
|
|
|
625
631
|
# Supported Method Parameters::
|
|
626
|
-
# PWN::Migrate.prune_backups(dry_run: true
|
|
627
|
-
#
|
|
628
|
-
# Explicit expire_history policy bounds recovery generations instead.
|
|
632
|
+
# PWN::Migrate.prune_backups(dry_run: true)
|
|
633
|
+
# Retention now runs only at shared producer admission, never separately.
|
|
629
634
|
public_class_method def self.prune_backups(opts = {})
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
keep = [Integer(opts.fetch(:keep, 3)), 3].max
|
|
634
|
-
limit = Integer(opts.fetch(:limit, 10))
|
|
635
|
-
admission = opts[:admission]
|
|
636
|
-
result = { eligible: [], deleted: [], retained: [], incomplete: false }
|
|
637
|
-
return result unless File.directory?(BACKUP_ROOT)
|
|
638
|
-
raise IOError, 'Unsafe backup root' unless File.realpath(BACKUP_ROOT) == BACKUP_ROOT && File.stat(BACKUP_ROOT).uid == Process.uid
|
|
639
|
-
|
|
640
|
-
work = lambda do
|
|
641
|
-
names = Dir.children(BACKUP_ROOT).grep(/\A\d{8}_\d{6}(?:_[a-f0-9]{8})?\z/).sort
|
|
642
|
-
retained = names.last(keep)
|
|
643
|
-
manifests = retained.to_h { |name| [name, backup_inventory(path: File.join(BACKUP_ROOT, name), admission: admission)] }
|
|
644
|
-
names[0...-keep].to_a.last(limit).reverse_each do |name|
|
|
645
|
-
path = File.join(BACKUP_ROOT, name)
|
|
646
|
-
inventory = backup_inventory(path: path, admission: admission)
|
|
647
|
-
match = manifests.find { |_other, rows| rows == inventory }
|
|
648
|
-
unless match
|
|
649
|
-
result[:retained] << name
|
|
650
|
-
next
|
|
651
|
-
end
|
|
652
|
-
result[:eligible] << name
|
|
653
|
-
next if dry_run
|
|
654
|
-
|
|
655
|
-
canonical = File.join(BACKUP_ROOT, match.first)
|
|
656
|
-
raise IOError, 'Backup changed before pruning' unless backup_inventory(path: canonical, admission: admission) == inventory && backup_inventory(path: path, admission: admission) == inventory
|
|
657
|
-
|
|
658
|
-
FileUtils.remove_entry_secure(path)
|
|
659
|
-
raise IOError, 'Backup pruning failed' if File.exist?(path)
|
|
660
|
-
|
|
661
|
-
result[:deleted] << name
|
|
662
|
-
end
|
|
663
|
-
result
|
|
664
|
-
end
|
|
665
|
-
return work.call if dry_run
|
|
666
|
-
|
|
667
|
-
File.open(File.join(BACKUP_ROOT, '.retention.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
668
|
-
raise IOError, 'Backup maintenance active' unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
669
|
-
|
|
670
|
-
work.call
|
|
671
|
-
end
|
|
672
|
-
rescue IOError => e
|
|
673
|
-
result[:incomplete] = true
|
|
674
|
-
result[:error] = e.message
|
|
675
|
-
result
|
|
676
|
-
end
|
|
677
|
-
|
|
678
|
-
# Explicit opt-in expires historical versions, not merely duplicate bytes.
|
|
679
|
-
private_class_method def self.backup_retention_policy
|
|
680
|
-
path = File.join(BACKUP_ROOT, '.retention-policy.json')
|
|
681
|
-
return { 'keep' => 3, 'pins' => [], 'expire_history' => false } unless File.exist?(path) || File.symlink?(path)
|
|
682
|
-
raise IOError, 'Unsafe retention policy' unless File.realpath(path) == path && File.stat(path).uid == Process.uid
|
|
683
|
-
|
|
684
|
-
value = JSON.parse(File.binread(path))
|
|
685
|
-
valid = value['version'] == 1 && [true, false].include?(value['expire_history']) && value['keep'].is_a?(Integer) && value['keep'] >= 3 && value['pins'].is_a?(Array)
|
|
686
|
-
valid &&= value['pins'].all? { |name| name.is_a?(String) && name.match?(/\A\d{8}_\d{6}(?:_[a-f0-9]{8})?\z/) }
|
|
687
|
-
raise IOError, 'Invalid retention policy' unless valid
|
|
688
|
-
|
|
689
|
-
value
|
|
635
|
+
{ eligible: [], deleted: [], retained: [], incomplete: false,
|
|
636
|
+
dry_run: opts.fetch(:dry_run, true), keep: PWN::Backups::LIMIT,
|
|
637
|
+
scope: 'global', retention: 'shared_producer_admission' }
|
|
690
638
|
end
|
|
691
639
|
|
|
692
640
|
private_class_method def self.backup_input_names
|
|
693
641
|
(STATE_FILES.keys.map { |rel| rel.delete_suffix('/') } + %w[.schema pwn.yaml.decryptor]).uniq.reject { |rel| %w[sessions swarm].include?(rel.split('/').first) }
|
|
694
642
|
end
|
|
695
643
|
|
|
696
|
-
# Conservative literal-path protection in current evidence/continuation stores.
|
|
697
|
-
# Historical log/history/metrics copies are not operational dependencies.
|
|
698
|
-
private_class_method def self.backup_reference_pins(opts = {})
|
|
699
|
-
admission = opts[:admission]
|
|
700
|
-
pins = []
|
|
701
|
-
roots = %w[learning learning.jsonl memory.json mistakes.json policy.json policy_traj.jsonl preferences.jsonl engagements artifacts sessions swarm jobs runs open_goals open_goal.json curriculum finetune extrospection skill_review.jsonl maintenance]
|
|
702
|
-
roots.each do |name|
|
|
703
|
-
path = File.join(PWN_ROOT, name)
|
|
704
|
-
next unless File.exist?(path) || File.symlink?(path)
|
|
705
|
-
|
|
706
|
-
Find.find(path) do |file|
|
|
707
|
-
stat = File.lstat(file)
|
|
708
|
-
raise IOError, 'Unsafe current evidence entry' unless stat.file? || stat.directory?
|
|
709
|
-
next unless stat.file? && file.match?(/\.jsonl?\z/)
|
|
710
|
-
raise IOError, 'Backup work budget exhausted' if admission && !admission.call(stat.size + 1)
|
|
711
|
-
|
|
712
|
-
documents = file.end_with?('.jsonl') ? File.foreach(file).reject { |line| line.strip.empty? }.map { |line| JSON.parse(line) } : [JSON.parse(File.binread(file))]
|
|
713
|
-
documents.select! { |row| row.is_a?(Hash) && (%w[prepared interrupted].include?(row['state']) || row['pinned'] == true) } if name == 'maintenance'
|
|
714
|
-
until documents.empty?
|
|
715
|
-
value = documents.pop
|
|
716
|
-
case value
|
|
717
|
-
when Hash then documents.concat(value.values)
|
|
718
|
-
when Array then documents.concat(value)
|
|
719
|
-
when String
|
|
720
|
-
match = value.match(%r{\A(?:#{Regexp.escape(BACKUP_ROOT)}/|backup/)(\d{8}_\d{6}(?:_[a-f0-9]{8})?)(?:/|\z)})
|
|
721
|
-
pins << match[1] if match
|
|
722
|
-
end
|
|
723
|
-
end
|
|
724
|
-
end
|
|
725
|
-
end
|
|
726
|
-
pins.uniq
|
|
727
|
-
end
|
|
728
|
-
|
|
729
|
-
private_class_method def self.expire_backup_generations(opts = {})
|
|
730
|
-
require 'zlib'
|
|
731
|
-
result = { eligible: [], deleted: [], retained: [], incomplete: false, permanent_history_expiry: true }
|
|
732
|
-
return result unless File.directory?(BACKUP_ROOT)
|
|
733
|
-
raise IOError, 'Unsafe backup root' unless File.realpath(PWN_ROOT) == PWN_ROOT && File.realpath(BACKUP_ROOT) == BACKUP_ROOT && File.stat(BACKUP_ROOT).uid == Process.uid
|
|
734
|
-
|
|
735
|
-
dry_run = opts.fetch(:dry_run, true)
|
|
736
|
-
admission = opts[:admission]
|
|
737
|
-
policy = backup_retention_policy
|
|
738
|
-
keep = [Integer(opts.fetch(:keep, policy['keep'])), 3].max
|
|
739
|
-
limit = Integer(opts.fetch(:limit, 10))
|
|
740
|
-
raise ArgumentError, 'Invalid retention limit' unless limit.positive?
|
|
741
|
-
|
|
742
|
-
operation = lambda do
|
|
743
|
-
names = Dir.children(BACKUP_ROOT).grep(/\A\d{8}_\d{6}(?:_[a-f0-9]{8})?\z/).sort
|
|
744
|
-
raise IOError, 'Pinned backup missing' unless (policy['pins'] - names).empty?
|
|
745
|
-
|
|
746
|
-
if (names - (names.last(keep) + policy['pins'])).empty?
|
|
747
|
-
result[:retained] = names
|
|
748
|
-
result[:pins] = policy['pins']
|
|
749
|
-
result[:evidence_scan] = 'not_needed_no_expiry'
|
|
750
|
-
return result
|
|
751
|
-
end
|
|
752
|
-
|
|
753
|
-
pins = (policy['pins'] + backup_reference_pins(admission: admission)).uniq
|
|
754
|
-
raise IOError, 'Pinned backup missing' unless (pins - names).empty?
|
|
755
|
-
|
|
756
|
-
retained = (names.last(keep) + pins).uniq.sort
|
|
757
|
-
result[:retained] = retained
|
|
758
|
-
result[:pins] = pins
|
|
759
|
-
return result if (names - retained).empty?
|
|
760
|
-
raise IOError, 'Insufficient recovery generations' if retained.length < keep
|
|
761
|
-
|
|
762
|
-
current = backup_inventory(path: PWN_ROOT, names: backup_input_names, admission: admission)
|
|
763
|
-
inventories = retained.to_h { |name| [name, backup_inventory(path: File.join(BACKUP_ROOT, name), admission: admission)] }
|
|
764
|
-
newest = inventories.fetch(names.last)
|
|
765
|
-
raise IOError, 'Current migration input coverage missing' unless current.all? { |path, row| newest[path] && newest[path].first == row.first }
|
|
766
|
-
|
|
767
|
-
%w[pwn.yaml pwn.yaml.decryptor].each do |name|
|
|
768
|
-
raise IOError, 'Current encrypted configuration recovery missing' if current.key?(name) && newest[name] != current[name]
|
|
769
|
-
end
|
|
770
|
-
result[:coverage] = { current_paths: current.length, newest: names.last, legacy_completeness_certified: false, restore: 'copy migration-owned paths only; never overlay entire legacy trees or edit shared inodes' }
|
|
771
|
-
candidates = (names - retained).first(limit)
|
|
772
|
-
expired = candidates.to_h { |name| [name, backup_inventory(path: File.join(BACKUP_ROOT, name), admission: admission)] }
|
|
773
|
-
result[:eligible] = candidates
|
|
774
|
-
return result if dry_run
|
|
775
|
-
|
|
776
|
-
# Recheck everything before publishing the single private metadata ledger.
|
|
777
|
-
raise IOError, 'Current migration input changed' unless current == backup_inventory(path: PWN_ROOT, names: backup_input_names, admission: admission)
|
|
778
|
-
raise IOError, 'Backup references changed' unless pins == (policy['pins'] + backup_reference_pins(admission: admission)).uniq
|
|
779
|
-
|
|
780
|
-
inventories.merge(expired).each do |name, rows|
|
|
781
|
-
raise IOError, 'Backup changed before expiry' unless rows == backup_inventory(path: File.join(BACKUP_ROOT, name), admission: admission)
|
|
782
|
-
end
|
|
783
|
-
ledger = File.join(BACKUP_ROOT, '.retention-manifest.json.gz')
|
|
784
|
-
raise IOError, 'Unsafe retention manifest' if File.symlink?(ledger)
|
|
785
|
-
|
|
786
|
-
record = { version: 1, state: 'prepared', policy: policy, coverage: result[:coverage], retained: inventories, expiring: expired, deleted: [] }
|
|
787
|
-
verification_bytes = inventories.values.sum { |rows| rows.values.sum { |row| row.first == 'file' ? row[4] + 1 : 0 } }
|
|
788
|
-
reserved_bytes = (JSON.generate(record).bytesize * 2) + verification_bytes
|
|
789
|
-
raise IOError, 'Backup commit budget exhausted' if admission && !admission.call(reserved_bytes)
|
|
790
|
-
|
|
791
|
-
publish_backup_retention(path: ledger, record: record)
|
|
792
|
-
candidates.each do |name|
|
|
793
|
-
FileUtils.remove_entry_secure(File.join(BACKUP_ROOT, name))
|
|
794
|
-
raise IOError, 'Backup expiry failed' if File.exist?(File.join(BACKUP_ROOT, name))
|
|
795
|
-
|
|
796
|
-
result[:deleted] << name
|
|
797
|
-
record[:deleted] << name
|
|
798
|
-
end
|
|
799
|
-
inventories.each do |name, rows|
|
|
800
|
-
raise IOError, 'Retained backup changed' unless rows == backup_inventory(path: File.join(BACKUP_ROOT, name))
|
|
801
|
-
end
|
|
802
|
-
record[:state] = 'committed'
|
|
803
|
-
publish_backup_retention(path: ledger, record: record)
|
|
804
|
-
result
|
|
805
|
-
end
|
|
806
|
-
return operation.call if dry_run
|
|
807
|
-
|
|
808
|
-
File.open(File.join(BACKUP_ROOT, '.retention.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
809
|
-
raise IOError, 'Backup maintenance active' unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
810
|
-
|
|
811
|
-
operation.call
|
|
812
|
-
end
|
|
813
|
-
rescue IOError => e
|
|
814
|
-
result[:incomplete] = true
|
|
815
|
-
result[:error] = e.message
|
|
816
|
-
result
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
private_class_method def self.publish_backup_retention(opts = {})
|
|
820
|
-
path = opts[:path]
|
|
821
|
-
temporary = "#{path}.#{SecureRandom.hex(8)}"
|
|
822
|
-
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
823
|
-
writer = Zlib::GzipWriter.new(file)
|
|
824
|
-
writer.write(JSON.generate(opts[:record]))
|
|
825
|
-
writer.finish
|
|
826
|
-
file.flush
|
|
827
|
-
file.fsync
|
|
828
|
-
end
|
|
829
|
-
File.rename(temporary, path)
|
|
830
|
-
File.open(BACKUP_ROOT, File::RDONLY, &:fsync)
|
|
831
|
-
end
|
|
832
|
-
|
|
833
644
|
private_class_method def self.backup_inventory(opts = {})
|
|
834
645
|
path = opts[:path]
|
|
835
646
|
admission = opts[:admission]
|
|
@@ -859,38 +670,30 @@ module PWN
|
|
|
859
670
|
end
|
|
860
671
|
|
|
861
672
|
private_class_method def self.backup!
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
File.open(File.join(BACKUP_ROOT, '.retention.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
866
|
-
lock.flock(File::LOCK_EX)
|
|
867
|
-
ts = "#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(4)}"
|
|
868
|
-
dst = File.join(BACKUP_ROOT, ts)
|
|
869
|
-
partial = File.join(BACKUP_ROOT, ".partial-#{ts}")
|
|
870
|
-
Dir.mkdir(partial, 0o700)
|
|
871
|
-
# Only state owned by the migration registry. Logs, caches, artifacts,
|
|
872
|
-
# jobs and prior maintenance archives are not schema migration inputs.
|
|
673
|
+
PWN::Backups.transaction(root: BACKUP_ROOT) do
|
|
674
|
+
dst = PWN::Backups.directory(label: 'migration')
|
|
873
675
|
names = backup_input_names
|
|
874
676
|
inventory = backup_inventory(path: PWN_ROOT, names: names)
|
|
875
677
|
names.each do |name|
|
|
876
678
|
src = File.join(PWN_ROOT, name)
|
|
877
679
|
next unless File.exist?(src)
|
|
878
680
|
|
|
879
|
-
target = File.join(
|
|
681
|
+
target = File.join(dst, name)
|
|
880
682
|
FileUtils.mkdir_p(File.dirname(target), mode: 0o700)
|
|
881
683
|
FileUtils.cp_r(src, target, preserve: true)
|
|
882
684
|
end
|
|
883
685
|
raise IOError, 'Migration input changed during backup' unless inventory == backup_inventory(path: PWN_ROOT, names: names)
|
|
884
|
-
raise IOError, 'Backup verification failed' unless inventory == backup_inventory(path:
|
|
686
|
+
raise IOError, 'Backup verification failed' unless inventory == backup_inventory(path: dst, names: names)
|
|
885
687
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
688
|
+
inventory.each do |name, row|
|
|
689
|
+
File.open(File.join(dst, name), File::RDONLY, &:fsync) if row.first == 'file'
|
|
690
|
+
end
|
|
691
|
+
File.open(File.join(dst, '.snapshot-manifest.json'), File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
692
|
+
file.write(JSON.generate(version: 1, complete: true, scope: names.sort, files: inventory))
|
|
889
693
|
file.flush
|
|
890
694
|
file.fsync
|
|
891
695
|
end
|
|
892
|
-
File.
|
|
893
|
-
File.open(BACKUP_ROOT, File::RDONLY, &:fsync)
|
|
696
|
+
File.open(dst, File::RDONLY, &:fsync)
|
|
894
697
|
dst
|
|
895
698
|
end
|
|
896
699
|
end
|
|
@@ -908,18 +711,12 @@ module PWN
|
|
|
908
711
|
SCHEMA_VERSION
|
|
909
712
|
end
|
|
910
713
|
|
|
911
|
-
private_class_method def self.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
return [nil, nil] unless File.file?(dec)
|
|
918
|
-
|
|
919
|
-
d = YAML.load_file(dec, symbolize_names: true)
|
|
920
|
-
[d[:key], d[:iv]]
|
|
921
|
-
rescue StandardError
|
|
922
|
-
[nil, nil]
|
|
714
|
+
private_class_method def self.vault_paths(opts = {})
|
|
715
|
+
driver = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env[:driver_opts] : nil
|
|
716
|
+
driver = {} unless driver.is_a?(Hash)
|
|
717
|
+
file = opts[:pwn_env_path] || driver[:pwn_env_path] || File.join(PWN_ROOT, 'pwn.yaml')
|
|
718
|
+
decryptor_file = opts[:pwn_dec_path] || driver[:pwn_dec_path] || "#{file}.decryptor"
|
|
719
|
+
{ file: file, decryptor_file: decryptor_file }
|
|
923
720
|
end
|
|
924
721
|
|
|
925
722
|
private_class_method def self.safe_read(opts = {})
|
|
@@ -1011,7 +808,9 @@ module PWN
|
|
|
1011
808
|
#{self}.needed?
|
|
1012
809
|
|
|
1013
810
|
# Run status and return its result
|
|
1014
|
-
#{self}.status
|
|
811
|
+
#{self}.status(
|
|
812
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml'
|
|
813
|
+
)
|
|
1015
814
|
|
|
1016
815
|
# Human-readable ~/.pwn state doctor. Read-only. Called from
|
|
1017
816
|
#{self}.check(
|
|
@@ -1025,29 +824,35 @@ module PWN
|
|
|
1025
824
|
dry_run: 'optional - report what WOULD happen, write nothing (default false)',
|
|
1026
825
|
key: 'optional - vault key (default: read ~/.pwn/pwn.yaml.decryptor)',
|
|
1027
826
|
iv: 'optional - vault iv (default: read ~/.pwn/pwn.yaml.decryptor)',
|
|
827
|
+
yubikey: 'optional - Hardware selector overrides: slot, serial, reader, device_path, connection, timeout',
|
|
828
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml',
|
|
829
|
+
pwn_dec_path: 'optional - Legacy decryptor path; defaults to driver options then vault sidecar',
|
|
1028
830
|
io: 'optional - IO to write to (default $stdout)'
|
|
1029
831
|
)
|
|
1030
832
|
|
|
1031
833
|
# Run vault drift and return its result
|
|
1032
834
|
#{self}.vault_drift(
|
|
1033
|
-
key: 'optional - vault key
|
|
835
|
+
key: 'optional - Legacy vault key override',
|
|
836
|
+
iv: 'optional - Legacy vault IV override',
|
|
837
|
+
yubikey: 'optional - Hardware selector overrides: slot, serial, reader, device_path, connection, timeout',
|
|
838
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml',
|
|
839
|
+
pwn_dec_path: 'optional - Legacy decryptor path; hardware vaults need no sidecar'
|
|
1034
840
|
)
|
|
1035
841
|
|
|
1036
842
|
# Run backfill vault and return its result
|
|
1037
843
|
#{self}.backfill_vault(
|
|
1038
|
-
key: 'optional -
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
844
|
+
key: 'optional - Legacy vault key override',
|
|
845
|
+
iv: 'optional - Legacy vault IV override',
|
|
846
|
+
yubikey: 'optional - Hardware selector overrides: slot, serial, reader, device_path, connection, timeout',
|
|
847
|
+
pwn_env_path: 'optional - Vault path; defaults to driver options then ~/.pwn/pwn.yaml',
|
|
848
|
+
pwn_dec_path: 'optional - Legacy decryptor path; hardware vaults need no sidecar',
|
|
849
|
+
io: 'optional - Report output; defaults to $stdout',
|
|
850
|
+
dry_run: 'optional - Report missing keys without writing; defaults to false'
|
|
1042
851
|
)
|
|
1043
852
|
|
|
1044
|
-
#
|
|
853
|
+
# Compatibility report; shared producer admission owns retention.
|
|
1045
854
|
#{self}.prune_backups(
|
|
1046
|
-
|
|
1047
|
-
dry_run: 'optional - preview without writes, default true',
|
|
1048
|
-
keep: 'optional - recent recovery points to retain, minimum three',
|
|
1049
|
-
limit: 'optional - maximum older snapshots examined per invocation, default ten',
|
|
1050
|
-
admission: 'optional - callable byte/work budget admission guard'
|
|
855
|
+
dry_run: 'optional - preview without writes, default true'
|
|
1051
856
|
)
|
|
1052
857
|
|
|
1053
858
|
# Run env template and return its result
|
data/lib/pwn/module_skills.rb
CHANGED
|
@@ -273,6 +273,13 @@ module PWN
|
|
|
273
273
|
pending = []
|
|
274
274
|
next
|
|
275
275
|
end
|
|
276
|
+
if line =~ /^\s*class\s+([A-Z][\w:]*)/
|
|
277
|
+
ident = Regexp.last_match(1)
|
|
278
|
+
if snake(name: ident.split('::').last) == File.basename(src, '.rb')
|
|
279
|
+
const = ident.include?('::') ? ident : (stack + [ident]).join('::')
|
|
280
|
+
blurb = pending.join(' ').strip if blurb.empty?
|
|
281
|
+
end
|
|
282
|
+
end
|
|
276
283
|
pending = []
|
|
277
284
|
next if line =~ /^\s*private_class_method/
|
|
278
285
|
|