pwn 0.5.632 → 0.5.634
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/.github/workflows/install-matrix.yml +17 -7
- data/.rubocop.yml +1 -0
- data/Gemfile +2 -2
- data/Rakefile +1 -1
- data/bin/pwn_fuzz_net_app_proto +1 -7
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Installation.md +1 -1
- data/git_commit.sh +1 -1
- data/lib/pwn/ai/agent/learning.rb +13 -16
- data/lib/pwn/ai/agent/prompt_builder.rb +7 -3
- data/lib/pwn/ai/agent/tools/skills.rb +122 -63
- data/lib/pwn/config.rb +245 -30
- data/lib/pwn/ffi/stdio.rb +3 -1
- data/lib/pwn/plugins/fuzz.rb +9 -7
- data/lib/pwn/plugins/github.rb +205 -47
- data/lib/pwn/setup.rb +16 -5
- data/lib/pwn/version.rb +1 -1
- data/packer/provisioners/pwn.sh +2 -1
- data/pwn.gemspec +35 -3
- data/spec/integration/bin_drivers_spec.rb +73 -0
- data/spec/integration/config_spec.rb +68 -0
- data/spec/integration/dispatch_spec.rb +106 -0
- data/spec/integration/documentation_sync_spec.rb +74 -0
- data/spec/integration/extrospection_classify_spec.rb +90 -0
- data/spec/integration/help_contract_spec.rb +104 -0
- data/spec/integration/persistence_roundtrip_spec.rb +112 -0
- data/spec/integration/prompt_builder_spec.rb +68 -0
- data/spec/integration/reinforced_feedback_loop_spec.rb +121 -0
- data/spec/integration/reports_spec.rb +76 -0
- data/spec/integration/result_condition_spec.rb +77 -0
- data/spec/integration/sast_functional_spec.rb +80 -0
- data/spec/integration/tool_registry_spec.rb +91 -0
- data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +88 -1
- data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +5 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/support/sandbox.rb +77 -0
- data/third_party/pwn_rdoc.jsonl +16 -5
- metadata +31 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8632bee6e87072a40fbd993834156ad06e1c448918b2922125d592debd7c033d
|
|
4
|
+
data.tar.gz: 399687f4f6ea85666f9e7d8e4c0450998be2949218b45d68b061e8c20c452010
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 964a576117407e3e5907a302106c2a0f657bedc9a4ebd59167ca360f7f89dbcb3975aec4b6c036e3cc2f138461044f162371f8b2e62d05e5f0ca1c498fa852b4
|
|
7
|
+
data.tar.gz: 8cae2ebdeb246fc190c49aed8b5a787372a482e2eecc3027116eab1bca985b33d9451cd22df4f158f1d4e0e8dc4314f6b1754f4e3b42f68dd8f7c1808b7ea3e6
|
|
@@ -57,8 +57,8 @@ jobs:
|
|
|
57
57
|
# Base axes → 5 images × 2 profiles = 10 legs.
|
|
58
58
|
image:
|
|
59
59
|
- kalilinux/kali-rolling
|
|
60
|
-
- debian:
|
|
61
|
-
- ubuntu:
|
|
60
|
+
- debian:trixie
|
|
61
|
+
- ubuntu:latest
|
|
62
62
|
- fedora:latest
|
|
63
63
|
- archlinux:latest
|
|
64
64
|
profile:
|
|
@@ -73,10 +73,10 @@ jobs:
|
|
|
73
73
|
- image: kalilinux/kali-rolling
|
|
74
74
|
pm: apt
|
|
75
75
|
ruby: apt-get update && apt-get install -y ruby-full ruby-dev build-essential git
|
|
76
|
-
- image: debian:
|
|
76
|
+
- image: debian:trixie
|
|
77
77
|
pm: apt
|
|
78
78
|
ruby: apt-get update && apt-get install -y ruby-full ruby-dev build-essential git
|
|
79
|
-
- image: ubuntu:
|
|
79
|
+
- image: ubuntu:latest
|
|
80
80
|
pm: apt
|
|
81
81
|
ruby: apt-get update && apt-get install -y ruby-full ruby-dev build-essential git
|
|
82
82
|
- image: fedora:latest
|
|
@@ -95,8 +95,15 @@ jobs:
|
|
|
95
95
|
run: ${{ matrix.ruby }}
|
|
96
96
|
|
|
97
97
|
- name: checkout
|
|
98
|
-
uses: actions/checkout@
|
|
98
|
+
uses: actions/checkout@v5
|
|
99
99
|
|
|
100
|
+
|
|
101
|
+
- name: trust git checkout owned by host UID (container job)
|
|
102
|
+
# actions/checkout marks the repo safe under a temp $HOME that doesn't
|
|
103
|
+
# survive into subsequent steps. Without this, `git ls-files` in
|
|
104
|
+
# pwn.gemspec (and any spec that shells out to git) fails with
|
|
105
|
+
# "detected dubious ownership" and the gem builds with ZERO files.
|
|
106
|
+
run: git config --global --add safe.directory "$PWD"
|
|
100
107
|
- name: build & install gem from checkout (git-checkout path in doc)
|
|
101
108
|
run: |
|
|
102
109
|
gem build pwn.gemspec
|
|
@@ -124,7 +131,7 @@ jobs:
|
|
|
124
131
|
PWN_EXPECTED_PM: brew
|
|
125
132
|
PWN_PROFILE: core
|
|
126
133
|
steps:
|
|
127
|
-
- uses: actions/checkout@
|
|
134
|
+
- uses: actions/checkout@v5
|
|
128
135
|
|
|
129
136
|
- uses: ruby/setup-ruby@v1
|
|
130
137
|
with:
|
|
@@ -164,8 +171,11 @@ jobs:
|
|
|
164
171
|
steps:
|
|
165
172
|
- run: apt-get update && apt-get install -y ruby-full ruby-dev build-essential git
|
|
166
173
|
|
|
167
|
-
- uses: actions/checkout@
|
|
174
|
+
- uses: actions/checkout@v5
|
|
175
|
+
|
|
168
176
|
|
|
177
|
+
- name: trust git checkout owned by host UID (container job)
|
|
178
|
+
run: git config --global --add safe.directory "$PWD"
|
|
169
179
|
- name: run legacy provisioner (must delegate to `pwn setup`)
|
|
170
180
|
run: bash packer/provisioners/pwn.sh
|
|
171
181
|
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
|
@@ -50,7 +50,7 @@ gem 'libusb', '0.7.2'
|
|
|
50
50
|
gem 'luhn', '3.0.0'
|
|
51
51
|
gem 'mail', '2.9.1'
|
|
52
52
|
gem 'mcp', '0.24.0'
|
|
53
|
-
gem 'meshtastic', '0.0.
|
|
53
|
+
gem 'meshtastic', '0.0.172'
|
|
54
54
|
gem 'metasm', '1.0.6'
|
|
55
55
|
gem 'mongo', '2.24.1'
|
|
56
56
|
gem 'msfrpc-client', '1.1.2'
|
|
@@ -105,4 +105,4 @@ gem 'webrick', '1.9.2'
|
|
|
105
105
|
gem 'whois', '6.0.3'
|
|
106
106
|
gem 'whois-parser', '2.0.0'
|
|
107
107
|
gem 'wicked_pdf', '2.8.2'
|
|
108
|
-
gem 'yard', '0.9.
|
|
108
|
+
gem 'yard', '0.9.45'
|
data/Rakefile
CHANGED
data/bin/pwn_fuzz_net_app_proto
CHANGED
|
@@ -75,13 +75,7 @@ pwn_provider = 'ruby-gem'
|
|
|
75
75
|
# pwn_provider = ENV.fetch('PWN_PROVIDER') if ENV.keys.select { |s| s == 'PWN_PROVIDER' }.any?
|
|
76
76
|
pwn_provider = ENV.fetch('PWN_PROVIDER') if ENV.keys.any? { |s| s == 'PWN_PROVIDER' }
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
require 'pwn'
|
|
80
|
-
elsif pwn_provider == 'docker'
|
|
81
|
-
require '/opt/pwn/lib/pwn'
|
|
82
|
-
else
|
|
83
|
-
raise "PWN_PROVIDER env variable is not set to 'ruby-gem' or 'docker'"
|
|
84
|
-
end
|
|
78
|
+
raise "PWN_PROVIDER env variable is not set to 'ruby-gem' or 'docker'" unless %w[ruby-gem docker].include?(pwn_provider)
|
|
85
79
|
|
|
86
80
|
dir_path = opts[:dir_path] ||= '.'
|
|
87
81
|
target = opts[:target]
|
|
@@ -13,9 +13,9 @@ toolsets; the JSON-Schema for each tool is what the model actually sees.
|
|
|
13
13
|
| `terminal` | `shell` | `Open3.capture3` on the host |
|
|
14
14
|
| `pwn` | `pwn_eval` | `TOPLEVEL_BINDING.eval` in the live REPL process |
|
|
15
15
|
| `memory` | `memory_remember` · `memory_recall` · `memory_forget` · `memory_clear` | `PWN::Memory` → `~/.pwn/memory.json` |
|
|
16
|
-
| `skills` | `skill_list` · `skill_view` · `skill_create` · `skill_add_reference` · `skill_delete` | `~/.pwn/skills
|
|
16
|
+
| `skills` | `skill_list` · `skill_view` · `skill_create` · `skill_add_reference` · `skill_delete` · `skill_migrate_legacy` | `~/.pwn/skills/<name>/SKILL.md` (agentskills.io; legacy flat `*.md` still read) |
|
|
17
17
|
| `sessions` | `sessions_list` · `sessions_view` · `sessions_current` · `sessions_delete` · `sessions_stats` | `PWN::Sessions` → `~/.pwn/sessions/` |
|
|
18
|
-
| `learning` | `learning_note_outcome` · `learning_reflect` · `learning_distill_skill` · `learning_stats` · `learning_outcomes` · `learning_consolidate` · `learning_reset` · `learning_auto_introspect_toggle` · **`mistakes_list`** · **`mistakes_record`** · **`mistakes_resolve`** · **`mistakes_reset`** | `PWN::AI::Agent::Learning` + `
|
|
18
|
+
| `learning` | `learning_note_outcome` · `learning_reflect` · `learning_distill_skill` · `learning_stats` · `learning_outcomes` · `learning_consolidate` · `learning_reset` · `learning_auto_introspect_toggle` · `learning_purge_noise` · **`mistakes_list`** · **`mistakes_record`** · **`mistakes_resolve`** · **`mistakes_reset`** · **`reward_judge`** · **`reward_prm`** · **`reward_sentinel`** · **`reward_preferences`** · **`reward_export_dpo`** · **`curriculum_practice`** · **`curriculum_train`** · **`curriculum_hindsight`** | `PWN::AI::Agent::Learning` + `Mistakes` + `Reward` + `Curriculum` → `~/.pwn/learning.jsonl` + `~/.pwn/mistakes.json` + `~/.pwn/preferences.jsonl` + `~/.pwn/curriculum/` |
|
|
19
19
|
| `metrics` | `metrics_summary` · `metrics_reset` | `PWN::AI::Agent::Metrics` → `~/.pwn/metrics.json` |
|
|
20
20
|
| `extrospection` | `extro_snapshot` · `extro_drift` · `extro_observe` · `extro_observations` · `extro_intel` · **`extro_watch`** · **`extro_verify`** · **`extro_rf_tune`** · **`extro_osint`** · **`extro_serial`** · **`extro_telecomm`** · **`extro_packet`** · **`extro_vision`** · **`extro_voice`** · `extro_correlate` · `extro_stats` · `extro_reset` · `extro_auto_toggle` | `PWN::AI::Agent::Extrospection` (+ Serial/Packet/OCR/Voice/BareSIP/TransparentBrowser) → `~/.pwn/extrospection.json` |
|
|
21
21
|
| `cron` | `cron_list` · `cron_create` · `cron_run` · `cron_enable` · `cron_disable` · `cron_remove` | `PWN::Cron` → `~/.pwn/cron/jobs.yml` |
|
|
@@ -17,7 +17,7 @@ needed to unlock the ones you want. No `/opt/pwn` checkout, no `rvmsudo`
|
|
|
17
17
|
chain, no bash provisioners required.
|
|
18
18
|
|
|
19
19
|
Tested on **Kali / Debian / Ubuntu**, **Fedora**, **Arch**, and **macOS**
|
|
20
|
-
(Homebrew or MacPorts). Ruby ≥ 3.
|
|
20
|
+
(Homebrew or MacPorts). Ruby ≥ 3.3 (RVM, rbenv, asdf, or system Ruby).
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
data/git_commit.sh
CHANGED
|
@@ -27,7 +27,7 @@ if (( $# == 3 )); then
|
|
|
27
27
|
if [[ $? -ne 0 ]]; then
|
|
28
28
|
echo 'ERROR: pwn_autoinc_version failed! Reinstalling pwn gemset...'
|
|
29
29
|
rvmsudo ./reinstall_pwn_gemset.sh
|
|
30
|
-
|
|
30
|
+
rake
|
|
31
31
|
rvmsudo rake install
|
|
32
32
|
if [[ $? -ne 0 ]]; then
|
|
33
33
|
echo 'ERROR: Attempt to reinstall pwn gemset failed! Please investigate and fix before trying again.'
|
|
@@ -225,26 +225,23 @@ module PWN
|
|
|
225
225
|
# )
|
|
226
226
|
|
|
227
227
|
public_class_method def self.distill_skill(opts = {})
|
|
228
|
-
name
|
|
229
|
-
raise 'ERROR: name is required' if name.empty?
|
|
228
|
+
raise 'ERROR: name is required' if opts[:name].to_s.strip.empty?
|
|
230
229
|
|
|
231
230
|
body = opts[:content].to_s
|
|
232
|
-
body = build_skill_from_session(session_id: opts[:session_id], name: name) if body.strip.empty? && opts[:session_id]
|
|
231
|
+
body = build_skill_from_session(session_id: opts[:session_id], name: opts[:name]) if body.strip.empty? && opts[:session_id]
|
|
233
232
|
raise 'ERROR: content or session_id is required' if body.strip.empty?
|
|
234
233
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
note_outcome(task: "distill_skill:#{name}", success: true, details: "Saved #{path}", tags: %w[skill auto])
|
|
247
|
-
{ saved: true, name: name, path: path, bytes: body.bytesize, references: refs }
|
|
234
|
+
root = skills_dir
|
|
235
|
+
out = PWN::Config.write_skill(
|
|
236
|
+
name: opts[:name],
|
|
237
|
+
description: opts[:description],
|
|
238
|
+
content: body,
|
|
239
|
+
references: opts[:references],
|
|
240
|
+
pwn_skills_path: root
|
|
241
|
+
)
|
|
242
|
+
PWN::Config.load_skills(pwn_skills_path: root) if PWN::Config.respond_to?(:load_skills)
|
|
243
|
+
note_outcome(task: "distill_skill:#{out[:name]}", success: true, details: "Saved #{out[:path]}", tags: %w[skill auto])
|
|
244
|
+
out.merge(saved: true)
|
|
248
245
|
end
|
|
249
246
|
|
|
250
247
|
# Supported Method Parameters::
|
|
@@ -117,11 +117,15 @@ module PWN
|
|
|
117
117
|
return '' unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) && !PWN::Skills.empty?
|
|
118
118
|
|
|
119
119
|
lines = PWN::Skills.map do |name, meta|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
desc = meta[:description].to_s.strip
|
|
121
|
+
if desc.empty?
|
|
122
|
+
# legacy / stubbed entry without a parsed description — fall back
|
|
123
|
+
desc = meta[:content].to_s.lines.reject { |l| l.strip.empty? || l.start_with?('---') }.first.to_s.strip
|
|
124
|
+
end
|
|
125
|
+
desc = desc[0, 100]
|
|
122
126
|
rc = Array(meta[:references]).length
|
|
123
127
|
ref_tag = rc.positive? ? " [#{rc} refs]" : ''
|
|
124
|
-
" - #{name}: #{
|
|
128
|
+
" - #{name}: #{desc}#{ref_tag}"
|
|
125
129
|
end
|
|
126
130
|
"SKILLS (call skill_view to read full body)\n#{lines.join("\n")}\n\n"
|
|
127
131
|
rescue StandardError
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'pwn/ai/agent/registry'
|
|
5
5
|
|
|
6
|
-
# Thin wrappers around the PWN::Skills store
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Thin wrappers around the PWN::Skills store so the model can list / read /
|
|
7
|
+
# write reusable procedures. PromptBuilder injects the index (name +
|
|
8
|
+
# description); skill_view loads the full body on demand — the two-tier
|
|
9
|
+
# progressive-disclosure pattern from https://agentskills.io/specification.
|
|
10
|
+
#
|
|
11
|
+
# On disk (spec-conformant, written by PWN::Config.write_skill):
|
|
12
|
+
# ~/.pwn/skills/<name>/SKILL.md ← YAML frontmatter (name, description, …)
|
|
13
|
+
# ~/.pwn/skills/<name>/scripts/ ← optional executable helpers
|
|
14
|
+
# ~/.pwn/skills/<name>/references/ ← optional supporting docs
|
|
15
|
+
#
|
|
16
|
+
# Legacy flat ~/.pwn/skills/<name>.md files are still LOADED (format:
|
|
17
|
+
# :legacy) so existing installs keep working; new writes always use the
|
|
18
|
+
# directory layout. Run PWN::Config.migrate_legacy_skills to convert.
|
|
9
19
|
#
|
|
10
20
|
# Each skill may carry a :references Array (URLs, CWE/CVE/ATT&CK ids,
|
|
11
|
-
# NIST 800-53 controls, docs) parsed from
|
|
12
|
-
# "## References" markdown section
|
|
13
|
-
# authoritative sources when applying a skill and mirror the
|
|
21
|
+
# NIST 800-53 controls, docs) parsed from frontmatter `metadata.references`
|
|
22
|
+
# and/or a "## References" markdown section — mirroring the
|
|
14
23
|
# PWN::SAST::*.security_references convention.
|
|
15
24
|
|
|
16
25
|
PWN::AI::Agent::Registry.register(
|
|
@@ -18,7 +27,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
18
27
|
toolset: 'skills',
|
|
19
28
|
schema: {
|
|
20
29
|
name: 'skill_list',
|
|
21
|
-
description: 'List available pwn-ai skills (name +
|
|
30
|
+
description: 'List available pwn-ai skills (name + description + reference count + format).',
|
|
22
31
|
parameters: { type: 'object', properties: {}, required: [] }
|
|
23
32
|
},
|
|
24
33
|
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
@@ -27,7 +36,8 @@ PWN::AI::Agent::Registry.register(
|
|
|
27
36
|
{
|
|
28
37
|
name: name,
|
|
29
38
|
type: meta[:type],
|
|
30
|
-
|
|
39
|
+
format: meta[:format] || :legacy,
|
|
40
|
+
description: (meta[:description] || meta[:content].to_s.lines.first.to_s.strip)[0, 200],
|
|
31
41
|
references: Array(meta[:references]).length
|
|
32
42
|
}
|
|
33
43
|
end
|
|
@@ -39,7 +49,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
39
49
|
toolset: 'skills',
|
|
40
50
|
schema: {
|
|
41
51
|
name: 'skill_view',
|
|
42
|
-
description: 'Read the full content of a named skill, including its references.',
|
|
52
|
+
description: 'Read the full content of a named skill, including its frontmatter and references.',
|
|
43
53
|
parameters: {
|
|
44
54
|
type: 'object',
|
|
45
55
|
properties: { name: { type: 'string' } },
|
|
@@ -49,13 +59,17 @@ PWN::AI::Agent::Registry.register(
|
|
|
49
59
|
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
50
60
|
handler: lambda { |args|
|
|
51
61
|
key = args[:name].to_s.to_sym
|
|
52
|
-
meta = PWN::Skills[key]
|
|
62
|
+
meta = PWN::Skills[key] || PWN::Skills[PWN::Config.sanitize_skill_name(name: key).to_sym]
|
|
53
63
|
raise ArgumentError, "no such skill: #{key}" unless meta
|
|
54
64
|
|
|
55
65
|
{
|
|
56
66
|
name: key,
|
|
57
67
|
type: meta[:type],
|
|
68
|
+
format: meta[:format] || :legacy,
|
|
58
69
|
path: meta[:path],
|
|
70
|
+
dir: meta[:dir],
|
|
71
|
+
description: meta[:description],
|
|
72
|
+
frontmatter: meta[:frontmatter] || {},
|
|
59
73
|
references: Array(meta[:references]),
|
|
60
74
|
content: meta[:content]
|
|
61
75
|
}
|
|
@@ -67,49 +81,46 @@ PWN::AI::Agent::Registry.register(
|
|
|
67
81
|
toolset: 'skills',
|
|
68
82
|
schema: {
|
|
69
83
|
name: 'skill_create',
|
|
70
|
-
description: 'Save a new reusable skill
|
|
71
|
-
'
|
|
72
|
-
'
|
|
84
|
+
description: 'Save a new reusable skill to ~/.pwn/skills/<name>/SKILL.md ' \
|
|
85
|
+
'(agentskills.io format: required name + description YAML ' \
|
|
86
|
+
'frontmatter). Appears in every future pwn-ai system prompt. ' \
|
|
87
|
+
'Optionally attach references (URLs, CWE/CVE ids, MITRE ' \
|
|
88
|
+
'ATT&CK, NIST 800-53, docs) and an allowed-tools allowlist.',
|
|
73
89
|
parameters: {
|
|
74
90
|
type: 'object',
|
|
75
91
|
properties: {
|
|
76
|
-
name: { type: 'string', description: '
|
|
77
|
-
|
|
92
|
+
name: { type: 'string', description: 'Free-form; sanitised to [a-z0-9-]{1,64} per spec.' },
|
|
93
|
+
description: { type: 'string', description: '1..1024 char summary shown in the SKILLS index. Derived from body when omitted.' },
|
|
94
|
+
content: { type: 'string', description: 'Full markdown body of the skill (WITHOUT frontmatter — it is generated).' },
|
|
78
95
|
references: {
|
|
79
96
|
type: 'array',
|
|
80
97
|
items: { type: 'string' },
|
|
81
98
|
description: 'Optional list of reference URLs / identifiers (CWE-79, T1059, ' \
|
|
82
|
-
'https://..., NIST SI-3, etc.). Written
|
|
83
|
-
'a "## References" section.'
|
|
84
|
-
}
|
|
99
|
+
'https://..., NIST SI-3, etc.). Written under frontmatter ' \
|
|
100
|
+
'metadata.references and as a "## References" section.'
|
|
101
|
+
},
|
|
102
|
+
license: { type: 'string', description: 'Optional SPDX identifier or free text.' },
|
|
103
|
+
allowed_tools: { type: 'array', items: { type: 'string' }, description: 'Optional toolset allowlist for this skill.' },
|
|
104
|
+
metadata: { type: 'object', description: 'Optional arbitrary metadata hash.' }
|
|
85
105
|
},
|
|
86
106
|
required: %w[name content]
|
|
87
107
|
}
|
|
88
108
|
},
|
|
89
|
-
check: -> { defined?(PWN::Config) && PWN::Config.respond_to?(:
|
|
109
|
+
check: -> { defined?(PWN::Config) && PWN::Config.respond_to?(:write_skill) },
|
|
90
110
|
handler: lambda { |args|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
body = "#{body.rstrip}\n\n## References\n#{refs.map { |r| "- #{r}" }.join("\n")}\n" unless body =~ /^\#{1,3}\s*References\s*$/i
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
dir = PWN::Config.pwn_skills_path
|
|
108
|
-
FileUtils.mkdir_p(dir)
|
|
109
|
-
path = File.join(dir, "#{name}.md")
|
|
110
|
-
File.write(path, body)
|
|
111
|
-
PWN::Config.load_skills(pwn_skills_path: dir) # refresh PWN::Skills const
|
|
112
|
-
{ saved: true, path: path, references: refs, total: PWN::Skills.keys.length }
|
|
111
|
+
root = PWN::Config.pwn_skills_path
|
|
112
|
+
out = PWN::Config.write_skill(
|
|
113
|
+
name: args[:name],
|
|
114
|
+
description: args[:description],
|
|
115
|
+
content: args[:content],
|
|
116
|
+
references: args[:references],
|
|
117
|
+
license: args[:license],
|
|
118
|
+
allowed_tools: args[:allowed_tools],
|
|
119
|
+
metadata: args[:metadata],
|
|
120
|
+
pwn_skills_path: root
|
|
121
|
+
)
|
|
122
|
+
PWN::Config.load_skills(pwn_skills_path: root)
|
|
123
|
+
out.merge(saved: true, total: PWN::Skills.keys.length)
|
|
113
124
|
}
|
|
114
125
|
)
|
|
115
126
|
|
|
@@ -119,7 +130,8 @@ PWN::AI::Agent::Registry.register(
|
|
|
119
130
|
schema: {
|
|
120
131
|
name: 'skill_add_reference',
|
|
121
132
|
description: 'Append one or more references (URL, CWE/CVE id, MITRE ATT&CK ' \
|
|
122
|
-
'technique, NIST control, doc link) to an existing skill.'
|
|
133
|
+
'technique, NIST control, doc link) to an existing skill. ' \
|
|
134
|
+
'Rewrites frontmatter metadata.references + ## References section.',
|
|
123
135
|
parameters: {
|
|
124
136
|
type: 'object',
|
|
125
137
|
properties: {
|
|
@@ -132,31 +144,50 @@ PWN::AI::Agent::Registry.register(
|
|
|
132
144
|
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
133
145
|
handler: lambda { |args|
|
|
134
146
|
key = args[:name].to_s.to_sym
|
|
135
|
-
meta = PWN::Skills[key]
|
|
147
|
+
meta = PWN::Skills[key] || PWN::Skills[PWN::Config.sanitize_skill_name(name: key).to_sym]
|
|
136
148
|
raise ArgumentError, "no such skill: #{key}" unless meta
|
|
137
149
|
|
|
138
150
|
new_refs = Array(args[:references]).map(&:to_s).map(&:strip).reject(&:empty?)
|
|
139
151
|
raise ArgumentError, 'references must be a non-empty array' if new_refs.empty?
|
|
140
152
|
|
|
141
|
-
|
|
153
|
+
root = PWN::Config.pwn_skills_path
|
|
142
154
|
existing = Array(meta[:references])
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
combined = (existing + new_refs).uniq
|
|
156
|
+
added = combined - existing
|
|
157
|
+
|
|
158
|
+
if meta[:format] == :agentskills
|
|
159
|
+
# Rewrite via the canonical writer so frontmatter stays valid.
|
|
160
|
+
body = PWN::Config.parse_skill_frontmatter(content: meta[:content])[:body]
|
|
161
|
+
.to_s.sub(/^\#{1,3}\s*References\s*$.*\z/mi, '').rstrip
|
|
162
|
+
PWN::Config.write_skill(
|
|
163
|
+
name: key.to_s,
|
|
164
|
+
description: meta[:description],
|
|
165
|
+
content: body,
|
|
166
|
+
references: combined,
|
|
167
|
+
license: (meta[:frontmatter] || {})['license'],
|
|
168
|
+
allowed_tools: meta[:allowed_tools],
|
|
169
|
+
metadata: (meta[:frontmatter] || {})['metadata'],
|
|
170
|
+
pwn_skills_path: root
|
|
171
|
+
)
|
|
148
172
|
else
|
|
149
|
-
|
|
173
|
+
# Legacy flat file — append to (or create) the ## References section in place.
|
|
174
|
+
body = File.read(meta[:path])
|
|
175
|
+
body = if body =~ /^(\#{1,3}\s*References\s*)$/i
|
|
176
|
+
"#{body.rstrip}\n#{added.map { |r| "- #{r}" }.join("\n")}\n"
|
|
177
|
+
else
|
|
178
|
+
"#{body.rstrip}\n\n## References\n#{combined.map { |r| "- #{r}" }.join("\n")}\n"
|
|
179
|
+
end
|
|
180
|
+
File.write(meta[:path], body)
|
|
150
181
|
end
|
|
151
182
|
|
|
152
|
-
|
|
153
|
-
PWN::Config.
|
|
183
|
+
PWN::Config.load_skills(pwn_skills_path: root)
|
|
184
|
+
reloaded = PWN::Skills[key] || PWN::Skills[PWN::Config.sanitize_skill_name(name: key).to_sym]
|
|
154
185
|
{
|
|
155
186
|
saved: true,
|
|
156
187
|
name: key,
|
|
157
|
-
path:
|
|
158
|
-
added:
|
|
159
|
-
references: Array(
|
|
188
|
+
path: reloaded[:path],
|
|
189
|
+
added: added,
|
|
190
|
+
references: Array(reloaded[:references])
|
|
160
191
|
}
|
|
161
192
|
}
|
|
162
193
|
)
|
|
@@ -166,13 +197,15 @@ PWN::AI::Agent::Registry.register(
|
|
|
166
197
|
toolset: 'skills',
|
|
167
198
|
schema: {
|
|
168
199
|
name: 'skill_delete',
|
|
169
|
-
description: 'Delete a skill from ~/.pwn/skills/ by name.
|
|
170
|
-
'
|
|
171
|
-
'
|
|
200
|
+
description: 'Delete a skill from ~/.pwn/skills/ by name. Removes the ' \
|
|
201
|
+
'entire <name>/ directory (agentskills format) or the flat ' \
|
|
202
|
+
'file (legacy). Use to prune low-quality auto-distilled ' \
|
|
203
|
+
'skills so they stop appearing in every future system prompt. ' \
|
|
204
|
+
'Irreversible.',
|
|
172
205
|
parameters: {
|
|
173
206
|
type: 'object',
|
|
174
207
|
properties: {
|
|
175
|
-
name: { type: 'string', description: 'Existing skill name
|
|
208
|
+
name: { type: 'string', description: 'Existing skill name.' }
|
|
176
209
|
},
|
|
177
210
|
required: %w[name]
|
|
178
211
|
}
|
|
@@ -180,13 +213,39 @@ PWN::AI::Agent::Registry.register(
|
|
|
180
213
|
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
181
214
|
handler: lambda { |args|
|
|
182
215
|
key = args[:name].to_s.to_sym
|
|
183
|
-
meta = PWN::Skills[key]
|
|
216
|
+
meta = PWN::Skills[key] || PWN::Skills[PWN::Config.sanitize_skill_name(name: key).to_sym]
|
|
184
217
|
raise ArgumentError, "no such skill: #{key}" unless meta
|
|
185
218
|
|
|
219
|
+
root = PWN::Config.pwn_skills_path
|
|
186
220
|
path = meta[:path]
|
|
187
|
-
dir
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
if meta[:format] == :agentskills && meta[:dir] && File.basename(File.dirname(path)) == File.basename(meta[:dir])
|
|
222
|
+
FileUtils.rm_rf(meta[:dir])
|
|
223
|
+
else
|
|
224
|
+
FileUtils.rm_f(path)
|
|
225
|
+
end
|
|
226
|
+
PWN::Config.load_skills(pwn_skills_path: root)
|
|
190
227
|
{ deleted: true, name: key, path: path, remaining: PWN::Skills.keys.length }
|
|
191
228
|
}
|
|
192
229
|
)
|
|
230
|
+
|
|
231
|
+
PWN::AI::Agent::Registry.register(
|
|
232
|
+
name: 'skill_migrate_legacy',
|
|
233
|
+
toolset: 'skills',
|
|
234
|
+
schema: {
|
|
235
|
+
name: 'skill_migrate_legacy',
|
|
236
|
+
description: 'One-shot: convert every legacy flat ~/.pwn/skills/*.md into ' \
|
|
237
|
+
'a spec-conformant <name>/SKILL.md with backfilled name + ' \
|
|
238
|
+
'description frontmatter. Idempotent; safe to re-run.',
|
|
239
|
+
parameters: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
delete_legacy: { type: 'boolean', default: true, description: 'Remove the flat file after migration.' }
|
|
243
|
+
},
|
|
244
|
+
required: []
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
check: -> { defined?(PWN::Config) && PWN::Config.respond_to?(:migrate_legacy_skills) },
|
|
248
|
+
handler: lambda { |args|
|
|
249
|
+
PWN::Config.migrate_legacy_skills(delete_legacy: args.fetch(:delete_legacy, true))
|
|
250
|
+
}
|
|
251
|
+
)
|