pwn 0.5.739 → 0.5.741
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/Gemfile +2 -2
- data/documentation/Agent-Tool-Registry.md +163 -6
- data/documentation/Diagrams.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Plugin-Degradation.md +6 -2
- data/documentation/Recon-Findings-API.md +33 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/etc/default_skills/pwn/ai/agent/confirmation/SKILL.md +49 -0
- data/etc/default_skills/pwn/ai/agent/result/SKILL.md +4 -1
- data/etc/default_skills/pwn/ai/agent/task_dag/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_goto/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nmap_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nuclei_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/radare2/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/router/SKILL.md +51 -0
- data/etc/default_skills/pwn/engagement/SKILL.md +2 -0
- data/etc/default_skills/pwn/ffi/capstone/SKILL.md +54 -0
- data/etc/default_skills/pwn/ffi/keystone/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +5 -1
- data/etc/default_skills/pwn/plugins/ai_sandbox/SKILL.md +52 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/assembly/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +8 -2
- data/etc/default_skills/pwn/plugins/httpx/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/method_catalog/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -1
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/rop/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/vault/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/SKILL.md +2 -0
- data/lib/pwn/ai/agent/confirmation.rb +163 -0
- data/lib/pwn/ai/agent/dispatch.rb +43 -5
- data/lib/pwn/ai/agent/loop.rb +45 -11
- data/lib/pwn/ai/agent/prompt_builder.rb +7 -4
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/result.rb +76 -6
- data/lib/pwn/ai/agent/task_dag.rb +220 -0
- data/lib/pwn/ai/agent/tool_guard.rb +166 -24
- data/lib/pwn/ai/agent/tools/artifacts.rb +35 -5
- data/lib/pwn/ai/agent/tools/binary_triage.rb +7 -3
- data/lib/pwn/ai/agent/tools/browser_goto.rb +32 -0
- data/lib/pwn/ai/agent/tools/debug_lane.rb +23 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +32 -12
- data/lib/pwn/ai/agent/tools/finding_record.rb +31 -5
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -21
- data/lib/pwn/ai/agent/tools/job_run.rb +30 -18
- data/lib/pwn/ai/agent/tools/loot_query.rb +44 -0
- data/lib/pwn/ai/agent/tools/nmap_scan.rb +32 -0
- data/lib/pwn/ai/agent/tools/nuclei_scan.rb +31 -0
- data/lib/pwn/ai/agent/tools/radare2.rb +55 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +19 -2
- data/lib/pwn/ai/agent/tools/shell.rb +22 -10
- data/lib/pwn/ai/agent/turn_finalizer.rb +46 -5
- data/lib/pwn/ai/agent.rb +3 -1
- data/lib/pwn/ai/cli.rb +26 -2
- data/lib/pwn/ai/context.rb +4 -2
- data/lib/pwn/ai/router.rb +196 -0
- data/lib/pwn/ai/tools/README.md +5 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +10 -0
- data/lib/pwn/engagement.rb +45 -1
- data/lib/pwn/ffi/capstone.rb +120 -0
- data/lib/pwn/ffi/keystone.rb +107 -0
- data/lib/pwn/ffi.rb +2 -0
- data/lib/pwn/migrate.rb +5 -1
- data/lib/pwn/plugins/aflplusplus.rb +238 -13
- data/lib/pwn/plugins/ai_sandbox.rb +240 -0
- data/lib/pwn/plugins/artifact_registry.rb +188 -22
- data/lib/pwn/plugins/assembly.rb +73 -2
- data/lib/pwn/plugins/basic_auth.rb +17 -4
- data/lib/pwn/plugins/binary_parser.rb +439 -38
- data/lib/pwn/plugins/exploit_dev.rb +83 -14
- data/lib/pwn/plugins/findings.rb +208 -42
- data/lib/pwn/plugins/gdbmi.rb +265 -38
- data/lib/pwn/plugins/httpx.rb +93 -0
- data/lib/pwn/plugins/jobs.rb +260 -102
- data/lib/pwn/plugins/method_catalog.rb +328 -0
- data/lib/pwn/plugins/nmap_it.rb +157 -26
- data/lib/pwn/plugins/nuclei.rb +122 -29
- data/lib/pwn/plugins/preflight_checker.rb +3 -2
- data/lib/pwn/plugins/process_tube.rb +51 -2
- data/lib/pwn/plugins/radare2.rb +120 -26
- data/lib/pwn/plugins/recon.rb +33 -0
- data/lib/pwn/plugins/repl/mesh.rb +203 -13
- data/lib/pwn/plugins/rop.rb +121 -31
- data/lib/pwn/plugins/transparent_browser.rb +212 -14
- data/lib/pwn/plugins/vault.rb +182 -24
- data/lib/pwn/plugins.rb +4 -0
- data/lib/pwn/reports/engagement.rb +8 -2
- data/lib/pwn/reports/html.rb +28 -9
- data/lib/pwn/reports/json.rb +2 -1
- data/lib/pwn/reports/markdown.rb +49 -12
- data/lib/pwn/reports/sarif.rb +17 -3
- data/lib/pwn/reports.rb +184 -34
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reports_spec.rb +93 -0
- data/spec/integration/result_condition_spec.rb +11 -8
- data/spec/lib/pwn/ai/agent/confirmation_spec.rb +59 -0
- data/spec/lib/pwn/ai/agent/dispatch_confirmation_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/dispatch_policy_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/result_spec.rb +84 -1
- data/spec/lib/pwn/ai/agent/task_dag_spec.rb +63 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/ai_sandbox_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/tools/browser_goto_spec.rb +57 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +42 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +46 -2
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/nmap_scan_spec.rb +26 -0
- data/spec/lib/pwn/ai/agent/tools/nuclei_scan_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/tools/pwn_eval_schema_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/tools/radare2_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +76 -0
- data/spec/lib/pwn/ai/cli_spec.rb +12 -1
- data/spec/lib/pwn/ai/router_spec.rb +57 -0
- data/spec/lib/pwn/ffi/capstone_spec.rb +33 -0
- data/spec/lib/pwn/ffi/keystone_spec.rb +33 -0
- data/spec/lib/pwn/migrate_spec.rb +30 -2
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +40 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +44 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +116 -1
- data/spec/lib/pwn/plugins/assembly_spec.rb +12 -0
- data/spec/lib/pwn/plugins/basic_auth_spec.rb +15 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +59 -2
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +44 -0
- data/spec/lib/pwn/plugins/findings_structured_spec.rb +196 -1
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +99 -1
- data/spec/lib/pwn/plugins/httpx_spec.rb +23 -0
- data/spec/lib/pwn/plugins/jobs_spec.rb +141 -0
- data/spec/lib/pwn/plugins/method_catalog_spec.rb +29 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +49 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +33 -1
- data/spec/lib/pwn/plugins/radare2_spec.rb +115 -0
- data/spec/lib/pwn/plugins/recon_spec.rb +15 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +107 -1
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
- data/spec/lib/pwn/plugins/rop_spec.rb +19 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +71 -0
- data/spec/lib/pwn/plugins/vault_spec.rb +41 -0
- data/spec/lib/pwn/reports/engagement_spec.rb +51 -0
- data/spec/lib/pwn/reports_spec.rb +196 -0
- data/spec/spec_helper.rb +3 -0
- data/third_party/pwn_rdoc.jsonl +230 -4
- metadata +47 -5
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module PWN
|
|
9
|
+
module AI
|
|
10
|
+
module Agent
|
|
11
|
+
# Per-engagement ACK for exploit/destructive tool calls (PWN-AI-005 tiers).
|
|
12
|
+
module Confirmation
|
|
13
|
+
TIERS = %w[read_only active_scan exploit destructive].freeze
|
|
14
|
+
DEFAULTS = { 'read_only' => 'auto', 'active_scan' => 'auto', 'exploit' => 'prompt', 'destructive' => 'prompt' }.freeze
|
|
15
|
+
AUTONOMOUS = { 'read_only' => 'auto', 'active_scan' => 'auto', 'exploit' => 'auto', 'destructive' => 'auto' }.freeze
|
|
16
|
+
|
|
17
|
+
public_class_method def self.required_bins
|
|
18
|
+
[]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Classify a tool call into a PWN-AI-005 side-effect tier.
|
|
22
|
+
public_class_method def self.tier(opts = {})
|
|
23
|
+
name = opts[:name].to_s
|
|
24
|
+
args = opts[:args]
|
|
25
|
+
args = {} unless args.is_a?(Hash)
|
|
26
|
+
tagged = (args[:side_effect] || args['side_effect']).to_s
|
|
27
|
+
return tagged if TIERS.include?(tagged)
|
|
28
|
+
return 'exploit' if name.match?(/exploit|gdb_run_to_crash|fuzz_campaign/)
|
|
29
|
+
return 'active_scan' if name.match?(/nmap_scan|nuclei_scan|sbom_scan|http_proxy/)
|
|
30
|
+
return payload_tier(payload: args[:command] || args['command']) if name == 'shell'
|
|
31
|
+
return payload_tier(payload: args[:code] || args['code']) if name == 'pwn_eval'
|
|
32
|
+
|
|
33
|
+
'read_only'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Return an ACK pause hash, or nil when the call may proceed.
|
|
37
|
+
public_class_method def self.gate(opts = {})
|
|
38
|
+
name = opts[:name].to_s
|
|
39
|
+
args = opts[:args]
|
|
40
|
+
args = {} unless args.is_a?(Hash)
|
|
41
|
+
klass = tier(name: name, args: args)
|
|
42
|
+
policy = load_policy(opts)
|
|
43
|
+
action = (policy[klass] || DEFAULTS[klass] || 'auto').to_s
|
|
44
|
+
return { success: false, error: "confirmation deny for #{klass}", code: 'ACK_DENY', tier: klass } if action == 'deny'
|
|
45
|
+
return nil unless action == 'prompt'
|
|
46
|
+
|
|
47
|
+
eng = engagement_id(opts.merge(args: args))
|
|
48
|
+
return nil if acked?(engagement_id: eng)
|
|
49
|
+
|
|
50
|
+
if truthy?(value: opts[:operator_ack] || args[:operator_ack] || args['operator_ack'])
|
|
51
|
+
persist_ack(engagement_id: eng)
|
|
52
|
+
return nil
|
|
53
|
+
end
|
|
54
|
+
{
|
|
55
|
+
success: false,
|
|
56
|
+
needs_ack: true,
|
|
57
|
+
code: 'ACK_REQUIRED',
|
|
58
|
+
tier: klass,
|
|
59
|
+
engagement_id: eng,
|
|
60
|
+
diff: intended_diff(name: name, args: args, tier: klass),
|
|
61
|
+
error: "operator ACK required for #{klass}-tier tool call"
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
public_class_method def self.authors
|
|
66
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
public_class_method def self.help
|
|
70
|
+
puts "USAGE:
|
|
71
|
+
# List host binaries this module expects to be installed.
|
|
72
|
+
#{self}.required_bins
|
|
73
|
+
|
|
74
|
+
# Classify a tool call into a PWN-AI-005 side-effect tier.
|
|
75
|
+
#{self}.tier(
|
|
76
|
+
name: 'required - registry tool name',
|
|
77
|
+
args: 'optional - Hash of tool arguments'
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Return an ACK pause hash, or nil when the call may proceed.
|
|
81
|
+
#{self}.gate(
|
|
82
|
+
name: 'required - registry tool name',
|
|
83
|
+
args: 'optional - Hash of tool arguments',
|
|
84
|
+
engagement_id: 'optional - engagement id used to cache the ACK',
|
|
85
|
+
operator_ack: 'optional - true records a one-time ACK for this engagement',
|
|
86
|
+
scope_path: 'optional - scope.yaml path; defaults to ~/.pwn/scope.yaml'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Print the AUTHOR(S) string for this module.
|
|
90
|
+
#{self}.authors
|
|
91
|
+
"
|
|
92
|
+
constants.sort
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private_class_method def self.payload_tier(opts = {})
|
|
96
|
+
kind = PWN::Plugins::AISandbox.classify(payload: opts[:payload].to_s)
|
|
97
|
+
case kind.to_s
|
|
98
|
+
when 'write' then 'destructive'
|
|
99
|
+
when 'network' then 'active_scan'
|
|
100
|
+
else 'read_only'
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private_class_method def self.engagement_id(opts = {})
|
|
105
|
+
args = opts[:args]
|
|
106
|
+
args = {} unless args.is_a?(Hash)
|
|
107
|
+
value = (opts[:engagement_id] || args[:engagement_id] || args['engagement_id'] || 'default').to_s
|
|
108
|
+
value = 'default' if value.empty?
|
|
109
|
+
raise ArgumentError, 'engagement_id must be a simple identifier' unless value.match?(/\A[a-zA-Z0-9_-]+\z/)
|
|
110
|
+
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private_class_method def self.load_policy(opts = {})
|
|
115
|
+
path = opts[:scope_path].to_s
|
|
116
|
+
path = File.join(Dir.home, '.pwn', 'scope.yaml') if path.empty?
|
|
117
|
+
return AUTONOMOUS unless File.file?(path)
|
|
118
|
+
|
|
119
|
+
doc = YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {}
|
|
120
|
+
return AUTONOMOUS unless doc.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
conf = doc['confirmation'] || doc[:confirmation] || {}
|
|
123
|
+
return AUTONOMOUS unless conf.is_a?(Hash) && !conf.empty?
|
|
124
|
+
|
|
125
|
+
DEFAULTS.merge(conf.transform_keys(&:to_s))
|
|
126
|
+
rescue StandardError
|
|
127
|
+
AUTONOMOUS
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private_class_method def self.ack_path(opts = {})
|
|
131
|
+
File.join(Dir.home, '.pwn', 'engagements', engagement_id(opts), 'acks.json')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private_class_method def self.acked?(opts = {})
|
|
135
|
+
path = ack_path(opts)
|
|
136
|
+
return false unless File.file?(path)
|
|
137
|
+
|
|
138
|
+
JSON.parse(File.read(path))['ack'] == true
|
|
139
|
+
rescue StandardError
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private_class_method def self.persist_ack(opts = {})
|
|
144
|
+
path = ack_path(opts)
|
|
145
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
146
|
+
File.write(path, JSON.generate(ack: true, at: Time.now.utc.iso8601))
|
|
147
|
+
File.chmod(0o600, path)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private_class_method def self.truthy?(opts = {})
|
|
151
|
+
[true, 'true', 1, '1'].include?(opts[:value])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private_class_method def self.intended_diff(opts = {})
|
|
155
|
+
name = opts[:name]
|
|
156
|
+
tier = opts[:tier]
|
|
157
|
+
args = JSON.generate(opts[:args] || {})
|
|
158
|
+
"+ tool: #{name}\n+ tier: #{tier}\n+ args: #{args}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -4,6 +4,7 @@ require 'json'
|
|
|
4
4
|
require 'digest'
|
|
5
5
|
require 'json_schemer'
|
|
6
6
|
require 'securerandom'
|
|
7
|
+
require 'base64'
|
|
7
8
|
require 'pwn/ai/agent/tool_guard'
|
|
8
9
|
require 'pwn/ai/agent/manifest'
|
|
9
10
|
|
|
@@ -57,10 +58,12 @@ module PWN
|
|
|
57
58
|
schema = { allOf: [schema, declaration['params']] } if declaration
|
|
58
59
|
if raw.nil?
|
|
59
60
|
required = Array(schema[:required] || schema['required']).map(&:to_s)
|
|
60
|
-
|
|
61
|
+
missing = required.reject { |key| ToolGuard.present?(value: args[key.to_sym] || args[key]) }
|
|
62
|
+
return schema_denial(type: 'required', error: "Supply required fields: #{missing.join(', ')}") unless missing.empty?
|
|
61
63
|
end
|
|
62
64
|
type_schema = drop_required(node: schema)
|
|
63
|
-
|
|
65
|
+
violation = JSONSchemer.schema(JSON.parse(JSON.generate(type_schema))).validate(JSON.parse(JSON.generate(args))).first
|
|
66
|
+
return schema_denial(violation.transform_keys(&:to_sym)) if violation
|
|
64
67
|
|
|
65
68
|
blob = args.inspect
|
|
66
69
|
if defined?(PWN::Plugins::Vault)
|
|
@@ -100,6 +103,15 @@ module PWN
|
|
|
100
103
|
code: 'CAP_DENY'
|
|
101
104
|
)
|
|
102
105
|
end
|
|
106
|
+
ack = Confirmation.gate(
|
|
107
|
+
name: entry.name,
|
|
108
|
+
args: args,
|
|
109
|
+
engagement_id: opts[:engagement_id] || args[:engagement_id] || args['engagement_id'],
|
|
110
|
+
operator_ack: opts[:operator_ack] || args[:operator_ack] || args['operator_ack'],
|
|
111
|
+
scope_path: opts[:scope_path]
|
|
112
|
+
)
|
|
113
|
+
return JSON.generate(ack) if ack
|
|
114
|
+
|
|
103
115
|
budget = prepare_budget(opts.merge(entry: entry, args: args))
|
|
104
116
|
return JSON.generate(budget[:denial]) if budget && budget[:denial]
|
|
105
117
|
|
|
@@ -111,6 +123,7 @@ module PWN
|
|
|
111
123
|
raise
|
|
112
124
|
end
|
|
113
125
|
telemetry = finish_budget(budget: budget, result: result, elapsed: Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
|
|
126
|
+
result = Result.page(value: result, entry: entry, session_id: opts[:session_id])
|
|
114
127
|
result = ToolGuard.quarantine_output(text: result) if defined?(ToolGuard) && result.is_a?(String) && ToolGuard.respond_to?(:quarantine_output)
|
|
115
128
|
note_taint(text: result)
|
|
116
129
|
if defined?(PWN::Plugins::Vault) && result.is_a?(String)
|
|
@@ -169,13 +182,32 @@ module PWN
|
|
|
169
182
|
nil
|
|
170
183
|
end
|
|
171
184
|
|
|
185
|
+
private_class_method def self.schema_denial(opts = {})
|
|
186
|
+
pointer = opts[:data_pointer].to_s
|
|
187
|
+
message = opts[:error].to_s
|
|
188
|
+
data = opts[:data]
|
|
189
|
+
# Schema failures identify a JSON field, not a span of command bytes.
|
|
190
|
+
# Do not echo whole argument objects (which may contain credentials).
|
|
191
|
+
token = data.to_s unless data.nil? || data.is_a?(Hash) || data.is_a?(Array)
|
|
192
|
+
denial = ToolGuard.invalid_payload(
|
|
193
|
+
code: 'SCHEMA_DENY', rule_id: "schema.#{opts[:type]}",
|
|
194
|
+
match: token, hint: message,
|
|
195
|
+
remedy: "Correct #{pointer.empty? ? 'the tool arguments' : pointer}: #{message}. Resubmit arguments matching the advertised JSON schema."
|
|
196
|
+
)
|
|
197
|
+
JSON.generate(denial.merge(success: false, data_pointer: pointer))
|
|
198
|
+
end
|
|
199
|
+
|
|
172
200
|
# Caller owns task lifecycle. Fallback is thread-local, never process-global.
|
|
173
201
|
private_class_method def self.prepare_budget(opts = {})
|
|
174
202
|
entry = opts[:entry]
|
|
203
|
+
args = opts[:args]
|
|
204
|
+
if entry.name == 'shell' && (args[:background] == true || (!args.key?(:timeout) && ToolGuard.auto_job?(payload: args[:command].to_s)))
|
|
205
|
+
args[:background] = true
|
|
206
|
+
return nil
|
|
207
|
+
end
|
|
175
208
|
schema = JSON.parse(JSON.generate(entry.schema))
|
|
176
209
|
return nil unless schema.dig('parameters', 'properties', 'timeout')
|
|
177
210
|
|
|
178
|
-
args = opts[:args]
|
|
179
211
|
ledger = opts[:budget_ledger] || (Thread.current[:pwn_dispatch_budget] ||= {})
|
|
180
212
|
chains = ledger[:chains] ||= {}
|
|
181
213
|
key = opts[:budget_key] || entry.name
|
|
@@ -496,7 +528,10 @@ module PWN
|
|
|
496
528
|
args = opts[:args]
|
|
497
529
|
args = JSON.parse(args, symbolize_names: true) if args.is_a?(String) && args.strip.start_with?('{')
|
|
498
530
|
case args
|
|
499
|
-
when Hash
|
|
531
|
+
when Hash
|
|
532
|
+
return Base64.strict_decode64((args[:data] || args['data']).to_s) if (args[:encoding] || args['encoding']).to_s == 'base64'
|
|
533
|
+
|
|
534
|
+
args.values.join(' ')
|
|
500
535
|
else args.to_s
|
|
501
536
|
end
|
|
502
537
|
rescue StandardError
|
|
@@ -573,8 +608,11 @@ module PWN
|
|
|
573
608
|
budget_key: 'optional - trusted approach identifier; defaults to tool name',
|
|
574
609
|
scope_policy: 'optional - trusted policy Hash',
|
|
575
610
|
scope_path: 'optional - trusted scope file path',
|
|
611
|
+
session_id: 'optional - session directory for oversized result artifacts',
|
|
576
612
|
audit_path: 'optional - trusted audit JSONL path',
|
|
577
|
-
approval_callback: 'optional - trusted callback for prompt risk gates'
|
|
613
|
+
approval_callback: 'optional - trusted callback for prompt risk gates',
|
|
614
|
+
engagement_id: 'optional - engagement id used to cache exploit/destructive ACK',
|
|
615
|
+
operator_ack: 'optional - true records a one-time ACK for this engagement'
|
|
578
616
|
)
|
|
579
617
|
|
|
580
618
|
# Run repair name and return its result
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -67,7 +67,7 @@ module PWN
|
|
|
67
67
|
public_class_method def self.evidence_satisfied?(opts = {})
|
|
68
68
|
messages = Array(opts[:messages] || opts[:trace])
|
|
69
69
|
text = opts[:text].to_s
|
|
70
|
-
return false if TurnFinalizer.
|
|
70
|
+
return false if TurnFinalizer.required_artifacts(request: opts[:request]).any? && completion_unmet(request: opts[:request], messages: messages).any?
|
|
71
71
|
|
|
72
72
|
if defined?(TurnFinalizer) && TurnFinalizer.respond_to?(:arbitrate)
|
|
73
73
|
row = TurnFinalizer.arbitrate(request: opts[:request].to_s, messages: messages, paths: [])
|
|
@@ -783,7 +783,7 @@ module PWN
|
|
|
783
783
|
end
|
|
784
784
|
|
|
785
785
|
private_class_method def self.declared_contract(opts = {})
|
|
786
|
-
literals = TurnFinalizer.
|
|
786
|
+
literals = TurnFinalizer.required_artifacts(request: opts[:request])
|
|
787
787
|
cached = Thread.current[:pwn_loop_deliverables]
|
|
788
788
|
contract = if cached.is_a?(Array) || cached.is_a?(Hash)
|
|
789
789
|
normalize_contract(raw: cached)
|
|
@@ -908,7 +908,7 @@ module PWN
|
|
|
908
908
|
end
|
|
909
909
|
|
|
910
910
|
private_class_method def self.may_finalize?(opts = {})
|
|
911
|
-
paths = TurnFinalizer.
|
|
911
|
+
paths = TurnFinalizer.required_artifacts(request: opts[:request])
|
|
912
912
|
if paths.any?
|
|
913
913
|
artifact = TurnFinalizer.arbitrate(request: opts[:request], paths: paths, messages: opts[:messages])
|
|
914
914
|
unless artifact[:complete]
|
|
@@ -1177,7 +1177,7 @@ module PWN
|
|
|
1177
1177
|
shape = :timeout
|
|
1178
1178
|
end
|
|
1179
1179
|
m = Mistakes.record(tool: name, error: err, args: opts[:args], session_id: opts[:session_id], source: :tool, cause: cause, shape: shape)
|
|
1180
|
-
m = Mistakes.extinguish!(signature: m[:signature], args: opts[:args], shape: sem[:shape]) || m if m && defined?(Mistakes) && Mistakes.respond_to?(:extinguish!)
|
|
1180
|
+
m = Mistakes.extinguish!(signature: m[:signature], args: opts[:args], shape: sem[:shape]) || m if m && defined?(Mistakes) && Mistakes.respond_to?(:extinguish!) && !retryable_execution_failure?(raw: raw)
|
|
1181
1181
|
end
|
|
1182
1182
|
{ ok: sem[:semantic_ok], err: sem[:err], mistake: m, benign: sem[:benign] }
|
|
1183
1183
|
rescue StandardError
|
|
@@ -1330,6 +1330,33 @@ module PWN
|
|
|
1330
1330
|
counts[sig]
|
|
1331
1331
|
end
|
|
1332
1332
|
|
|
1333
|
+
# Only trusted structured execution outcomes grant the retry exemption.
|
|
1334
|
+
# Checkpoint/policy/budget denials are not failed command executions.
|
|
1335
|
+
private_class_method def self.retryable_execution_failure?(opts = {})
|
|
1336
|
+
raw = opts[:raw]
|
|
1337
|
+
data = raw.is_a?(Hash) ? raw : JSON.parse(raw.to_s)
|
|
1338
|
+
return false unless data.is_a?(Hash)
|
|
1339
|
+
|
|
1340
|
+
data = data.transform_keys(&:to_s)
|
|
1341
|
+
return false if data['checkpoint'] || data['denied'] || data['code'] || %w[retry_required budget_exhausted].include?(data['error'])
|
|
1342
|
+
|
|
1343
|
+
result = data['result'].is_a?(Hash) ? data['result'].transform_keys(&:to_s) : {}
|
|
1344
|
+
[data, result].any? do |row|
|
|
1345
|
+
row['timed_out'] == true || row['error'].to_s.match?(/\A(?:Timeout::Error:|timeout\b)/i) ||
|
|
1346
|
+
%w[exit exit_code exit_status].any? { |key| row[key].to_s.match?(/\A-?\d+\z/) && !row[key].to_i.zero? }
|
|
1347
|
+
end
|
|
1348
|
+
rescue JSON::ParserError
|
|
1349
|
+
false
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
private_class_method def self.note_payload_result!(opts = {})
|
|
1353
|
+
return unless retryable_execution_failure?(raw: opts[:raw])
|
|
1354
|
+
|
|
1355
|
+
sig = payload_sig(opts)
|
|
1356
|
+
Thread.current[:pwn_same_payload]&.delete(sig)
|
|
1357
|
+
Thread.current[:pwn_extinguished]&.delete(sig)
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1333
1360
|
private_class_method def self.read_like?(opts = {})
|
|
1334
1361
|
name = opts[:name].to_s
|
|
1335
1362
|
return true if name.match?(/_(read|get|list|status|recall|tail)\z/)
|
|
@@ -1365,6 +1392,7 @@ module PWN
|
|
|
1365
1392
|
hint = opts[:hint].to_s
|
|
1366
1393
|
thresh = defined?(Mistakes) ? Mistakes::REPEAT_THRESHOLD : 3
|
|
1367
1394
|
result = "#{result}\n#{hint}" unless hint.empty?
|
|
1395
|
+
return result if retryable_execution_failure?(raw: opts[:raw] || opts[:result])
|
|
1368
1396
|
return result if count < thresh
|
|
1369
1397
|
|
|
1370
1398
|
if defined?(Mistakes) && Mistakes.respond_to?(:extinguish!)
|
|
@@ -2832,6 +2860,7 @@ module PWN
|
|
|
2832
2860
|
# )
|
|
2833
2861
|
|
|
2834
2862
|
public_class_method def self.run(opts = {})
|
|
2863
|
+
prior_artifact_contract = Thread.current[:pwn_artifact_contract]
|
|
2835
2864
|
request = opts[:request].to_s
|
|
2836
2865
|
session_id = opts[:session_id]
|
|
2837
2866
|
on_tool = opts[:on_tool]
|
|
@@ -2869,6 +2898,8 @@ module PWN
|
|
|
2869
2898
|
OpenGoal.begin!(request: request, session_id: session_id)
|
|
2870
2899
|
end
|
|
2871
2900
|
Thread.current[:pwn_request_intent] = intent
|
|
2901
|
+
TurnFinalizer.commit_artifacts!(request: request, inherit: nested)
|
|
2902
|
+
required_artifacts = TurnFinalizer.required_artifacts(request: request)
|
|
2872
2903
|
Thread.current[:pwn_extinguished] = {}
|
|
2873
2904
|
Thread.current[:pwn_same_payload] = Hash.new(0)
|
|
2874
2905
|
Thread.current[:pwn_loop_t0] = Time.now unless nested
|
|
@@ -2880,9 +2911,10 @@ module PWN
|
|
|
2880
2911
|
expose_current_session(session_id: session_id)
|
|
2881
2912
|
Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
|
|
2882
2913
|
|
|
2883
|
-
|
|
2914
|
+
allow_text_only = required_artifacts.empty? && opts[:force_tools] != true
|
|
2915
|
+
cheap = allow_text_only && %i[greeting howto recall].include?(intent)
|
|
2884
2916
|
|
|
2885
|
-
if intent == :greeting
|
|
2917
|
+
if allow_text_only && intent == :greeting
|
|
2886
2918
|
debug_progress(msg: 'path=greeting', debug: opts[:debug])
|
|
2887
2919
|
quiet_debug_tui!(debug: opts[:debug], reason: 'greeting')
|
|
2888
2920
|
txt = answer_greeting(
|
|
@@ -3242,7 +3274,7 @@ module PWN
|
|
|
3242
3274
|
argv_s = args.is_a?(String) ? args.to_s : args.inspect
|
|
3243
3275
|
debug_progress(msg: "tool #{name} start:\n#{argv_s}", keep_newlines: true, cap: 0, tee: nil)
|
|
3244
3276
|
sig = payload_sig(name: name, args: args)
|
|
3245
|
-
declared_paths =
|
|
3277
|
+
declared_paths = required_artifacts
|
|
3246
3278
|
arg_paths = tool_arg_paths(args: args)
|
|
3247
3279
|
watch_paths = (declared_paths + arg_paths).uniq
|
|
3248
3280
|
before_host = TurnFinalizer.artifact_snapshot(paths: watch_paths)
|
|
@@ -3254,9 +3286,10 @@ module PWN
|
|
|
3254
3286
|
raw = if same_n >= 3
|
|
3255
3287
|
checkpoint_result(name: name, args: args)
|
|
3256
3288
|
else
|
|
3257
|
-
Dispatch.call(tool_call: tc)
|
|
3289
|
+
Dispatch.call(tool_call: tc, session_id: session_id)
|
|
3258
3290
|
end
|
|
3259
3291
|
end
|
|
3292
|
+
note_payload_result!(name: name, args: args, raw: raw)
|
|
3260
3293
|
tools_called += 1
|
|
3261
3294
|
if opts[:verification_contract]
|
|
3262
3295
|
after_artifacts = Verification.snapshot(opts[:verification_contract])
|
|
@@ -3265,7 +3298,7 @@ module PWN
|
|
|
3265
3298
|
end
|
|
3266
3299
|
tele = record_metrics(name: name, action_id: tc[:id], trusted_context: Policy.current_episode&.dig(:trusted_context) || trusted_context,
|
|
3267
3300
|
started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
|
|
3268
|
-
result = Result.condition(content: raw, entry: entry)
|
|
3301
|
+
result = Result.condition(content: raw, entry: entry, session_id: session_id)
|
|
3269
3302
|
|
|
3270
3303
|
unless tele[:ok]
|
|
3271
3304
|
fkey = Digest::SHA256.hexdigest("#{name}|#{args}")[0, 16]
|
|
@@ -3280,11 +3313,11 @@ module PWN
|
|
|
3280
3313
|
# P17 — never fork counterfactual when budget fingerprints dominate:
|
|
3281
3314
|
# CF is another mini agent loop and is the #1 amplifier of
|
|
3282
3315
|
# iteration-budget exhaustion on this host.
|
|
3283
|
-
if count >= thresh && !escalated && defined?(Curriculum) && !budget_exhaustion_hot?
|
|
3316
|
+
if count >= thresh && !retryable_execution_failure?(raw: raw) && !escalated && defined?(Curriculum) && !budget_exhaustion_hot?
|
|
3284
3317
|
cf = (turn_fails["cf:#{fkey}"] += 1) == 1 ? Curriculum.counterfactual(request: request, name: name, args: args, error: tele[:err] || raw[0, 200], hint: hint) : nil
|
|
3285
3318
|
hint = "#{hint}\n[pwn-ai/counterfactual] branch #{cf[:branch]} (score=#{cf[:score].round(2)}): #{cf[:content]}" if cf
|
|
3286
3319
|
end
|
|
3287
|
-
result = guard_repeated_failure(name: name, count: count, hint: hint, result: result, mistake: tele[:mistake], args: args, shape: tele.dig(:mistake, :shape))
|
|
3320
|
+
result = guard_repeated_failure(name: name, count: count, hint: hint, raw: raw, result: result, mistake: tele[:mistake], args: args, shape: tele.dig(:mistake, :shape))
|
|
3288
3321
|
end
|
|
3289
3322
|
|
|
3290
3323
|
on_tool&.call(name, args, result)
|
|
@@ -3345,6 +3378,7 @@ module PWN
|
|
|
3345
3378
|
end
|
|
3346
3379
|
raise
|
|
3347
3380
|
ensure
|
|
3381
|
+
Thread.current[:pwn_artifact_contract] = prior_artifact_contract
|
|
3348
3382
|
unless nested
|
|
3349
3383
|
Thread.current[:pwn_loop_active] = nil
|
|
3350
3384
|
Thread.current[:pwn_loop_deliverables] = nil
|
|
@@ -217,7 +217,10 @@ module PWN
|
|
|
217
217
|
"(defaults eval=#{eval_s || 20}s shell=#{shell_s || 30}s, clamped). " \
|
|
218
218
|
"LIVING OFF THE LAND #{land} #{doc}#{jobs} #{facts} LESSONS_REV #{rev} " \
|
|
219
219
|
"CANARY #{canary} never copy this token into tool args. " \
|
|
220
|
-
'
|
|
220
|
+
'LONG WORK: run fuzzers, full scans and Ghidra analysis through job_run, not blocking shell timeouts. ' \
|
|
221
|
+
'Use max_runtime for the campaign lifetime (21600 for six hours; 0 unlimited). Retain its id; poll job_status(id) and ' \
|
|
222
|
+
'read job_tail(id, offset) using next_offset. Current log eof is not job completion. Never relaunch a running job or apply the +180 timeout retry ladder to it. ' \
|
|
223
|
+
'Use job_status without id to recover jobs from earlier sessions. DOMAIN TOOLS for pentest/RE: binary_triage, pty_open, job_run, exploitdev, fuzz_campaign, finding_record, decompile.'
|
|
221
224
|
rescue StandardError
|
|
222
225
|
'load unknown — pass a conservative timeout on pwn_eval/shell anyway.'
|
|
223
226
|
end
|
|
@@ -370,13 +373,13 @@ module PWN
|
|
|
370
373
|
request = opts[:request].to_s
|
|
371
374
|
return '' unless defined?(PWN::AI::Agent::TurnFinalizer)
|
|
372
375
|
|
|
373
|
-
paths = PWN::AI::Agent::TurnFinalizer.
|
|
376
|
+
paths = PWN::AI::Agent::TurnFinalizer.required_artifacts(request: request)
|
|
374
377
|
return '' if paths.empty?
|
|
375
378
|
|
|
376
|
-
rows = paths.map { |path| " - #{path} (
|
|
379
|
+
rows = paths.map { |path| " - #{path} (current-turn write-effect tool call, then host stat + SHA-256 readback)" }.join("\n")
|
|
377
380
|
<<~BLOCK
|
|
378
381
|
DELIVERABLES
|
|
379
|
-
Do not finalize until every path below
|
|
382
|
+
Required artifacts are precommitted. Do not finalize until every path below has a verified current-turn write, fresh mtime, non-trivial size, and stat + SHA-256 readback:
|
|
380
383
|
#{rows}
|
|
381
384
|
|
|
382
385
|
BLOCK
|
|
@@ -43,7 +43,8 @@ module PWN
|
|
|
43
43
|
|
|
44
44
|
CORE_TOOLS = %w[memory_recall session_recall skills_recall pwn_eval shell
|
|
45
45
|
mistakes_record mistakes_resolve learning_note_outcome
|
|
46
|
-
memory_remember skills_update
|
|
46
|
+
memory_remember skills_update artifact_read artifact_grep
|
|
47
|
+
job_run job_status job_tail job_result job_kill].freeze
|
|
47
48
|
|
|
48
49
|
# Schema order = CORE_TOOLS. Current session is injected (RECENT TURNS).
|
|
49
50
|
# Then memory / prior sessions / skills, then pwn_eval before shell.
|
data/lib/pwn/ai/agent/result.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
3
6
|
module PWN
|
|
4
7
|
module AI
|
|
5
8
|
module Agent
|
|
6
9
|
# Conditioning applied to every tool result before it re-enters the
|
|
7
|
-
# conversation as a role:'tool' message:
|
|
10
|
+
# conversation as a role:'tool' message: lossless paging + credential
|
|
8
11
|
# redaction. Keeps the context window bounded and avoids leaking
|
|
9
12
|
# PWN::Env credentials back into the model.
|
|
10
13
|
module Result
|
|
@@ -36,13 +39,66 @@ module PWN
|
|
|
36
39
|
|
|
37
40
|
public_class_method def self.condition(opts = {})
|
|
38
41
|
content = opts[:content].to_s
|
|
39
|
-
|
|
40
|
-
cap = entry ? entry.max_chars : default_max
|
|
41
|
-
|
|
42
|
-
content = "#{content[0, cap]}…[truncated #{opts[:content].to_s.length - cap} chars]" if content.length > cap
|
|
42
|
+
content = JSON.generate(spill_summary(content: content, session_id: opts[:session_id])) if content.bytesize > token_limit(entry: opts[:entry]) || !content.dup.force_encoding('UTF-8').valid_encoding?
|
|
43
43
|
redact(content: content)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Preserve the original handler result before redaction/quarantine.
|
|
47
|
+
public_class_method def self.page(opts = {})
|
|
48
|
+
value = opts[:value]
|
|
49
|
+
safe_value = value.is_a?(String) ? value : transport_value(value: value)
|
|
50
|
+
content = value.is_a?(String) ? value : JSON.generate(safe_value)
|
|
51
|
+
return safe_value if content.bytesize <= token_limit(entry: opts[:entry]) && content.dup.force_encoding('UTF-8').valid_encoding?
|
|
52
|
+
|
|
53
|
+
summary = spill_summary(content: content, session_id: opts[:session_id])
|
|
54
|
+
summary[:serialization] = value.is_a?(String) ? 'raw' : 'json (binary strings use encoding/base64 wrappers)'
|
|
55
|
+
if value.is_a?(Hash)
|
|
56
|
+
%w[success ok exit exit_code exit_status timed_out error code].each do |key|
|
|
57
|
+
next unless value.key?(key.to_sym) || value.key?(key)
|
|
58
|
+
|
|
59
|
+
field = value.key?(key.to_sym) ? value[key.to_sym] : value[key]
|
|
60
|
+
summary[key.to_sym] = field if [true, false, nil].include?(field) || field.is_a?(Numeric) || (field.is_a?(String) && field.bytesize <= 128)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
summary
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A byte is a conservative token upper bound; never assume four bytes
|
|
67
|
+
# per token for arbitrary strings/hex/binary dumps. No tokenizer needed.
|
|
68
|
+
public_class_method def self.token_limit(opts = {})
|
|
69
|
+
configured = PWN::Env.dig(:ai, :agent, :artifact_max_tokens).to_i if defined?(PWN::Env)
|
|
70
|
+
limit = configured.to_i.positive? ? configured : default_max / 4
|
|
71
|
+
cap = opts[:entry]&.max_chars.to_i
|
|
72
|
+
limit = [limit, cap].min if cap.positive?
|
|
73
|
+
[limit, 1024].max
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reserve room for JSON escaping, metadata, and the dispatch envelope.
|
|
77
|
+
public_class_method def self.page_length
|
|
78
|
+
((token_limit - 768) / 8).clamp(32, 2048)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private_class_method def self.spill_summary(opts = {})
|
|
82
|
+
content = opts[:content]
|
|
83
|
+
artifact = PWN::Plugins::ArtifactRegistry.spill(bytes: content, session_id: opts[:session_id] || Thread.current[:pwn_session_id] || 'default')
|
|
84
|
+
{
|
|
85
|
+
artifact: artifact,
|
|
86
|
+
summary: 'Full result saved without truncation. Use artifact_read(handle, offset, length) or artifact_grep(handle, regex); offsets are bytes.',
|
|
87
|
+
preview: redact(content: content.byteslice(0, 32).to_s.dup.force_encoding('UTF-8').scrub)
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private_class_method def self.transport_value(opts = {})
|
|
92
|
+
value = opts[:value]
|
|
93
|
+
case value
|
|
94
|
+
when Hash then value.transform_values { |v| transport_value(value: v) }
|
|
95
|
+
when Array then value.map { |v| transport_value(value: v) }
|
|
96
|
+
when String
|
|
97
|
+
value.dup.force_encoding('UTF-8').valid_encoding? ? value.dup.force_encoding('UTF-8') : { encoding: 'base64', body: Base64.strict_encode64(value) }
|
|
98
|
+
else value
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
46
102
|
# Engine-aware default: ollama keeps history inside a tight num_ctx;
|
|
47
103
|
# a 24k tool dump on every call eats the window before useful work.
|
|
48
104
|
# Override via PWN::Env[:ai][:ollama][:result_max].
|
|
@@ -109,9 +165,23 @@ module PWN
|
|
|
109
165
|
# Run condition and return its result
|
|
110
166
|
#{self}.condition(
|
|
111
167
|
content: 'required - String returned by Dispatch.call',
|
|
112
|
-
entry: 'optional - Registry::Entry (used for max_chars; nil → DEFAULT_MAX)'
|
|
168
|
+
entry: 'optional - Registry::Entry (used for max_chars; nil → DEFAULT_MAX)',
|
|
169
|
+
session_id: 'optional - session directory used for lossless spilled results'
|
|
113
170
|
)
|
|
114
171
|
|
|
172
|
+
# Preserve oversized handler results before redaction or quarantine.
|
|
173
|
+
#{self}.page(
|
|
174
|
+
value: 'required - original tool handler result to condition',
|
|
175
|
+
entry: 'optional - registry entry with an inline size cap',
|
|
176
|
+
session_id: 'optional - session directory for the saved result'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Conservative byte-based token bound, configurable through ai.agent.artifact_max_tokens.
|
|
180
|
+
#{self}.token_limit(entry: 'optional - registry entry imposing a smaller cap')
|
|
181
|
+
|
|
182
|
+
# Maximum page bytes with space reserved for response metadata.
|
|
183
|
+
#{self}.page_length
|
|
184
|
+
|
|
115
185
|
# Engine-aware default: ollama keeps history inside a tight num_ctx;
|
|
116
186
|
#{self}.default_max
|
|
117
187
|
|