pwn 0.5.682 → 0.5.683
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/documentation/Agent-Tool-Registry.md +11 -7
- data/documentation/Configuration.md +4 -2
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Reinforcement-Learning.md +1 -1
- data/documentation/diagrams/agent-tool-registry.svg +1 -1
- data/documentation/diagrams/dot/agent-tool-registry.dot +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/lib/pwn/ai/agent/loop.rb +40 -6
- data/lib/pwn/ai/agent/policy.rb +2 -3
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -12
- data/lib/pwn/ai/agent/registry.rb +6 -9
- data/lib/pwn/ai/agent/tools/sessions.rb +32 -0
- data/lib/pwn/ai/agent/tools/skills.rb +56 -0
- data/lib/pwn/ai/anthropic.rb +0 -1
- data/lib/pwn/ai/gemini.rb +0 -1
- data/lib/pwn/ai/grok.rb +0 -1
- data/lib/pwn/ai/ollama.rb +0 -1
- data/lib/pwn/ai/open_ai.rb +0 -1
- data/lib/pwn/ai/open_web_ui.rb +0 -1
- data/lib/pwn/config.rb +1 -1
- data/lib/pwn/plugins/repl.rb +37 -0
- data/lib/pwn/plugins/tty_spinner.rb +55 -6
- data/lib/pwn/sessions.rb +82 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/prompt_builder_spec.rb +3 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +63 -1
- data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +7 -3
- data/spec/lib/pwn/ai/agent/registry_spec.rb +16 -9
- data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +16 -0
- data/spec/lib/pwn/ai/red_team/test_case_engine_spec.rb +20 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +9 -0
- data/spec/lib/pwn/plugins/tty_spinner_spec.rb +30 -0
- data/spec/lib/pwn/sessions_spec.rb +29 -0
- data/spec/spec_helper.rb +6 -0
- data/third_party/pwn_rdoc.jsonl +6 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 138deb9b9916fd71329a5898d6cb49ab53751e597f408177aee7682704f33500
|
|
4
|
+
data.tar.gz: 77f88100898efbff17bea33bbd2efa186afecf2776c02d3e577a71b26d1b2d3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 265a38e4abaead447360e549c98f88dc0a3d3220358d0e30c9e9419d6c1b53dd558dac87b2ce093e968b53bbab9051db7032b405d5835194c33934acd64ecb4d
|
|
7
|
+
data.tar.gz: 7c0e34b2be0c96ddef980ee81493f5b2d394680ca956f0b6208cc32758fda13f212670febca563092cd5b0331ac46464bdecd0f6c88d2f7a15744f4b1cd79d48
|
|
@@ -13,8 +13,8 @@ toolsets; the JSON-Schema for each tool is what the model actually sees.
|
|
|
13
13
|
| `terminal` | `shell` | `Open3.capture3` on the host, after `PWN::AI::Agent::ToolGuard` |
|
|
14
14
|
| `pwn` | `pwn_eval` | `TOPLEVEL_BINDING.eval` in the live REPL process, after `ToolGuard` |
|
|
15
15
|
| `memory` | `memory_remember` · `memory_recall` · `memory_forget` · `memory_clear` · **`memory_lean`** | `PWN::Memory` → `~/.pwn/memory.json` |
|
|
16
|
-
| `skills` | `skill_list` · `skill_view` · `skill_create` · `skill_add_reference` · `skill_delete` · `skill_migrate_legacy` | `~/.pwn/skills/<name>/SKILL.md` (**[agentskills.io](https://agentskills.io) spec**; legacy flat `*.md` auto-migrated) |
|
|
17
|
-
| `sessions` | `sessions_list` · `sessions_view` · `sessions_current` · `sessions_delete` · `sessions_stats` · **`sessions_lean`** | `PWN::Sessions` → `~/.pwn/sessions/` |
|
|
16
|
+
| `skills` | **`skills_recall`** · `skill_list` · `skill_view` · `skill_create` · `skill_add_reference` · `skill_delete` · `skill_migrate_legacy` | `~/.pwn/skills/<name>/SKILL.md` (**[agentskills.io](https://agentskills.io) spec**; legacy flat `*.md` auto-migrated) |
|
|
17
|
+
| `sessions` | **`session_recall`** · `sessions_list` · `sessions_view` · `sessions_current` · `sessions_delete` · `sessions_stats` · **`sessions_lean`** | `PWN::Sessions` → `~/.pwn/sessions/` |
|
|
18
18
|
| `learning` | `learning_note_outcome` · `learning_reflect` · `learning_distill_skill` · `learning_stats` · `learning_outcomes` · `learning_consolidate` · `learning_reset` · `learning_auto_introspect_toggle` · **`learning_gc_stores`** · **`learning_purge_noise`** · **`mistakes_list`** · **`mistakes_record`** · **`mistakes_resolve`** · **`mistakes_reset`** · **`mistakes_lean`** · **`reward_judge`** · **`reward_prm`** · **`reward_sentinel`** · **`reward_preferences`** · **`reward_export_dpo`** · **`reward_warm_sentinel`** · **`reward_scrub_preferences`** · **`reward_preference_balance`** · **`curriculum_practice`** · **`curriculum_train`** · **`curriculum_hindsight`** · **`curriculum_offline_judge`** · **`curriculum_preference_balance`** | `PWN::AI::Agent::Learning` + `Mistakes` + `Reward` + `Curriculum` → `~/.pwn/learning.jsonl` + `~/.pwn/mistakes.json` + `~/.pwn/preferences.jsonl` + `~/.pwn/curriculum/` + `~/.pwn/finetune/` |
|
|
19
19
|
| `reward` | **`reward_generator_mix`** | `PWN::AI::Agent::Reward.generator_mix` → online preference source-mix controller (`preferences.jsonl`) |
|
|
20
20
|
| `curriculum` | **`curriculum_practice_kpi`** | `PWN::AI::Agent::Curriculum.practice_kpi` → `~/.pwn/curriculum_kpi.jsonl` |
|
|
@@ -56,8 +56,9 @@ user request through as `relevance:`, `Registry.definitions` shrinks the
|
|
|
56
56
|
pool to:
|
|
57
57
|
|
|
58
58
|
```text
|
|
59
|
-
CORE_TOOLS
|
|
60
|
-
|
|
59
|
+
CORE_TOOLS / DEFAULT_PREFERENCE (same list):
|
|
60
|
+
memory_recall · session_recall · skills_recall · pwn_eval · shell
|
|
61
|
+
mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember
|
|
61
62
|
+ top-K keyword-ranked matches for THIS request
|
|
62
63
|
(ties break on Metrics per-engine success_rate, then
|
|
63
64
|
ai.agent.tool_preference)
|
|
@@ -80,7 +81,7 @@ When keyword fit and other rank scores tie, the registry prefers this
|
|
|
80
81
|
default order:
|
|
81
82
|
|
|
82
83
|
```text
|
|
83
|
-
memory_recall · pwn_eval · shell
|
|
84
|
+
memory_recall · session_recall · skills_recall · pwn_eval · shell
|
|
84
85
|
mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember
|
|
85
86
|
```
|
|
86
87
|
|
|
@@ -88,9 +89,12 @@ Set `ai.agent.tool_preference` in `~/.pwn/pwn.yaml`, or pass `order:` /
|
|
|
88
89
|
`preference:` into `Registry.definitions`, `.rank`, or `.apply_preference`.
|
|
89
90
|
An explicit empty list turns preference off (no Env / default fallback).
|
|
90
91
|
|
|
92
|
+
Learned facts and this session are injected (MEMORY / RECENT TURNS), not
|
|
93
|
+
first tools. Preference then lists `pwn_eval` before `shell`. `sessions_view`
|
|
94
|
+
is not a CORE tool. There is no separate ACT_PREFERENCE.
|
|
95
|
+
|
|
91
96
|
Keyword fit stays the primary signal. Preference is a smaller bonus plus a
|
|
92
|
-
stable sort after the router slims the pool
|
|
93
|
-
tie against `shell` without hiding a better keyword match.
|
|
97
|
+
stable sort after the router slims the pool.
|
|
94
98
|
|
|
95
99
|
`Policy` uses the same list when it suggests a next action in the prompt.
|
|
96
100
|
|
|
@@ -133,8 +133,10 @@ ai:
|
|
|
133
133
|
shell_bash: false # true -> run shell via bash -lc. Default is /bin/sh.
|
|
134
134
|
plan_first: ~ # Plan-then-act pre-pass. nil = auto (true when ai.active is ollama or openwebui).
|
|
135
135
|
tool_router: ~ # Dynamic tool-set slimming. nil = auto (true for ollama / openwebui).
|
|
136
|
-
tool_preference: #
|
|
136
|
+
tool_preference: # Same order as CORE_TOOLS. Current session is injected; then memory_recall, session_recall, skills_recall, pwn_eval, shell.
|
|
137
137
|
- memory_recall
|
|
138
|
+
- session_recall
|
|
139
|
+
- skills_recall
|
|
138
140
|
- pwn_eval
|
|
139
141
|
- shell
|
|
140
142
|
- mistakes_record
|
|
@@ -315,7 +317,7 @@ PWN::Config.refresh_env
|
|
|
315
317
|
| `ai.agent.shell_bash` | Boolean | `false` | `PWN::AI::Agent::ToolGuard.shell_bash?` | When true, `shell` runs via `bash -lc` so bash-only syntax is allowed. Default is POSIX `/bin/sh` and bashisms are rejected with a rewrite hint. |
|
|
316
318
|
| `ai.agent.plan_first` | Boolean \| `nil` | `nil` (auto: `true` when `ai.active` is `ollama` or `openwebui`) | `PWN::AI::Agent::Loop.plan_first` | Plan-then-act pre-pass: the model must emit a numbered tool plan (as an assistant message) *before* it may dispatch anything. Cheap chain-of-thought scaffolding for local models. |
|
|
317
319
|
| `ai.agent.tool_router` | Boolean \| `nil` | `nil` (auto: `true` for `ollama` / `openwebui`) | `PWN::AI::Agent::Registry.definitions` | Dynamic tool-set slimming: expose only `Registry::CORE_TOOLS` + the top-K keyword-relevant schemas for *this* request. Ties break on historical `Metrics` success rate, then `ai.agent.tool_preference`. |
|
|
318
|
-
| `ai.agent.tool_preference` | Array\<String\> | `memory_recall`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember` | `PWN::AI::Agent::Registry.preference_order` / `.rank` / `.apply_preference`, `Policy` |
|
|
320
|
+
| `ai.agent.tool_preference` | Array\<String\> | `memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember` | `PWN::AI::Agent::Registry.preference_order` / `.rank` / `.apply_preference`, `Policy` | Same order as `CORE_TOOLS`. Current session is injected; then `memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`. Explicit empty list disables preference. |
|
|
319
321
|
| `ai.agent.defer_introspect` | Boolean | `true` | `PWN::AI::Agent::TurnFinalizer` | Run `Learning.auto_introspect` on a background thread after the user-visible reply. Specs and cron stay inline. |
|
|
320
322
|
| `ai.agent.prompt_cache` | Boolean | `true` | `PWN::AI::Agent::PromptCache` | Engine-native prefix cache. Anthropic uses `cache_control`; OpenAI uses `prompt_cache_key`; Grok uses `x-grok-conv-id`; Gemini splits `systemInstruction`. Ollama and Open WebUI have no native prefix-cache field. |
|
|
321
323
|
| `ai.agent.local_introspect` | Symbol | `failure_only` | `PWN::AI::Agent::Learning.auto_introspect` | End-of-turn introspect policy for local engines: `always` · `failure_only` · `every_n` (with `introspect_every_n`). |
|
|
@@ -29,7 +29,7 @@ hardware).
|
|
|
29
29
|
|---|---|
|
|
30
30
|
| `Loop` | plan → **TaskSummarizer** briefs → dispatch tool_calls → observe → repeat until final answer; tightens runway when recent turns exhausted the budget |
|
|
31
31
|
| **`TaskSummarizer`** | Executive UX: every request gets an English task compass (`emit_plan!` · `about_to` as `task k/n`) — no statement/question/goal type |
|
|
32
|
-
| `Registry` | JSON-Schema function definitions grouped into 13 **toolsets** · **
|
|
32
|
+
| `Registry` | JSON-Schema function definitions grouped into 13 **toolsets** · **87 tools** · `CORE_TOOLS` = `DEFAULT_PREFERENCE` (`memory_recall` · `session_recall` · `skills_recall` · `pwn_eval` · `shell`) |
|
|
33
33
|
| `Dispatch` / `Result` | execute a tool, capture stdout/value/error/duration |
|
|
34
34
|
| `PromptBuilder` | inject MEMORY / SKILLS / LEARNING / **KNOWN MISTAKES + FIXES** / METRICS / **POLICY** / EXTROSPECTION / RECENT TURNS |
|
|
35
35
|
| `Metrics` · `Learning` · `Reflect` · **`Policy`** | **introspection** - how well am I doing? (Policy is live Q / REINFORCE, advisory rank only) |
|
|
@@ -169,7 +169,7 @@ This table is the live control list. Track the outcomes, not source comments.
|
|
|
169
169
|
:max_iters: 75 # hard cap; budget pressure may lower effective value
|
|
170
170
|
:defer_introspect: true # post-answer Learning after the user-visible reply
|
|
171
171
|
:prompt_cache: true # engine-native prefix cache (not ollama / openwebui)
|
|
172
|
-
:tool_preference: [memory_recall, pwn_eval, shell, mistakes_record, mistakes_resolve, learning_note_outcome, memory_remember]
|
|
172
|
+
:tool_preference: [memory_recall, session_recall, skills_recall, pwn_eval, shell, mistakes_record, mistakes_resolve, learning_note_outcome, memory_remember]
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
## Cron self-improvement
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<text xml:space="preserve" text-anchor="middle" x="105.12" y="-521.82" font-family="sans-Serif" font-size="12.00" fill="#0f172a">JSON-Schema function defs</text>
|
|
30
30
|
<text xml:space="preserve" text-anchor="middle" x="105.12" y="-507.57" font-family="sans-Serif" font-size="12.00" fill="#0f172a">rank(query) · CORE_TOOLS</text>
|
|
31
31
|
<text xml:space="preserve" text-anchor="middle" x="105.12" y="-493.32" font-family="sans-Serif" font-size="12.00" fill="#0f172a">tool_router → CORE + top-K</text>
|
|
32
|
-
<text xml:space="preserve" text-anchor="middle" x="105.12" y="-479.07" font-family="sans-Serif" font-size="12.00" fill="#0f172a">tool_preference (
|
|
32
|
+
<text xml:space="preserve" text-anchor="middle" x="105.12" y="-479.07" font-family="sans-Serif" font-size="12.00" fill="#0f172a">tool_preference (pwn_eval then shell)</text>
|
|
33
33
|
</g>
|
|
34
34
|
<!-- terminal -->
|
|
35
35
|
<g id="node2" class="node">
|
|
@@ -9,7 +9,7 @@ digraph "PWN_Agent_Tool_Registry" {
|
|
|
9
9
|
shape=box, penwidth=1.3, color="#334155", fontcolor="#0f172a"];
|
|
10
10
|
edge [color="#94a3b8", penwidth=1.1, arrowsize=0.7];
|
|
11
11
|
|
|
12
|
-
Registry [label="Registry\nJSON-Schema function defs\nrank(query) · CORE_TOOLS\ntool_router → CORE + top-K\ntool_preference (
|
|
12
|
+
Registry [label="Registry\nJSON-Schema function defs\nrank(query) · CORE_TOOLS\ntool_router → CORE + top-K\ntool_preference (pwn_eval then shell)", fillcolor="#c4b5fd", fontsize=12, penwidth=2];
|
|
13
13
|
|
|
14
14
|
subgraph cluster_ts {
|
|
15
15
|
label="Toolsets"; fontcolor="#a7f3d0"; style=rounded;
|
|
@@ -197,7 +197,7 @@ max_iters: 75 # budget pressure may lower the effective cap (s
|
|
|
197
197
|
| `reward_llm_timeout` | `12` | seconds for the cheap ORM chat (clamped 2..30) |
|
|
198
198
|
| `verify_as_reward` | `nil` (auto) | browser-grounded claim sample policy |
|
|
199
199
|
| `local_introspect` | `:failure_only` | ollama / openwebui end-of-turn introspect policy |
|
|
200
|
-
| `tool_preference` |
|
|
200
|
+
| `tool_preference` | same list as CORE_TOOLS (`memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`) | Rank bonus + Policy suggested-action order |
|
|
201
201
|
| `defer_introspect` | `true` | Post-answer Learning on a background thread |
|
|
202
202
|
| `prompt_cache` | `true` | Engine-native prefix cache (not Ollama / Open WebUI) |
|
|
203
203
|
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -321,9 +321,15 @@ module PWN
|
|
|
321
321
|
LOOKUP_REQUEST_RX = /
|
|
322
322
|
\b(what\s+is\s+my|hostname|uname|cwd|whoami|status|version|how\s+many)\b
|
|
323
323
|
/ix
|
|
324
|
-
|
|
324
|
+
# Real filesystem paths only — not https://host.tld (that was matching //host.tld).
|
|
325
|
+
HOST_PATH_RX = %r{(?:(?<![.:/])/(?!/)|\./)[\w./-]+\.\w+}
|
|
326
|
+
BROWSER_REQUEST_RX = /
|
|
327
|
+
TransparentBrowser|browser_obj|\bdevtools\b|
|
|
328
|
+
\b(navigate|dump_links|headless_?chrome|watir)\b
|
|
329
|
+
/ix
|
|
330
|
+
BROWSER_EVIDENCE_RX = %r{https?://|dump_links|TransparentBrowser|\.close\b|browser_obj}i
|
|
325
331
|
|
|
326
|
-
# True only when the ask needs a live host/file effect. World-knowledge
|
|
332
|
+
# True only when the ask needs a live host/file/browser effect. World-knowledge
|
|
327
333
|
# questions ("what color is a cherry") do not.
|
|
328
334
|
public_class_method def self.needs_host_work?(opts = {})
|
|
329
335
|
request = opts[:request].to_s
|
|
@@ -331,6 +337,7 @@ module PWN
|
|
|
331
337
|
return true if request.match?(ACT_REQUEST_RX)
|
|
332
338
|
return true if request.match?(LOOKUP_REQUEST_RX)
|
|
333
339
|
return true if request.match?(HOST_PATH_RX)
|
|
340
|
+
return true if request.match?(BROWSER_REQUEST_RX)
|
|
334
341
|
|
|
335
342
|
false
|
|
336
343
|
rescue StandardError
|
|
@@ -377,10 +384,16 @@ module PWN
|
|
|
377
384
|
|
|
378
385
|
return false if request.match?(LOOKUP_REQUEST_RX) && tools.any? && blob.length >= 20
|
|
379
386
|
return true if tools.empty?
|
|
380
|
-
return false if blob.match?(MUTATION_EVIDENCE_RX)
|
|
381
|
-
return false if request_path_evidenced?(request: request, blob: blob)
|
|
382
387
|
|
|
383
|
-
|
|
388
|
+
if request.match?(ACT_REQUEST_RX) || request.match?(HOST_PATH_RX)
|
|
389
|
+
return false if blob.match?(MUTATION_EVIDENCE_RX)
|
|
390
|
+
return false if request_path_evidenced?(request: request, blob: blob)
|
|
391
|
+
|
|
392
|
+
return true
|
|
393
|
+
end
|
|
394
|
+
return false if request.match?(BROWSER_REQUEST_RX) && blob.match?(BROWSER_EVIDENCE_RX)
|
|
395
|
+
|
|
396
|
+
blob.length < 20
|
|
384
397
|
rescue StandardError
|
|
385
398
|
false
|
|
386
399
|
end
|
|
@@ -1326,6 +1339,7 @@ module PWN
|
|
|
1326
1339
|
last\s+thing\s+(?:i|you)\s+said
|
|
1327
1340
|
)\b
|
|
1328
1341
|
/ix
|
|
1342
|
+
LAST_SESSION_RX = /\b(?:in|from|of)\s+(?:the\s+)?(?:last|previous|prior)\s+session\b|\blast\s+session\b/i
|
|
1329
1343
|
|
|
1330
1344
|
# Pure greeting / light smalltalk — never full :act tool loop.
|
|
1331
1345
|
# Anchored short forms only so "hi, please scan X" stays :act/:recon_act.
|
|
@@ -1393,6 +1407,14 @@ module PWN
|
|
|
1393
1407
|
return :recall unless doing
|
|
1394
1408
|
end
|
|
1395
1409
|
|
|
1410
|
+
if req.match?(LAST_SESSION_RX) && !req.match?(HOWTO_RX) && !req.match?(LIVE_RECON_RX)
|
|
1411
|
+
doing = req.match?(
|
|
1412
|
+
/\b(implement|fix|patch|refactor|run|execute|scan|write|edit|
|
|
1413
|
+
change|deploy|install|build|compile|commit|push)\b/ix
|
|
1414
|
+
)
|
|
1415
|
+
return :recall unless doing
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1396
1418
|
# Live-action recon takes precedence over bare "how to" when both appear
|
|
1397
1419
|
# only if the user clearly asks the agent to do the sweep here.
|
|
1398
1420
|
live = req.match?(LIVE_RECON_RX) && req.match?(
|
|
@@ -1715,6 +1737,18 @@ module PWN
|
|
|
1715
1737
|
session_id = opts[:session_id]
|
|
1716
1738
|
system_role_content = opts[:system_role_content].to_s
|
|
1717
1739
|
target = recall_target(request: request)
|
|
1740
|
+
last_session = request.match?(LAST_SESSION_RX)
|
|
1741
|
+
if last_session && defined?(PWN::Sessions) && PWN::Sessions.respond_to?(:previous_id)
|
|
1742
|
+
prev = PWN::Sessions.previous_id(exclude_session_id: session_id)
|
|
1743
|
+
if prev.to_s.empty?
|
|
1744
|
+
txt = 'I do not have a previous session transcript yet.'
|
|
1745
|
+
append_session(session_id: opts[:session_id], role: 'user', content: request)
|
|
1746
|
+
append_session(session_id: opts[:session_id], role: 'assistant', content: txt)
|
|
1747
|
+
return txt
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
session_id = prev
|
|
1751
|
+
end
|
|
1718
1752
|
|
|
1719
1753
|
prior_user = nil
|
|
1720
1754
|
prior_asst = nil
|
|
@@ -1757,7 +1791,7 @@ module PWN
|
|
|
1757
1791
|
# User-target / fallback: skip meta intermediate recall asks so
|
|
1758
1792
|
# "what did I just say?" after a nested chain still surfaces the
|
|
1759
1793
|
# original utterance when appropriate; default stays newest.
|
|
1760
|
-
skip_meta = target == :assistant
|
|
1794
|
+
skip_meta = target == :assistant || last_session
|
|
1761
1795
|
prior_user = PWN::Memory.prior_user_message(
|
|
1762
1796
|
session_id: session_id,
|
|
1763
1797
|
max_chars: 4_000,
|
data/lib/pwn/ai/agent/policy.rb
CHANGED
|
@@ -580,10 +580,9 @@ module PWN
|
|
|
580
580
|
end
|
|
581
581
|
|
|
582
582
|
ev = evaluate(limit: opts[:limit] || 40)
|
|
583
|
-
fallback =
|
|
583
|
+
fallback = PWN::AI::Agent::Registry::DEFAULT_PREFERENCE
|
|
584
584
|
pref = begin
|
|
585
|
-
|
|
586
|
-
PWN::AI::Agent::Registry.preference_order(intent: intent)
|
|
585
|
+
PWN::AI::Agent::Registry.preference_order
|
|
587
586
|
rescue StandardError
|
|
588
587
|
[]
|
|
589
588
|
end
|
|
@@ -63,16 +63,14 @@ module PWN
|
|
|
63
63
|
|
|
64
64
|
# Heredoc (not a "..." literal): an unescaped "..." inside a
|
|
65
65
|
# double-quoted string is parsed as Range (begin..."...end).
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# known-fix. Expand MEMORY/SKILLS/LEARNING/METRICS/POLICY/EXTRO
|
|
70
|
-
# only when the turn is stuck or the operator asked for the full harness.
|
|
66
|
+
# Mid-turn: current session + memory + skills + learning + known-fix,
|
|
67
|
+
# then tools (memory_recall / session_recall / skills_recall / pwn_eval / shell).
|
|
68
|
+
# Expand METRICS/POLICY/EXTRO when stuck or expand_harness.
|
|
71
69
|
expand = opts[:expand_harness] == true || opts[:stuck] == true
|
|
72
70
|
harness = if expand
|
|
73
|
-
"#{skills_block}#{
|
|
71
|
+
"#{skills_block}#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{memory_block(limit: b[:memory], request: request)}#{learning_block(limit: b[:learning])}#{mistakes_block(limit: b[:mistakes], request: request)}#{metrics_block(limit: b[:metrics], engine: engine)}#{policy_block if b[:policy].to_i.positive?}#{extrospection_block if b[:extro]}"
|
|
74
72
|
else
|
|
75
|
-
"#{recent_turns_block(session_id: session_id, request: request, limit:
|
|
73
|
+
"#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{memory_block(limit: b[:memory], request: request)}#{skills_block}#{learning_block(limit: b[:learning])}#{mistakes_block(limit: b[:mistakes], request: request)}"
|
|
76
74
|
end
|
|
77
75
|
<<~PROMPT
|
|
78
76
|
#{base}
|
|
@@ -92,8 +90,11 @@ module PWN
|
|
|
92
90
|
"I will run…", "one more thing…") — that is treated as an incomplete
|
|
93
91
|
reply. Emit a real tool_call instead, or a complete final answer
|
|
94
92
|
with evidence. A reply with no tool_calls is your FINAL answer to the user.
|
|
95
|
-
Prefer
|
|
96
|
-
|
|
93
|
+
Prefer this order: use RECENT TURNS (current session already in
|
|
94
|
+
context), then `memory_recall`, then `session_recall`, then
|
|
95
|
+
`skills_recall`, then `pwn_eval` for PWN:: work, then `shell` for OS
|
|
96
|
+
commands. Save durable facts with `memory_remember`.
|
|
97
|
+
TransparentBrowser: open once, reuse browser_obj, close once.
|
|
97
98
|
|
|
98
99
|
AUTONOMY
|
|
99
100
|
Multi-step goals must be finished in one Loop.run. Keep calling
|
|
@@ -140,13 +141,13 @@ module PWN
|
|
|
140
141
|
metrics: (b[:metrics] || (local ? 3 : 8)).to_i,
|
|
141
142
|
mistakes: (b[:mistakes] || (local ? 3 : 6)).to_i,
|
|
142
143
|
learning: (b[:learning] || (local ? 2 : 5)).to_i,
|
|
143
|
-
# Always inject
|
|
144
|
-
recent_turns: (b[:recent_turns] || (local ?
|
|
144
|
+
# Always inject prior user/assistant pairs from this session.
|
|
145
|
+
recent_turns: (b[:recent_turns] || (local ? 3 : 6)).to_i,
|
|
145
146
|
policy: (b[:policy] || 1).to_i,
|
|
146
147
|
extro: b[:extro].nil? ? !local : b[:extro]
|
|
147
148
|
}
|
|
148
149
|
rescue StandardError
|
|
149
|
-
{ memory: 25, metrics: 8, mistakes: 6, learning: 5, recent_turns:
|
|
150
|
+
{ memory: 25, metrics: 8, mistakes: 6, learning: 5, recent_turns: 6, policy: 1, extro: true }
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
private_class_method def self.active_engine
|
|
@@ -40,17 +40,14 @@ module PWN
|
|
|
40
40
|
keyword_init: true
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
CORE_TOOLS = %w[
|
|
44
|
-
mistakes_record mistakes_resolve learning_note_outcome].freeze
|
|
43
|
+
CORE_TOOLS = %w[memory_recall session_recall skills_recall pwn_eval shell
|
|
44
|
+
mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
46
|
+
# Schema order = CORE_TOOLS. Current session is injected (RECENT TURNS).
|
|
47
|
+
# Then memory / prior sessions / skills, then pwn_eval before shell.
|
|
48
48
|
# opts[:preference], or PWN::Env[:ai][:agent][:tool_preference].
|
|
49
49
|
# Explicit nil/empty order disables preference (no Env/DEFAULT fallback).
|
|
50
|
-
|
|
51
|
-
# with shell / pwn_eval. Names stay inside CORE_TOOLS.
|
|
52
|
-
DEFAULT_PREFERENCE = %w[memory_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
|
|
53
|
-
ACT_PREFERENCE = %w[shell pwn_eval memory_recall mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
|
|
50
|
+
DEFAULT_PREFERENCE = CORE_TOOLS
|
|
54
51
|
|
|
55
52
|
@entries = {}
|
|
56
53
|
@discovered = false
|
|
@@ -156,7 +153,7 @@ module PWN
|
|
|
156
153
|
raw = PWN::Env.dig(:ai, :agent, :tool_preference) if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
157
154
|
return Array(raw).map(&:to_s).reject(&:empty?) unless raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
158
155
|
|
|
159
|
-
|
|
156
|
+
DEFAULT_PREFERENCE.dup
|
|
160
157
|
rescue StandardError
|
|
161
158
|
DEFAULT_PREFERENCE.dup
|
|
162
159
|
end
|
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
require 'pwn/ai/agent/registry'
|
|
4
4
|
require 'pwn/sessions'
|
|
5
5
|
|
|
6
|
+
PWN::AI::Agent::Registry.register(
|
|
7
|
+
name: 'session_recall',
|
|
8
|
+
toolset: 'sessions',
|
|
9
|
+
schema: {
|
|
10
|
+
name: 'session_recall',
|
|
11
|
+
description: 'Search previous pwn-ai session transcripts (~/.pwn/sessions) ' \
|
|
12
|
+
'for how a similar request was solved. Current session is ' \
|
|
13
|
+
'already in RECENT TURNS — this is for older sessions. ' \
|
|
14
|
+
'Call before pwn_eval / shell when prior method matters.',
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
query: { type: 'string', description: 'Keywords to match in prior user/assistant turns.' },
|
|
19
|
+
limit: { type: 'integer', default: 12, description: 'Max hits (newest / highest score first).' },
|
|
20
|
+
include_current: { type: 'boolean', default: false, description: 'Also search the active session.' }
|
|
21
|
+
},
|
|
22
|
+
required: []
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
check: -> { defined?(PWN::Sessions) },
|
|
26
|
+
handler: lambda { |args|
|
|
27
|
+
exclude = ''
|
|
28
|
+
exclude = PWN::Memory.current_session_id if defined?(PWN::Memory) && PWN::Memory.respond_to?(:current_session_id)
|
|
29
|
+
PWN::Sessions.recall(
|
|
30
|
+
query: args[:query],
|
|
31
|
+
limit: args[:limit] || 12,
|
|
32
|
+
exclude_session_id: exclude,
|
|
33
|
+
include_current: args[:include_current] == true
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
6
38
|
# Thin wrappers around PWN::Sessions so the model can DISCOVER and INSPECT
|
|
7
39
|
# the JSONL transcripts that learning_reflect / learning_distill_skill
|
|
8
40
|
# consume. Without these the model has to blindly `shell("ls ~/.pwn/sessions")`
|
|
@@ -22,6 +22,62 @@ require 'pwn/ai/agent/registry'
|
|
|
22
22
|
# and/or a "## References" markdown section — mirroring the
|
|
23
23
|
# PWN::SAST::*.security_references convention.
|
|
24
24
|
|
|
25
|
+
PWN::AI::Agent::Registry.register(
|
|
26
|
+
name: 'skills_recall',
|
|
27
|
+
toolset: 'skills',
|
|
28
|
+
schema: {
|
|
29
|
+
name: 'skills_recall',
|
|
30
|
+
description: 'Search installed pwn-ai skills (name, description, body, ' \
|
|
31
|
+
'references) for how to do similar work. Call before ' \
|
|
32
|
+
'pwn_eval / shell when a reusable procedure may exist. ' \
|
|
33
|
+
'The SKILLS index is already in the prompt — this loads ' \
|
|
34
|
+
'matching full methods.',
|
|
35
|
+
parameters: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
query: { type: 'string', description: 'Keywords to match against skill name / description / body.' },
|
|
39
|
+
limit: { type: 'integer', default: 6, description: 'Max hits (highest score first).' }
|
|
40
|
+
},
|
|
41
|
+
required: []
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
check: -> { true },
|
|
45
|
+
handler: lambda { |args|
|
|
46
|
+
return [] unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
query = args[:query].to_s.downcase
|
|
49
|
+
tokens = query.split(/\W+/).reject { |t| t.length < 3 }
|
|
50
|
+
limit = (args[:limit] || 6).to_i
|
|
51
|
+
limit = 6 if limit <= 0
|
|
52
|
+
trunc = 400
|
|
53
|
+
hits = []
|
|
54
|
+
PWN::Skills.each do |name, meta|
|
|
55
|
+
next unless meta.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
blob = [
|
|
58
|
+
name.to_s,
|
|
59
|
+
meta[:description],
|
|
60
|
+
meta[:content],
|
|
61
|
+
Array(meta[:references]).join(' ')
|
|
62
|
+
].join("\n").downcase
|
|
63
|
+
score = 0
|
|
64
|
+
score += 5 if !query.empty? && blob.include?(query)
|
|
65
|
+
tokens.each { |tok| score += 1 if blob.include?(tok) }
|
|
66
|
+
score = 1 if query.empty?
|
|
67
|
+
next if score <= 0
|
|
68
|
+
|
|
69
|
+
body = meta[:content].to_s
|
|
70
|
+
hits << {
|
|
71
|
+
name: name.to_s,
|
|
72
|
+
description: meta[:description].to_s,
|
|
73
|
+
score: score,
|
|
74
|
+
snippet: body.length > trunc ? "#{body[0, trunc]}…[truncated]" : body
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
hits.sort_by { |h| -h[:score].to_i }.first(limit)
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
25
81
|
PWN::AI::Agent::Registry.register(
|
|
26
82
|
name: 'skill_list',
|
|
27
83
|
toolset: 'skills',
|
data/lib/pwn/ai/anthropic.rb
CHANGED
data/lib/pwn/ai/gemini.rb
CHANGED
data/lib/pwn/ai/grok.rb
CHANGED
data/lib/pwn/ai/ollama.rb
CHANGED
data/lib/pwn/ai/open_ai.rb
CHANGED
data/lib/pwn/ai/open_web_ui.rb
CHANGED
data/lib/pwn/config.rb
CHANGED
|
@@ -160,7 +160,7 @@ module PWN
|
|
|
160
160
|
# LLM tangible-task decomposition for autonomous goals (default on).
|
|
161
161
|
task_summary_llm: nil,
|
|
162
162
|
tool_router: nil, # nil = auto (true when :active is local :ollama/:openwebui) — cuts ~11k→~3k schema tokens
|
|
163
|
-
tool_preference: %w[memory_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember],
|
|
163
|
+
tool_preference: %w[memory_recall session_recall skills_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember],
|
|
164
164
|
escalation_persona: 'escalator', # Swarm persona for frontier corrective hints when a local model is stuck
|
|
165
165
|
# sample E3 verify_as_reward: true|false|nil(auto: ~10% local / always frontier when CLAIM_RX hits)
|
|
166
166
|
verify_as_reward: nil,
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'curses'
|
|
4
4
|
require 'pry'
|
|
5
5
|
require 'reline'
|
|
6
|
+
require 'tty-cursor'
|
|
6
7
|
require 'tty-prompt'
|
|
7
8
|
require 'unicode/display_width'
|
|
8
9
|
require 'yaml'
|
|
@@ -240,6 +241,7 @@ module PWN
|
|
|
240
241
|
end
|
|
241
242
|
|
|
242
243
|
def readline(prompt)
|
|
244
|
+
PWN::Plugins::REPL.ready_tty!
|
|
243
245
|
# Ask the terminal to encode Shift+Enter distinctly from Enter for
|
|
244
246
|
# the duration of this read. Without this, most emulators send 0x0D
|
|
245
247
|
# for both and SHIFT_ENTER_SEQS can never match. Reset in `ensure`.
|
|
@@ -280,6 +282,40 @@ module PWN
|
|
|
280
282
|
end
|
|
281
283
|
end
|
|
282
284
|
|
|
285
|
+
# Restore the TTY after a spinner / agent turn so Pry/Reline prints
|
|
286
|
+
# the next PS1 immediately. hide_cursor + a background worker leave
|
|
287
|
+
# the cursor hidden on $stdout (Reline's stream) even after
|
|
288
|
+
# TTY::Spinner#stop writes show-cursor to $stderr. Reline then
|
|
289
|
+
# waits for a key without redrawing the prompt.
|
|
290
|
+
public_class_method def self.ready_tty!(opts = {})
|
|
291
|
+
return nil if opts[:skip]
|
|
292
|
+
|
|
293
|
+
out = opts[:io] || $stdout
|
|
294
|
+
return nil unless out.respond_to?(:write)
|
|
295
|
+
|
|
296
|
+
out.write("\e[0m#{TTY::Cursor.show}") if defined?(TTY::Cursor)
|
|
297
|
+
out.write("\e[0m\e[?25h") unless defined?(TTY::Cursor)
|
|
298
|
+
out.flush if out.respond_to?(:flush)
|
|
299
|
+
reset_reline_editor
|
|
300
|
+
nil
|
|
301
|
+
rescue StandardError
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
private_class_method def self.reset_reline_editor(opts = {})
|
|
306
|
+
return unless opts.is_a?(Hash)
|
|
307
|
+
return unless defined?(Reline)
|
|
308
|
+
return unless Reline.respond_to?(:core)
|
|
309
|
+
|
|
310
|
+
editor = Reline.core.instance_variable_get(:@line_editor)
|
|
311
|
+
return unless editor
|
|
312
|
+
|
|
313
|
+
editor.instance_variable_set(:@finished, false) if editor.instance_variable_defined?(:@finished)
|
|
314
|
+
nil
|
|
315
|
+
rescue StandardError
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
283
319
|
# Compact token-count formatter for the pwn.ai PS1 (e.g. 0, 843, 12K, 250K, 1M).
|
|
284
320
|
public_class_method def self.compact_context_tokens(opts = {})
|
|
285
321
|
n = opts[:tokens].to_i
|
|
@@ -1182,6 +1218,7 @@ module PWN
|
|
|
1182
1218
|
puts "\n\001\e[32m\002#{final}\001\e[0m\002\n\n"
|
|
1183
1219
|
pp PWN::Sessions.load(session_id: sess_id) if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
|
|
1184
1220
|
request.replace('nil')
|
|
1221
|
+
PWN::Plugins::REPL.ready_tty!
|
|
1185
1222
|
next
|
|
1186
1223
|
rescue StandardError => e
|
|
1187
1224
|
warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message}\n#{e.backtrace}); " \
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tty-spinner'
|
|
4
|
+
require 'tty-cursor'
|
|
4
5
|
|
|
5
6
|
module PWN
|
|
6
7
|
module Plugins
|
|
@@ -10,10 +11,7 @@ module PWN
|
|
|
10
11
|
# worker but does NOT join it. The worker is typically mid-sleep
|
|
11
12
|
# on its interval, so it stays alive long enough to write another
|
|
12
13
|
# frame (cursor col 1 + glyph) over the HTTP response that the
|
|
13
|
-
# caller is about to print.
|
|
14
|
-
# (the gem default) also reprints the last glyph + newline on
|
|
15
|
-
# stop, so the spinner appears to keep running whenever a
|
|
16
|
-
# response is provided.
|
|
14
|
+
# caller is about to print.
|
|
17
15
|
#
|
|
18
16
|
# #start uses clear:true + hide_cursor and owns the worker
|
|
19
17
|
# (spin.start + Thread.new { spin.spin until spin.done? }).
|
|
@@ -37,7 +35,18 @@ module PWN
|
|
|
37
35
|
}
|
|
38
36
|
args[:output] = opts[:output] unless opts[:output].nil?
|
|
39
37
|
|
|
40
|
-
TTY::Spinner.new(**args)
|
|
38
|
+
spin = TTY::Spinner.new(**args)
|
|
39
|
+
spin.start
|
|
40
|
+
interval = 1.0 / [spin.interval.to_f, 1.0].max
|
|
41
|
+
worker = Thread.new do
|
|
42
|
+
Thread.current.report_on_exception = false
|
|
43
|
+
until spin.done?
|
|
44
|
+
spin.spin
|
|
45
|
+
sleep(interval)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
spin.define_singleton_method(:pwn_worker_thread) { worker }
|
|
49
|
+
spin
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
# Supported Method Parameters::
|
|
@@ -49,7 +58,47 @@ module PWN
|
|
|
49
58
|
spin = opts.is_a?(Hash) ? opts[:spin] : opts
|
|
50
59
|
return if spin.nil?
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
worker = spin.respond_to?(:pwn_worker_thread) ? spin.pwn_worker_thread : nil
|
|
62
|
+
begin
|
|
63
|
+
spin.stop if spin.respond_to?(:stop)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
spin.kill if spin.respond_to?(:kill)
|
|
66
|
+
end
|
|
67
|
+
join_thread(thread: worker)
|
|
68
|
+
join_spinner(spin: spin)
|
|
69
|
+
begin
|
|
70
|
+
spin.clear_line if spin.respond_to?(:clear_line)
|
|
71
|
+
show = defined?(TTY::Cursor) ? TTY::Cursor.show : "\e[?25h"
|
|
72
|
+
out = spin.output if spin.respond_to?(:output)
|
|
73
|
+
out.print(show) if out.respond_to?(:print)
|
|
74
|
+
$stdout.write(show) if $stdout.respond_to?(:write)
|
|
75
|
+
$stdout.flush if $stdout.respond_to?(:flush)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private_class_method def self.join_thread(opts = {})
|
|
83
|
+
thread = opts[:thread]
|
|
84
|
+
return unless thread.is_a?(Thread)
|
|
85
|
+
return unless thread.alive?
|
|
86
|
+
|
|
87
|
+
unless thread.join(JOIN_SECS)
|
|
88
|
+
thread.kill
|
|
89
|
+
thread.join(JOIN_SECS)
|
|
90
|
+
end
|
|
91
|
+
rescue StandardError
|
|
92
|
+
thread.kill if thread.respond_to?(:kill)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private_class_method def self.join_spinner(opts = {})
|
|
96
|
+
spin = opts[:spin]
|
|
97
|
+
return unless spin.respond_to?(:join)
|
|
98
|
+
|
|
99
|
+
spin.join(JOIN_SECS)
|
|
100
|
+
rescue TTY::Spinner::NotSpinningError, StandardError
|
|
101
|
+
nil
|
|
53
102
|
end
|
|
54
103
|
|
|
55
104
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
data/lib/pwn/sessions.rb
CHANGED
|
@@ -119,6 +119,86 @@ module PWN
|
|
|
119
119
|
File.readlines(path).map { |l| JSON.parse(l, symbolize_names: true) }
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# Newest session that is not exclude_session_id (the prior pwn-ai run).
|
|
123
|
+
#
|
|
124
|
+
# Supported Method Parameters::
|
|
125
|
+
# id = PWN::Sessions.previous_id(
|
|
126
|
+
# exclude_session_id: 'optional - current session to skip'
|
|
127
|
+
# )
|
|
128
|
+
public_class_method def self.previous_id(opts = {})
|
|
129
|
+
exclude = opts[:exclude_session_id].to_s
|
|
130
|
+
rows = list.sort_by { |s| [s[:mtime].to_s, s[:id].to_s] }.reverse
|
|
131
|
+
hit = rows.find { |s| s[:id].to_s != exclude }
|
|
132
|
+
hit && hit[:id].to_s
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Search prior session transcripts for query terms. Current session is
|
|
138
|
+
# excluded by default — that tail is already in RECENT TURNS.
|
|
139
|
+
#
|
|
140
|
+
# Supported Method Parameters::
|
|
141
|
+
# hits = PWN::Sessions.recall(
|
|
142
|
+
# query: 'optional - substring / tokens to match',
|
|
143
|
+
# exclude_session_id: 'optional - skip this id (current session)',
|
|
144
|
+
# include_current: 'optional - Boolean include excluded id (default false)',
|
|
145
|
+
# limit: 'optional - max hits (default 12)',
|
|
146
|
+
# max_files: 'optional - newest files to scan (default 40)',
|
|
147
|
+
# truncate: 'optional - chars per hit (default 280)'
|
|
148
|
+
# )
|
|
149
|
+
public_class_method def self.recall(opts = {})
|
|
150
|
+
query = opts[:query].to_s.downcase
|
|
151
|
+
tokens = query.split(/\W+/).reject { |t| t.length < 3 }
|
|
152
|
+
limit = (opts[:limit] || 12).to_i
|
|
153
|
+
limit = 12 if limit <= 0
|
|
154
|
+
max_files = (opts[:max_files] || 40).to_i
|
|
155
|
+
max_files = 40 if max_files <= 0
|
|
156
|
+
trunc = (opts[:truncate] || 280).to_i
|
|
157
|
+
trunc = 280 if trunc <= 0
|
|
158
|
+
exclude = opts[:exclude_session_id].to_s
|
|
159
|
+
include_current = opts[:include_current] == true
|
|
160
|
+
|
|
161
|
+
hits = []
|
|
162
|
+
list.sort_by { |s| s[:mtime].to_s }.reverse.first(max_files).each do |sess|
|
|
163
|
+
sid = sess[:id].to_s
|
|
164
|
+
next if !include_current && !exclude.empty? && sid == exclude
|
|
165
|
+
|
|
166
|
+
load(session_id: sid).each do |row|
|
|
167
|
+
next unless row.is_a?(Hash)
|
|
168
|
+
next unless %w[user assistant].include?(row[:role].to_s)
|
|
169
|
+
|
|
170
|
+
body = row[:content].to_s
|
|
171
|
+
next if body.strip.empty?
|
|
172
|
+
|
|
173
|
+
score = recall_score(body: body.downcase, query: query, tokens: tokens)
|
|
174
|
+
next if score <= 0
|
|
175
|
+
|
|
176
|
+
hits << {
|
|
177
|
+
session_id: sid,
|
|
178
|
+
role: row[:role].to_s,
|
|
179
|
+
timestamp: row[:timestamp],
|
|
180
|
+
content: body.length > trunc ? "#{body[0, trunc]}…[truncated]" : body,
|
|
181
|
+
score: score
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
hits.sort_by { |h| [-h[:score].to_f, h[:timestamp].to_s] }.first(limit)
|
|
186
|
+
rescue StandardError
|
|
187
|
+
[]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private_class_method def self.recall_score(opts = {})
|
|
191
|
+
body = opts[:body].to_s
|
|
192
|
+
query = opts[:query].to_s
|
|
193
|
+
tokens = Array(opts[:tokens])
|
|
194
|
+
return 1 if query.empty?
|
|
195
|
+
|
|
196
|
+
score = 0
|
|
197
|
+
score += 5 if !query.empty? && body.include?(query)
|
|
198
|
+
tokens.each { |tok| score += 1 if body.include?(tok) }
|
|
199
|
+
score
|
|
200
|
+
end
|
|
201
|
+
|
|
122
202
|
# Supported Method Parameters::
|
|
123
203
|
# history_for_ai = PWN::Sessions.to_response_history(session_id:)
|
|
124
204
|
# (converts transcript to the response_history format used by PWN::AI::* .chat)
|
|
@@ -417,6 +497,8 @@ module PWN
|
|
|
417
497
|
PWN::Sessions.append(session_id: sess[:id], role: 'user', content: 'Run NmapIt...')
|
|
418
498
|
transcript = PWN::Sessions.load(session_id: sess[:id])
|
|
419
499
|
hist = PWN::Sessions.to_response_history(session_id: sess[:id])
|
|
500
|
+
PWN::Sessions.recall(query: 'hping3', exclude_session_id: sess[:id])
|
|
501
|
+
PWN::Sessions.previous_id(exclude_session_id: sess[:id])
|
|
420
502
|
PWN::Sessions.list
|
|
421
503
|
PWN::Sessions.stats
|
|
422
504
|
PWN::Sessions.delete(session_id: sess[:id], force: true)
|
data/lib/pwn/version.rb
CHANGED
|
@@ -29,14 +29,14 @@ RSpec.describe 'PWN::AI::Agent::PromptBuilder', :aggregate_failures do
|
|
|
29
29
|
PWN::Env[:ai] = { active: :anthropic, module_reflection: false, agent: @agent_cfg }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
it 'mid-turn
|
|
32
|
+
it 'mid-turn prompt injects MEMORY + RECENT TURNS + known-fix, not the parked harness' do
|
|
33
33
|
prompt = builder.build(session_id: 'sess_abc', request: 'Write hello into /tmp/x and verify it')
|
|
34
|
-
['ENVIRONMENT', 'KNOWN MISTAKES', 'TOOL USE'].each do |hdr|
|
|
34
|
+
['ENVIRONMENT', 'MEMORY', 'SKILLS', 'LEARNING', 'KNOWN MISTAKES', 'TOOL USE'].each do |hdr|
|
|
35
35
|
expect(prompt).to include(hdr), "missing section: #{hdr}"
|
|
36
36
|
end
|
|
37
37
|
expect(prompt).to include('session_id : sess_abc')
|
|
38
38
|
expect(prompt).to include(PWN::VERSION)
|
|
39
|
-
['
|
|
39
|
+
['TOOL EFFECTIVENESS', 'EXTROSPECTION', 'POLICY'].each do |hdr|
|
|
40
40
|
expect(prompt).not_to include(hdr), "mid-turn prompt still injects #{hdr}"
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -15,7 +15,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
15
15
|
expect(help_response).to respond_to :help
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
describe 'RL-adjacent loop contracts' do
|
|
18
|
+
describe 'RL-adjacent loop contracts' do # rubocop:disable Metrics/BlockLength
|
|
19
19
|
it 'exhaust path still calls task_summary_flush! and auto_introspect' do
|
|
20
20
|
src = File.read(described_class.method(:run).source_location.first)
|
|
21
21
|
# budget exhausted terminal path
|
|
@@ -316,6 +316,44 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
316
316
|
expect(described_class.needs_host_work?(request: 'Write hello into /tmp/x.txt')).to eq true
|
|
317
317
|
end
|
|
318
318
|
|
|
319
|
+
it 'does not treat https URLs as file paths; browser collect asks complete on URL evidence' do
|
|
320
|
+
req = <<~REQ.gsub(/\s+/, ' ').strip
|
|
321
|
+
Navigate to https://0dayinc.com using TransparentBrowser.open(browser_type: :chrome, devtools: true)
|
|
322
|
+
find all the blog posts ever made, and return a list of URLs.
|
|
323
|
+
Once complete close the browser via TransparentBrowser.close
|
|
324
|
+
REQ
|
|
325
|
+
expect(req.scan(described_class::HOST_PATH_RX)).to eq([])
|
|
326
|
+
expect(described_class.needs_host_work?(request: req)).to eq true
|
|
327
|
+
expect(described_class.world_knowledge?(request: req)).to eq false
|
|
328
|
+
expect(
|
|
329
|
+
described_class.send(
|
|
330
|
+
:request_unsatisfied?,
|
|
331
|
+
request: req,
|
|
332
|
+
messages: [{ role: 'assistant', content: 'opening chrome', tool_calls: [] }],
|
|
333
|
+
last_iter: false
|
|
334
|
+
)
|
|
335
|
+
).to eq true
|
|
336
|
+
collected = [
|
|
337
|
+
{
|
|
338
|
+
role: 'assistant',
|
|
339
|
+
tool_calls: [{ function: { name: 'pwn_eval', arguments: '{"code":"browser.goto"}' } }]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
role: 'tool',
|
|
343
|
+
name: 'pwn_eval',
|
|
344
|
+
content: '{"success":true,"result":["https://0dayinc.com/blog/one","https://0dayinc.com/blog/two"]}'
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
expect(
|
|
348
|
+
described_class.send(
|
|
349
|
+
:request_unsatisfied?,
|
|
350
|
+
request: req,
|
|
351
|
+
messages: collected,
|
|
352
|
+
last_iter: false
|
|
353
|
+
)
|
|
354
|
+
).to eq false
|
|
355
|
+
end
|
|
356
|
+
|
|
319
357
|
it 'last-iter strips tools only on the true last slot; leftover English tasks do not force required' do
|
|
320
358
|
src = File.read(described_class.method(:run).source_location.first)
|
|
321
359
|
expect(src).to match(/text_only_iters = 1/)
|
|
@@ -507,6 +545,30 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
507
545
|
FileUtils.rm_rf(sess_tmp) if sess_tmp
|
|
508
546
|
end
|
|
509
547
|
|
|
548
|
+
it 'answer_recall looks up the previous session file for last-session asks' do
|
|
549
|
+
sess_tmp = Dir.mktmpdir('pwn-sess-last')
|
|
550
|
+
stub_const('PWN::Sessions::SESSIONS_DIR', sess_tmp)
|
|
551
|
+
old = PWN::Sessions.create(id: '20200101_000000_oldone', title: 'old')
|
|
552
|
+
PWN::Sessions.append(session_id: old[:id], role: 'user', content: 'SHIP MARKER LAST SESSION')
|
|
553
|
+
PWN::Sessions.append(session_id: old[:id], role: 'assistant', content: 'ack old')
|
|
554
|
+
cur = PWN::Sessions.create(id: '20260101_000000_curone', title: 'current')
|
|
555
|
+
PWN::Sessions.append(session_id: cur[:id], role: 'user', content: 'what did I just say?')
|
|
556
|
+
PWN::Sessions.append(session_id: cur[:id], role: 'assistant', content: "You just said:\n\nwhat did I just say?")
|
|
557
|
+
allow(described_class).to receive(:should_auto_introspect?).and_return(false)
|
|
558
|
+
req = 'what did I just say in the last session?'
|
|
559
|
+
expect(described_class.request_intent(request: req)).to eq(:recall)
|
|
560
|
+
out = described_class.send(
|
|
561
|
+
:answer_recall,
|
|
562
|
+
request: req,
|
|
563
|
+
session_id: cur[:id],
|
|
564
|
+
system_role_content: 'test'
|
|
565
|
+
)
|
|
566
|
+
expect(out).to include('SHIP MARKER LAST SESSION')
|
|
567
|
+
expect(out).not_to match(/You just said:\s*\n\s*what did I just say\?/i)
|
|
568
|
+
ensure
|
|
569
|
+
FileUtils.rm_rf(sess_tmp) if sess_tmp
|
|
570
|
+
end
|
|
571
|
+
|
|
510
572
|
it 'answer_recall returns prior assistant text for how-did-you-respond without tools' do
|
|
511
573
|
sess_tmp = Dir.mktmpdir('pwn-sess-recall-asst')
|
|
512
574
|
stub_const('PWN::Sessions::SESSIONS_DIR', sess_tmp)
|
|
@@ -64,11 +64,15 @@ describe PWN::AI::Agent::PromptBuilder do
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
describe 'mid-turn
|
|
68
|
-
it '
|
|
67
|
+
describe 'mid-turn prompt' do
|
|
68
|
+
it 'injects current session + MEMORY + SKILLS + LEARNING + known-fix before tools' do
|
|
69
69
|
src = File.read(described_class.method(:build).source_location.first)
|
|
70
|
+
expect(src).to match(/recent_turns_block/)
|
|
71
|
+
expect(src).to match(/memory_block/)
|
|
72
|
+
expect(src).to match(/skills_block/)
|
|
73
|
+
expect(src).to match(/learning_block/)
|
|
70
74
|
expect(src).to match(/KNOWN MISTAKES|mistakes_block/)
|
|
71
|
-
expect(src).to match(/
|
|
75
|
+
expect(src).to match(/skills_recall/)
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
end
|
|
@@ -13,17 +13,24 @@ describe PWN::AI::Agent::Registry do
|
|
|
13
13
|
expect(help_response).to respond_to :help
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
it '
|
|
17
|
-
expect(described_class
|
|
18
|
-
|
|
16
|
+
it 'CORE_TOOLS is recall-then-act including skills_recall before pwn_eval' do
|
|
17
|
+
expect(described_class.const_defined?(:ACT_PREFERENCE)).to eq false
|
|
18
|
+
expect(described_class::CORE_TOOLS).to eq(
|
|
19
|
+
%w[memory_recall session_recall skills_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember]
|
|
19
20
|
)
|
|
20
|
-
expect(described_class::DEFAULT_PREFERENCE
|
|
21
|
+
expect(described_class::DEFAULT_PREFERENCE).to eq(described_class::CORE_TOOLS)
|
|
21
22
|
expect(described_class::DEFAULT_PREFERENCE).not_to include('sessions_view')
|
|
23
|
+
expect(described_class::CORE_TOOLS.index('memory_recall')).to be < described_class::CORE_TOOLS.index('session_recall')
|
|
24
|
+
expect(described_class::CORE_TOOLS.index('session_recall')).to be < described_class::CORE_TOOLS.index('skills_recall')
|
|
25
|
+
expect(described_class::CORE_TOOLS.index('skills_recall')).to be < described_class::CORE_TOOLS.index('pwn_eval')
|
|
26
|
+
expect(described_class::CORE_TOOLS.index('pwn_eval')).to be < described_class::CORE_TOOLS.index('shell')
|
|
22
27
|
end
|
|
23
28
|
|
|
24
|
-
it 'preference_order
|
|
25
|
-
expect(described_class.preference_order
|
|
26
|
-
expect(described_class.preference_order
|
|
29
|
+
it 'preference_order is DEFAULT_PREFERENCE — kind/intent do not change it' do
|
|
30
|
+
expect(described_class.preference_order).to eq(described_class::DEFAULT_PREFERENCE)
|
|
31
|
+
expect(described_class.preference_order.first(5)).to eq(%w[memory_recall session_recall skills_recall pwn_eval shell])
|
|
32
|
+
expect(described_class.preference_order(kind: :question)).to eq(described_class::DEFAULT_PREFERENCE)
|
|
33
|
+
expect(described_class.preference_order(intent: :recall)).to eq(described_class::DEFAULT_PREFERENCE)
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
it 'preference_order still honors explicit empty list and Env override' do
|
|
@@ -34,8 +41,8 @@ describe PWN::AI::Agent::Registry do
|
|
|
34
41
|
it 'definitions(core_only: true) ships CORE_TOOLS, not the full ~85 schema set' do
|
|
35
42
|
described_class.discover
|
|
36
43
|
names = described_class.definitions(core_only: true).map { |t| t.dig(:function, :name) }
|
|
37
|
-
expect(names).to
|
|
44
|
+
expect(names).to eq(described_class::CORE_TOOLS)
|
|
38
45
|
expect(names).not_to include('sessions_view')
|
|
39
|
-
expect(names.length).to
|
|
46
|
+
expect(names.length).to eq(described_class::CORE_TOOLS.length)
|
|
40
47
|
end
|
|
41
48
|
end
|
|
@@ -28,6 +28,11 @@ describe 'PWN::AI::Agent::Tools sessions' do
|
|
|
28
28
|
expect(PWN::AI::Agent::Registry.lookup(name: 'sessions_current')).not_to be_nil
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
it 'registers the session_recall tool' do
|
|
32
|
+
PWN::AI::Agent::Registry.discover(force: true)
|
|
33
|
+
expect(PWN::AI::Agent::Registry.lookup(name: 'session_recall')).not_to be_nil
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
it 'exposes the sessions toolset in the registry' do
|
|
32
37
|
PWN::AI::Agent::Registry.discover(force: true)
|
|
33
38
|
expect(PWN::AI::Agent::Registry.toolsets).to include('sessions')
|
|
@@ -10,6 +10,10 @@ describe 'PWN::AI::Agent::Tools skills' do
|
|
|
10
10
|
expect(PWN::AI::Agent::Registry.lookup(name: 'skill_list')).not_to be_nil
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
it 'registers the skills_recall tool' do
|
|
14
|
+
expect(PWN::AI::Agent::Registry.lookup(name: 'skills_recall')).not_to be_nil
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
it 'registers the skill_migrate_legacy tool' do
|
|
14
18
|
expect(PWN::AI::Agent::Registry.lookup(name: 'skill_migrate_legacy')).not_to be_nil
|
|
15
19
|
end
|
|
@@ -32,6 +36,18 @@ describe 'PWN::AI::Agent::Tools skills' do
|
|
|
32
36
|
let(:delete) { PWN::AI::Agent::Registry.lookup(name: 'skill_delete')[:handler] }
|
|
33
37
|
let(:list) { PWN::AI::Agent::Registry.lookup(name: 'skill_list')[:handler] }
|
|
34
38
|
let(:view) { PWN::AI::Agent::Registry.lookup(name: 'skill_view')[:handler] }
|
|
39
|
+
let(:recall) { PWN::AI::Agent::Registry.lookup(name: 'skills_recall')[:handler] }
|
|
40
|
+
|
|
41
|
+
it 'skills_recall ranks matching skills by query and skips non-matches' do
|
|
42
|
+
create.call(name: 'browser-dump', description: 'Dump links via TransparentBrowser.', content: "open once, dump_links, close once\n")
|
|
43
|
+
create.call(name: 'unrelated', description: 'Something else entirely.', content: "no overlap here\n")
|
|
44
|
+
hits = recall.call(query: 'TransparentBrowser dump_links', limit: 8)
|
|
45
|
+
expect(hits).to be_an(Array)
|
|
46
|
+
expect(hits).not_to be_empty
|
|
47
|
+
names = hits.map { |h| h[:name].to_s }
|
|
48
|
+
expect(names).to include('browser-dump')
|
|
49
|
+
expect(names).not_to include('unrelated')
|
|
50
|
+
end
|
|
35
51
|
|
|
36
52
|
it 'skill_create writes <name>/SKILL.md with required name+description frontmatter' do
|
|
37
53
|
out = create.call(name: 'Recon Quick_Scan', description: 'Fast host triage.', content: "# Recon\nnmap -T4 -F {t}\n", references: ['T1046'])
|
|
@@ -59,8 +59,13 @@ describe PWN::AI::RedTeam::TestCaseEngine do
|
|
|
59
59
|
described_class.define_singleton_method(:judge) { |_opts| 'EPSS 5%' }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
before do
|
|
63
|
+
described_class.class_variable_set(:@@logger, Logger.new(File::NULL))
|
|
64
|
+
end
|
|
65
|
+
|
|
62
66
|
after do
|
|
63
67
|
load File.expand_path('../../../../../lib/pwn/ai/red_team/test_case_engine.rb', __dir__)
|
|
68
|
+
described_class.class_variable_set(:@@logger, Logger.new(File::NULL))
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
it 'generates payload_count LLM payloads from strategies' do
|
|
@@ -85,6 +90,21 @@ describe PWN::AI::RedTeam::TestCaseEngine do
|
|
|
85
90
|
)
|
|
86
91
|
end
|
|
87
92
|
|
|
93
|
+
it 'does not print [INFO] completion banners to $stdout' do
|
|
94
|
+
apply_env_stub
|
|
95
|
+
apply_engine_stub({})
|
|
96
|
+
expect do
|
|
97
|
+
described_class.execute(
|
|
98
|
+
strategies: strategies,
|
|
99
|
+
payload_count: 1,
|
|
100
|
+
security_references: refs,
|
|
101
|
+
target_engine: :grok,
|
|
102
|
+
attacker_engine: :grok,
|
|
103
|
+
max_adaptive_rounds: 0
|
|
104
|
+
)
|
|
105
|
+
end.to output('').to_stdout
|
|
106
|
+
end
|
|
107
|
+
|
|
88
108
|
it 'defaults to ten generated payloads when payload_count is omitted' do
|
|
89
109
|
tracker = {}
|
|
90
110
|
apply_env_stub
|
|
@@ -24,4 +24,13 @@ describe PWN::Plugins::REPL do
|
|
|
24
24
|
expect(described_class.compact_context_tokens(tokens: 26_000)).to eq('26K')
|
|
25
25
|
expect(described_class.compact_context_tokens(tokens: 500_000)).to eq('500K')
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
it 'ready_tty! exists and the pwn-ai path resets the TTY before the next PS1' do
|
|
29
|
+
expect(described_class).to respond_to :ready_tty!
|
|
30
|
+
hook = File.read(described_class.method(:add_hooks).source_location.first)
|
|
31
|
+
expect(hook).to match(/ready_tty!/)
|
|
32
|
+
expect(hook).to match(/request\.replace\('nil'\)/)
|
|
33
|
+
reader = File.read(described_class.const_get(:PWNMultiLineInput).instance_method(:readline).source_location.first)
|
|
34
|
+
expect(reader).to match(/ready_tty!/)
|
|
35
|
+
end
|
|
27
36
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'stringio'
|
|
4
5
|
|
|
5
6
|
describe PWN::Plugins::TTYSpinner do
|
|
6
7
|
it 'should display information for authors' do
|
|
@@ -10,4 +11,33 @@ describe PWN::Plugins::TTYSpinner do
|
|
|
10
11
|
it 'should display information for existing help method' do
|
|
11
12
|
expect(described_class).to respond_to :help
|
|
12
13
|
end
|
|
14
|
+
|
|
15
|
+
it 'stop joins the auto_spin worker so no frames are written after the response' do
|
|
16
|
+
io = StringIO.new
|
|
17
|
+
spin = described_class.start(output: io, format: :dots)
|
|
18
|
+
spin.auto_spin
|
|
19
|
+
worker = spin.instance_variable_get(:@thread)
|
|
20
|
+
expect(worker).to be_a(Thread)
|
|
21
|
+
sleep 0.12
|
|
22
|
+
described_class.stop(spin: spin)
|
|
23
|
+
expect(worker.alive?).to eq false
|
|
24
|
+
after_stop = io.string.dup
|
|
25
|
+
sleep 0.2
|
|
26
|
+
expect(io.string).to eq(after_stop)
|
|
27
|
+
expect(spin.done?).to eq true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'stop is a no-op for nil and is idempotent' do
|
|
31
|
+
expect { described_class.stop(spin: nil) }.not_to raise_error
|
|
32
|
+
io = StringIO.new
|
|
33
|
+
spin = described_class.start(output: io)
|
|
34
|
+
described_class.stop(spin: spin)
|
|
35
|
+
expect { described_class.stop(spin: spin) }.not_to raise_error
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'stop shows the cursor on $stdout so the next PS1 is visible' do
|
|
39
|
+
src = File.read(described_class.method(:stop).source_location.first)
|
|
40
|
+
expect(src).to match(/Cursor\.show/)
|
|
41
|
+
expect(src).to match(/\$stdout/)
|
|
42
|
+
end
|
|
13
43
|
end
|
|
@@ -61,4 +61,33 @@ describe PWN::Sessions do
|
|
|
61
61
|
ensure
|
|
62
62
|
FileUtils.rm_rf(tmp) if tmp
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
it 'recall searches prior session transcripts and skips the current session' do
|
|
66
|
+
tmp = Dir.mktmpdir
|
|
67
|
+
stub_const('PWN::Sessions::SESSIONS_DIR', tmp)
|
|
68
|
+
old = PWN::Sessions.create(id: '20200101_000000_oldone', title: 'old')
|
|
69
|
+
PWN::Sessions.append(session_id: old[:id], role: 'user', content: 'use hping3 for a ping sweep')
|
|
70
|
+
PWN::Sessions.append(session_id: old[:id], role: 'assistant', content: 'Prefer pwn_eval then shell for that')
|
|
71
|
+
cur = PWN::Sessions.create(id: '20260101_000000_curone', title: 'current')
|
|
72
|
+
PWN::Sessions.append(session_id: cur[:id], role: 'user', content: 'unrelated current chatter')
|
|
73
|
+
hits = PWN::Sessions.recall(query: 'hping3', exclude_session_id: cur[:id], limit: 8)
|
|
74
|
+
expect(hits).to be_an(Array)
|
|
75
|
+
expect(hits).not_to be_empty
|
|
76
|
+
expect(hits.all? { |h| h[:session_id] != cur[:id] }).to eq true
|
|
77
|
+
expect(hits.any? { |h| h[:content].to_s.include?('hping3') }).to eq true
|
|
78
|
+
ensure
|
|
79
|
+
FileUtils.rm_rf(tmp) if tmp
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'previous_id returns the newest session that is not the current one' do
|
|
83
|
+
tmp = Dir.mktmpdir
|
|
84
|
+
stub_const('PWN::Sessions::SESSIONS_DIR', tmp)
|
|
85
|
+
old = PWN::Sessions.create(id: '20200101_000000_oldses', title: 'old')
|
|
86
|
+
PWN::Sessions.append(session_id: old[:id], role: 'user', content: 'SHIP MARKER LAST SESSION')
|
|
87
|
+
cur = PWN::Sessions.create(id: '20260101_000000_curses', title: 'current')
|
|
88
|
+
PWN::Sessions.append(session_id: cur[:id], role: 'user', content: 'what did I just say?')
|
|
89
|
+
expect(PWN::Sessions.previous_id(exclude_session_id: cur[:id])).to eq(old[:id])
|
|
90
|
+
ensure
|
|
91
|
+
FileUtils.rm_rf(tmp) if tmp
|
|
92
|
+
end
|
|
64
93
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -38,3 +38,9 @@ RSpec.configure do |config|
|
|
|
38
38
|
$stderr = original_stderr
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
# Logger.new($stdout) captures the IO at class load — the same stream RSpec
|
|
43
|
+
# uses for progress dots. Reassigning $stdout in before(:each) does not
|
|
44
|
+
# retarget that logger. Point the red-team engine at /dev/null so execute
|
|
45
|
+
# banners cannot leak into rake.
|
|
46
|
+
PWN::AI::RedTeam::TestCaseEngine.class_variable_set(:@@logger, Logger.new(File::NULL)) if defined?(PWN::AI::RedTeam::TestCaseEngine)
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -2043,7 +2043,9 @@
|
|
|
2043
2043
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.compact_context_tokens Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.compact_context_tokens`: "}]}
|
|
2044
2044
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.enable_autocomplete Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.enable_autocomplete`: Supported Method Parameters\n\nPWN::Plugins::REPL.enable_autocomplete(\n\nenabled: 'optional - Boolean (default true). false reverts to single-line cycling.'\n\n)\n\nIRB-style suggest-as-you-type for the pwn REPL.\n\nReplaces Pry’s default input (rb-readline — single-candidate TAB cycling) with Reline and turns on Reline.autocompletion, which renders a live dropdown of candidates below the cursor as you type (the same widget IRB uses). Pry already wires Reline.completion_proc → @pry.complete (Pry::InputCompleter) when input == Reline, so the menu is fed by the full Ruby/PWN object graph: constants (PWN::Plugins::Nm<TAB>), instance methods, local/global variables, and Pry slash-commands.\n\nNavigate with ↑/↓ or TAB, accept with → or ENTER, dismiss with ESC.\n\nScope: this drives the MAIN pwn REPL (Ruby). pwn-ai / pwn-asm swap to PWNMultiLineInput, which bypasses Pry’s Reline path on purpose (natural-language / opcode input — Ruby completion isn’t useful there); SHIFT+ENTER multi-line continues to work in those modes.\n"}]}
|
|
2045
2045
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.help Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.help`: "}]}
|
|
2046
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.ready_tty! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.ready_tty!`: "}]}
|
|
2046
2047
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.refresh_ps1_proc Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.refresh_ps1_proc`: "}]}
|
|
2048
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.reset_reline_editor Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.reset_reline_editor`: "}]}
|
|
2047
2049
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.start Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.start`: Supported Method Parameters\n\nPWN::Plugins::REPL.start\n"}]}
|
|
2048
2050
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.process Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.process`: "}]}
|
|
2049
2051
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::PWNMultiLineInput.new Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::PWNMultiLineInput.new`: "}]}
|
|
@@ -2115,6 +2117,8 @@
|
|
|
2115
2117
|
{"messages":[{"role":"user","content":"PWN::Plugins::Spider.help Usage"},{"role":"assistant","content":"`PWN::Plugins::Spider.help`: "}]}
|
|
2116
2118
|
{"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
2117
2119
|
{"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.help Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.help`: "}]}
|
|
2120
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.join_spinner Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.join_spinner`: "}]}
|
|
2121
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.join_thread Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.join_thread`: "}]}
|
|
2118
2122
|
{"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.start Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.start`: Supported Method Parameters\n\nspin = PWN::Plugins::TTYSpinner.start(\n\nformat: 'optional - TTY::Spinner format (defaults to :dots)',\noutput: 'optional - IO to draw on (defaults to $stderr)'\n\n)\n"}]}
|
|
2119
2123
|
{"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.stop Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.stop`: Supported Method Parameters\n\nPWN::Plugins::TTYSpinner.stop(\n\nspin: 'optional - TTY::Spinner from #start (no-op if nil)'\n\n)\n"}]}
|
|
2120
2124
|
{"messages":[{"role":"user","content":"PWN::Plugins::ThreadPool.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::ThreadPool.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
@@ -2700,6 +2704,8 @@
|
|
|
2700
2704
|
{"messages":[{"role":"user","content":"PWN::Sessions.list Usage"},{"role":"assistant","content":"`PWN::Sessions.list`: Supported Method Parameters\n\nsessions = PWN::Sessions.list\n"}]}
|
|
2701
2705
|
{"messages":[{"role":"user","content":"PWN::Sessions.load Usage"},{"role":"assistant","content":"`PWN::Sessions.load`: Supported Method Parameters\n\ntranscript = PWN::Sessions.load(session_id: ‘required’)\n"}]}
|
|
2702
2706
|
{"messages":[{"role":"user","content":"PWN::Sessions.protected_session_ids Usage"},{"role":"assistant","content":"`PWN::Sessions.protected_session_ids`: "}]}
|
|
2707
|
+
{"messages":[{"role":"user","content":"PWN::Sessions.recall Usage"},{"role":"assistant","content":"`PWN::Sessions.recall`: Supported Method Parameters\n\nhits = PWN::Sessions.recall(\n\nquery: 'optional - substring / tokens to match',\nexclude_session_id: 'optional - skip this id (current session)',\ninclude_current: 'optional - Boolean include excluded id (default false)',\nlimit: 'optional - max hits (default 12)',\nmax_files: 'optional - newest files to scan (default 40)',\ntruncate: 'optional - chars per hit (default 280)'\n\n)\n"}]}
|
|
2708
|
+
{"messages":[{"role":"user","content":"PWN::Sessions.recall_score Usage"},{"role":"assistant","content":"`PWN::Sessions.recall_score`: "}]}
|
|
2703
2709
|
{"messages":[{"role":"user","content":"PWN::Sessions.sessions_dir Usage"},{"role":"assistant","content":"`PWN::Sessions.sessions_dir`: Supported Method Parameters\n\ndir = PWN::Sessions.sessions_dir\n"}]}
|
|
2704
2710
|
{"messages":[{"role":"user","content":"PWN::Sessions.stats Usage"},{"role":"assistant","content":"`PWN::Sessions.stats`: Supported Method Parameters\n\nstats = PWN::Sessions.stats\n"}]}
|
|
2705
2711
|
{"messages":[{"role":"user","content":"PWN::Sessions.stub_session? Usage"},{"role":"assistant","content":"`PWN::Sessions.stub_session?`: "}]}
|