pwn 0.5.766 → 0.5.772
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 +3 -3
- data/documentation/Configuration.md +2 -0
- data/documentation/General-PWN-Usage.md +68 -0
- data/documentation/Harness-Evaluation.md +312 -0
- data/documentation/pwn-ai-Agent.md +135 -6
- data/etc/default_cron/session_cleanup.rb +6 -1
- data/etc/default_skills/pwn/ai/agent/dispatch/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/open_goal/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/skill_review/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/solve/SKILL.md +4 -2
- data/etc/default_skills/pwn/ai/context_ingestion/SKILL.md +1 -1
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console/SKILL.md +1 -1
- data/etc/default_skills/pwn/plugins/repl/mesh/SKILL.md +4 -0
- data/etc/default_skills/pwn/plugins/repl/mesh/references/urls.md +3 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +1 -1
- data/etc/default_skills/pwn/runtime_fingerprint/SKILL.md +47 -0
- data/lib/pwn/ai/agent/dispatch.rb +50 -6
- data/lib/pwn/ai/agent/loop.rb +192 -115
- data/lib/pwn/ai/agent/open_goal.rb +43 -1
- data/lib/pwn/ai/agent/registry.rb +6 -1
- data/lib/pwn/ai/agent/request_runtime.rb +508 -17
- data/lib/pwn/ai/agent/result.rb +23 -6
- data/lib/pwn/ai/agent/reward.rb +4 -1
- data/lib/pwn/ai/agent/skill_review.rb +148 -1
- data/lib/pwn/ai/agent/solve.rb +420 -39
- data/lib/pwn/ai/agent/swarm.rb +11 -2
- data/lib/pwn/ai/agent/tools/shell.rb +23 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +37 -6
- data/lib/pwn/ai/agent/verification.rb +42 -5
- data/lib/pwn/ai/anthropic.rb +59 -5
- data/lib/pwn/ai/cli.rb +48 -4
- data/lib/pwn/ai/context.rb +1 -1
- data/lib/pwn/ai/context_ingestion.rb +16 -1
- data/lib/pwn/ai/gemini.rb +36 -5
- data/lib/pwn/ai/grok.rb +19 -15
- data/lib/pwn/ai/http_retry.rb +14 -0
- data/lib/pwn/ai/mcp/combo_nation.rb +5 -2
- data/lib/pwn/ai/mcp.rb +5 -2
- data/lib/pwn/ai/ollama.rb +46 -5
- data/lib/pwn/ai/open_ai.rb +11 -11
- data/lib/pwn/ai/open_web_ui.rb +46 -5
- data/lib/pwn/config.rb +10 -2
- data/lib/pwn/migrate.rb +9 -1
- data/lib/pwn/plugins/repl/ai.rb +31 -1
- data/lib/pwn/plugins/repl/ai_console.rb +843 -19
- data/lib/pwn/plugins/repl/ai_console_commands.rb +23 -5
- data/lib/pwn/plugins/repl/ai_swarm.rb +65 -5
- data/lib/pwn/plugins/repl/mesh.rb +894 -83
- data/lib/pwn/plugins/repl.rb +2 -0
- data/lib/pwn/redaction.rb +29 -5
- data/lib/pwn/runtime_fingerprint.rb +84 -0
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +1 -0
- data/spec/fixtures/harness/adapters.json +62 -0
- data/spec/fixtures/harness/adapters.json.sha256 +1 -0
- data/spec/fixtures/harness/experiment_gold.json +9 -0
- data/spec/fixtures/harness/experiment_gold.json.sha256 +1 -0
- data/spec/fixtures/harness/experiment_tasks.json +12 -0
- data/spec/fixtures/harness/experiment_tasks.json.sha256 +1 -0
- data/spec/fixtures/harness/live_comparison.json +21 -0
- data/spec/fixtures/harness/live_comparison.json.sha256 +1 -0
- data/spec/fixtures/harness/manifest.json +290 -0
- data/spec/fixtures/harness/manifest.json.sha256 +1 -0
- data/spec/integration/harness_evaluation_spec.rb +171 -0
- data/spec/integration/harness_layout_spec.rb +15 -0
- data/spec/integration/harness_live_spec.rb +26 -0
- data/spec/integration/harness_package_spec.rb +38 -0
- data/spec/integration/native_ruby_helpers_spec.rb +4 -2
- data/spec/integration/reinforced_feedback_loop_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +47 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +479 -17
- data/spec/lib/pwn/ai/agent/open_goal_spec.rb +17 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +232 -0
- data/spec/lib/pwn/ai/agent/result_spec.rb +33 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/skill_review_spec.rb +17 -5
- data/spec/lib/pwn/ai/agent/solve_spec.rb +392 -7
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +19 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/verification_spec.rb +12 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +10 -0
- data/spec/lib/pwn/ai/cli_spec.rb +40 -0
- data/spec/lib/pwn/ai/context_ingestion_spec.rb +12 -0
- data/spec/lib/pwn/ai/grok_spec.rb +35 -0
- data/spec/lib/pwn/ai/mcp/combo_nation_spec.rb +10 -0
- data/spec/lib/pwn/ai/mcp_spec.rb +10 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +13 -0
- data/spec/lib/pwn/migrate_spec.rb +37 -4
- data/spec/lib/pwn/plugins/repl/ai_console_completion_wrap_spec.rb +110 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +307 -3
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +29 -0
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_spec.rb +221 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_identity_spec.rb +134 -0
- data/spec/lib/pwn/plugins/repl_mesh_dispatch_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_mesh_dm_transport_spec.rb +2 -1
- data/spec/lib/pwn/plugins/repl_mesh_multi_rx_spec.rb +315 -0
- data/spec/lib/pwn/plugins/repl_mesh_origin_gps_spec.rb +120 -0
- data/spec/lib/pwn/plugins/repl_mesh_rx_errors_spec.rb +325 -0
- data/spec/lib/pwn/plugins/repl_mesh_scroll_spec.rb +49 -0
- data/spec/lib/pwn/plugins/repl_mesh_transport_label_spec.rb +43 -15
- data/spec/lib/pwn/plugins/repl_spec.rb +31 -7
- data/spec/lib/pwn/redaction_spec.rb +17 -0
- data/spec/lib/pwn/runtime_fingerprint_spec.rb +53 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +1 -1
- data/spec/spec_helper.rb +3 -1
- data/spec/support/harness/cli_faults.rb +112 -0
- data/spec/support/harness/evaluate.rb +191 -0
- data/spec/support/harness/experiment_preload.rb +20 -0
- data/spec/support/harness/experiments.rb +137 -0
- data/spec/support/harness/fake_provider.rb +82 -0
- data/spec/support/harness/fault_preload.rb +44 -0
- data/spec/support/harness/package_acceptance.rb +77 -0
- data/spec/support/harness/record_training.rb +18 -0
- data/spec/support/harness/reference_adapter.py +128 -0
- data/spec/support/harness/runtime_faults.rb +97 -0
- data/spec/support/harness/session_acceptance.rb +70 -0
- data/spec/support/harness/swarm_acceptance.rb +61 -0
- data/third_party/pwn_rdoc.jsonl +129 -3
- metadata +46 -8
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -818,9 +818,11 @@ module PWN
|
|
|
818
818
|
|
|
819
819
|
private_class_method def self.request_unsatisfied?(opts = {})
|
|
820
820
|
request = opts[:request].to_s
|
|
821
|
+
return true if completion_unmet(request: request, messages: opts[:messages]).any?
|
|
822
|
+
return false if Thread.current[:pwn_request_runtime]&.scope_evidenced?
|
|
823
|
+
|
|
821
824
|
need = request_need(request: request)
|
|
822
825
|
return false if need == :none
|
|
823
|
-
return false if Thread.current[:pwn_loop_no_tools]
|
|
824
826
|
|
|
825
827
|
effects = tool_effects(messages: opts[:messages])
|
|
826
828
|
return !effects.intersect?(%i[read recall eval]) if need == :read && catalog_lookup?(request: request)
|
|
@@ -829,41 +831,21 @@ module PWN
|
|
|
829
831
|
live = effects.reject { |fx| %i[recall store].include?(fx) }
|
|
830
832
|
unmet = completion_unmet(request: request, messages: opts[:messages])
|
|
831
833
|
return true if unmet.any?
|
|
832
|
-
return true if live.empty? && %i[write browse read].include?(need)
|
|
833
834
|
return true if duration_unsatisfied?(request: request)
|
|
835
|
+
return false if need == :write && write_verified?(request: request, messages: opts[:messages])
|
|
836
|
+
return true if live.empty? && %i[write browse read].include?(need)
|
|
834
837
|
return true if need == :write && !write_verified?(effects: effects, request: request, messages: opts[:messages])
|
|
835
838
|
return true if need == :browse && !effects.include?(:browse)
|
|
836
839
|
return true if need == :any && live.any? && !live.intersect?(%i[write browse eval])
|
|
837
840
|
|
|
838
841
|
false
|
|
839
842
|
rescue StandardError
|
|
840
|
-
|
|
843
|
+
true
|
|
841
844
|
end
|
|
842
845
|
|
|
843
846
|
private_class_method def self.write_verified?(opts = {})
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if idx
|
|
847
|
-
tail = effects[(idx + 1)..] || []
|
|
848
|
-
return true if tail.intersect?(%i[read eval])
|
|
849
|
-
end
|
|
850
|
-
|
|
851
|
-
saw_write_argv = false
|
|
852
|
-
Array(opts[:messages]).each do |msg|
|
|
853
|
-
next unless msg.is_a?(Hash)
|
|
854
|
-
|
|
855
|
-
if msg[:role].to_s == 'assistant' && defined?(Dispatch)
|
|
856
|
-
Array(msg[:tool_calls]).each do |tc|
|
|
857
|
-
blob = Dispatch.send(:argv_blob, args: tc.dig(:function, :arguments) || tc.dig('function', 'arguments'))
|
|
858
|
-
saw_write_argv = true if blob.to_s.match?(Dispatch::WRITE_ARGV_RX)
|
|
859
|
-
end
|
|
860
|
-
elsif saw_write_argv && msg[:role].to_s == 'tool'
|
|
861
|
-
fx = stamped_effect(content: msg[:content])
|
|
862
|
-
return true if %i[read eval].include?(fx)
|
|
863
|
-
end
|
|
864
|
-
end
|
|
865
|
-
|
|
866
|
-
false
|
|
847
|
+
# A generic later read/eval is not verification of this write.
|
|
848
|
+
TurnFinalizer.evidence_satisfied?(request: opts[:request], messages: opts[:messages])
|
|
867
849
|
rescue StandardError
|
|
868
850
|
false
|
|
869
851
|
end
|
|
@@ -915,17 +897,20 @@ module PWN
|
|
|
915
897
|
private_class_method def self.declared_contract_unsatisfied?(opts = {})
|
|
916
898
|
completion_unmet(opts).any?
|
|
917
899
|
rescue StandardError
|
|
918
|
-
|
|
900
|
+
true
|
|
919
901
|
end
|
|
920
902
|
|
|
921
903
|
private_class_method def self.completion_unmet(opts = {})
|
|
922
904
|
contract = declared_contract(request: opts[:request])
|
|
923
|
-
request = opts[:request]
|
|
905
|
+
request = verification_text(text: opts[:request])
|
|
924
906
|
unmet = []
|
|
925
907
|
files = Array(contract[:paths]) + Array(contract[:proofs])
|
|
926
908
|
files.each { |path| unmet << "deliverable_missing:#{path}" if deliverable_missing?(path: path) }
|
|
927
909
|
artifact = TurnFinalizer.arbitrate(request: request, paths: contract[:paths], messages: opts[:messages])
|
|
928
|
-
artifact[:unmet].each
|
|
910
|
+
artifact[:unmet].each do |row|
|
|
911
|
+
unmet << "#{verification_text(text: row[:criterion])}:#{verification_text(text: row[:detail])}"
|
|
912
|
+
end
|
|
913
|
+
unmet.concat(Thread.current[:pwn_request_runtime].unmet) if Thread.current[:pwn_request_runtime]
|
|
929
914
|
trace_files = session_files(messages: opts[:messages])
|
|
930
915
|
unmet << 'issue_work_proofs' if contract[:issue_work] && Array(contract[:proofs]).empty? && files.empty? && trace_files.empty?
|
|
931
916
|
if contract[:issue_work] && defined?(PWN::Plugins::Findings)
|
|
@@ -935,14 +920,14 @@ module PWN
|
|
|
935
920
|
end
|
|
936
921
|
unmet << 'skills' if declared_skills_missing?(skills: contract[:skills], request: request)
|
|
937
922
|
blob = evidence_blob(messages: opts[:messages], files: files + trace_files)
|
|
938
|
-
asked_hosts = Array(contract[:hosts]).select { |host| request.downcase.include?(host.
|
|
939
|
-
asked_tech = Array(contract[:techniques]).select { |tech| request.downcase.include?(tech.
|
|
940
|
-
unmet << 'hosts'
|
|
923
|
+
asked_hosts = Array(contract[:hosts]).select { |host| request.downcase.include?(verification_text(text: host).downcase) }
|
|
924
|
+
asked_tech = Array(contract[:techniques]).select { |tech| request.downcase.include?(verification_text(text: tech).downcase) }
|
|
925
|
+
unmet << 'hosts' unless asked_hosts.all? { |host| Thread.current[:pwn_request_runtime]&.host_evidenced?(host) }
|
|
941
926
|
unmet << 'techniques' if evidence_tokens_missing?(tokens: asked_tech, blob: blob)
|
|
942
927
|
Array(Swarm.honesty_unmet(swarm_id: Thread.current[:pwn_swarm_id])).each { |u| unmet << u } if defined?(Swarm)
|
|
943
928
|
unmet
|
|
944
|
-
rescue StandardError
|
|
945
|
-
[]
|
|
929
|
+
rescue StandardError => e
|
|
930
|
+
["verification_error:#{e.class}:#{verification_text(text: e.message)}"]
|
|
946
931
|
end
|
|
947
932
|
|
|
948
933
|
private_class_method def self.write_or_read_evidenced?(opts = {})
|
|
@@ -968,16 +953,20 @@ module PWN
|
|
|
968
953
|
tokens = Array(opts[:tokens]).map(&:to_s).reject(&:empty?)
|
|
969
954
|
return false if tokens.empty?
|
|
970
955
|
|
|
971
|
-
blob = opts[:blob].
|
|
972
|
-
tokens.any? { |tok| !blob.include?(tok.downcase) }
|
|
956
|
+
blob = verification_text(text: opts[:blob]).downcase
|
|
957
|
+
tokens.any? { |tok| !blob.include?(verification_text(text: tok).downcase) }
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
private_class_method def self.verification_text(opts = {})
|
|
961
|
+
opts[:text].to_s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
973
962
|
end
|
|
974
963
|
|
|
975
964
|
private_class_method def self.evidence_blob(opts = {})
|
|
976
965
|
parts = Array(opts[:messages]).select { |msg| msg.is_a?(Hash) && %w[tool assistant].include?(msg[:role].to_s) }.map do |msg|
|
|
977
|
-
unwrap_untrusted(text: msg[:content]
|
|
966
|
+
verification_text(text: unwrap_untrusted(text: msg[:content]))
|
|
978
967
|
end
|
|
979
968
|
Array(opts[:files]).each do |path|
|
|
980
|
-
parts << File.binread(path) if File.file?(path.to_s)
|
|
969
|
+
parts << verification_text(text: File.binread(path)) if File.file?(path.to_s)
|
|
981
970
|
rescue StandardError
|
|
982
971
|
nil
|
|
983
972
|
end
|
|
@@ -996,7 +985,7 @@ module PWN
|
|
|
996
985
|
end
|
|
997
986
|
|
|
998
987
|
private_class_method def self.unwrap_untrusted(opts = {})
|
|
999
|
-
text = opts[:text]
|
|
988
|
+
text = verification_text(text: opts[:text])
|
|
1000
989
|
text = text.sub(/\A\[UNTRUSTED TOOL OUTPUT[^\]]*\]\s*/m, '')
|
|
1001
990
|
text.sub(%r{\s*\[/UNTRUSTED TOOL OUTPUT\]\s*\z}m, '')
|
|
1002
991
|
end
|
|
@@ -1010,10 +999,8 @@ module PWN
|
|
|
1010
999
|
cached = Thread.current[:pwn_loop_deliverables]
|
|
1011
1000
|
contract = if cached.is_a?(Array) || cached.is_a?(Hash)
|
|
1012
1001
|
normalize_contract(raw: cached)
|
|
1013
|
-
elsif Thread.current[:pwn_loop_nested] || !Thread.current[:pwn_loop_active]
|
|
1014
|
-
EMPTY_CONTRACT.dup
|
|
1015
1002
|
else
|
|
1016
|
-
|
|
1003
|
+
EMPTY_CONTRACT.dup
|
|
1017
1004
|
end
|
|
1018
1005
|
# Paths are user-owned. Inference cannot invent a destination or
|
|
1019
1006
|
# convert the source document into a write obligation.
|
|
@@ -1139,8 +1126,6 @@ module PWN
|
|
|
1139
1126
|
return false
|
|
1140
1127
|
end
|
|
1141
1128
|
end
|
|
1142
|
-
return true if evidence_satisfied?(messages: opts[:messages], text: opts[:text], request: opts[:request])
|
|
1143
|
-
return false if incomplete_final?(text: opts[:text], last_iter: false)
|
|
1144
1129
|
return false if request_unsatisfied?(
|
|
1145
1130
|
request: opts[:request],
|
|
1146
1131
|
messages: opts[:messages],
|
|
@@ -1306,6 +1291,9 @@ module PWN
|
|
|
1306
1291
|
end
|
|
1307
1292
|
|
|
1308
1293
|
private_class_method def self.active_engine
|
|
1294
|
+
owner = Thread.current[:pwn_request_runtime]
|
|
1295
|
+
return owner.engine.to_sym if owner.respond_to?(:engine)
|
|
1296
|
+
|
|
1309
1297
|
tl = Thread.current[:pwn_swarm_engine].to_s
|
|
1310
1298
|
return tl.downcase.to_sym unless tl.empty?
|
|
1311
1299
|
|
|
@@ -2042,7 +2030,7 @@ module PWN
|
|
|
2042
2030
|
|
|
2043
2031
|
private_class_method def self.thinking_effort(opts = {})
|
|
2044
2032
|
engine_name = (opts[:engine] || active_engine).to_s.to_sym
|
|
2045
|
-
slot =
|
|
2033
|
+
slot = PWN::AI::HttpRetry.provider_config(engine: engine_name)
|
|
2046
2034
|
slot = {} unless slot.is_a?(Hash)
|
|
2047
2035
|
value = opts[:effort] || slot[:reasoning_effort] || slot[:think]
|
|
2048
2036
|
text = value.to_s.strip
|
|
@@ -2060,7 +2048,7 @@ module PWN
|
|
|
2060
2048
|
)
|
|
2061
2049
|
|
|
2062
2050
|
runtime = Thread.current[:pwn_request_runtime]
|
|
2063
|
-
if runtime.respond_to?(:call)
|
|
2051
|
+
if runtime.respond_to?(:call)
|
|
2064
2052
|
return runtime.call(messages: messages, tools: tools) do |prepared, route|
|
|
2065
2053
|
invoke_provider_chat(engine: (route || {})[:provider] || active_engine, messages: prepared, tools: tools, model: (route || {})[:model], temp: (route || {})[:temperature])
|
|
2066
2054
|
end
|
|
@@ -2078,6 +2066,7 @@ module PWN
|
|
|
2078
2066
|
|
|
2079
2067
|
mod = Object.const_get(mod_name)
|
|
2080
2068
|
if mod.respond_to?(:chat_with_tools)
|
|
2069
|
+
config = PWN::AI::HttpRetry.provider_config(engine: engine) || {}
|
|
2081
2070
|
wire_msgs = if engine == :grok
|
|
2082
2071
|
openai_wire_messages(messages: messages)
|
|
2083
2072
|
elsif local_engine?(engine: engine)
|
|
@@ -2096,14 +2085,14 @@ module PWN
|
|
|
2096
2085
|
cwt_opts[:think] = true
|
|
2097
2086
|
else
|
|
2098
2087
|
cwt_opts[:think] = false
|
|
2099
|
-
cwt_opts[:reasoning_effort] = 'none' if engine == :openai &&
|
|
2088
|
+
cwt_opts[:reasoning_effort] = 'none' if engine == :openai && config[:reasoning_effort].to_s == 'none'
|
|
2100
2089
|
end
|
|
2101
|
-
model = opts[:model] || Thread.current[:pwn_swarm_model]
|
|
2090
|
+
model = opts[:model] || Thread.current[:pwn_swarm_model] || config[:model]
|
|
2102
2091
|
cwt_opts[:model] = model unless model.to_s.strip.empty?
|
|
2103
2092
|
cwt_opts[:temp] = opts[:temp] unless opts[:temp].nil?
|
|
2104
2093
|
if tools && !tools.empty?
|
|
2105
2094
|
env_tc = begin
|
|
2106
|
-
|
|
2095
|
+
config[:tool_choice]
|
|
2107
2096
|
rescue StandardError
|
|
2108
2097
|
nil
|
|
2109
2098
|
end
|
|
@@ -2594,7 +2583,7 @@ module PWN
|
|
|
2594
2583
|
SYS
|
|
2595
2584
|
|
|
2596
2585
|
txt =
|
|
2597
|
-
if mod.respond_to?(:chat)
|
|
2586
|
+
if mod.respond_to?(:chat) && !Thread.current[:pwn_request_runtime]
|
|
2598
2587
|
r = mod.chat(
|
|
2599
2588
|
request: request,
|
|
2600
2589
|
system_role_content: q_sys,
|
|
@@ -2660,7 +2649,7 @@ module PWN
|
|
|
2660
2649
|
SYS
|
|
2661
2650
|
|
|
2662
2651
|
txt =
|
|
2663
|
-
if mod.respond_to?(:chat)
|
|
2652
|
+
if mod.respond_to?(:chat) && !Thread.current[:pwn_request_runtime]
|
|
2664
2653
|
r = mod.chat(
|
|
2665
2654
|
request: request,
|
|
2666
2655
|
system_role_content: q_sys,
|
|
@@ -2752,7 +2741,7 @@ module PWN
|
|
|
2752
2741
|
SYS
|
|
2753
2742
|
|
|
2754
2743
|
txt =
|
|
2755
|
-
if mod.respond_to?(:chat)
|
|
2744
|
+
if mod.respond_to?(:chat) && !Thread.current[:pwn_request_runtime]
|
|
2756
2745
|
r = mod.chat(
|
|
2757
2746
|
request: request,
|
|
2758
2747
|
system_role_content: howto_sys,
|
|
@@ -3050,7 +3039,7 @@ module PWN
|
|
|
3050
3039
|
SYS
|
|
3051
3040
|
|
|
3052
3041
|
txt =
|
|
3053
|
-
if mod.respond_to?(:chat)
|
|
3042
|
+
if mod.respond_to?(:chat) && !Thread.current[:pwn_request_runtime]
|
|
3054
3043
|
r = mod.chat(
|
|
3055
3044
|
request: request,
|
|
3056
3045
|
system_role_content: recall_sys,
|
|
@@ -3104,7 +3093,15 @@ module PWN
|
|
|
3104
3093
|
# )
|
|
3105
3094
|
|
|
3106
3095
|
public_class_method def self.run(opts = {})
|
|
3107
|
-
|
|
3096
|
+
previous_runtime = Thread.current[:pwn_request_runtime]
|
|
3097
|
+
execution_lease = execution_lease(opts)
|
|
3098
|
+
runtime = opts[:runtime] || request_runtime(opts)
|
|
3099
|
+
Thread.current[:pwn_request_runtime] = runtime
|
|
3100
|
+
return 'Request cancelled.' if runtime.outcome&.dig(:status) == 'cancelled'
|
|
3101
|
+
|
|
3102
|
+
runtime.start!
|
|
3103
|
+
runtime.bind_verification!(contract: opts[:verification_contract])
|
|
3104
|
+
options = opts.merge(request: runtime.request, verification_contract: runtime.verification_contract)
|
|
3108
3105
|
sid = opts[:session_id].to_s
|
|
3109
3106
|
lease = PWN::Sessions.activate(session_id: sid) if !sid.empty? && File.file?(File.join(PWN::Sessions.sessions_dir, "#{sid}.jsonl"))
|
|
3110
3107
|
original_system = opts[:system_role_content]
|
|
@@ -3115,16 +3112,95 @@ module PWN
|
|
|
3115
3112
|
history.pop if history.last&.dig(:role).to_s == 'assistant' && Array(history.last[:tool_calls]).empty?
|
|
3116
3113
|
repair_steering_history!(messages: history)
|
|
3117
3114
|
e.instructions.each do |instruction|
|
|
3115
|
+
runtime.amend!(instruction: instruction)
|
|
3118
3116
|
history << { role: 'user', content: instruction }
|
|
3119
3117
|
append_session(session_id: options[:session_id], role: 'user', content: instruction)
|
|
3120
3118
|
end
|
|
3121
|
-
|
|
3119
|
+
return 'Request cancelled.' if runtime.outcome&.dig(:status) == 'cancelled'
|
|
3120
|
+
|
|
3121
|
+
options = options.merge(request: runtime.request, steering_history: history, system_role_content: original_system)
|
|
3122
3122
|
update_steered_open_goal!(options)
|
|
3123
|
-
options.delete(:verification_contract)
|
|
3124
3123
|
retry
|
|
3125
3124
|
end
|
|
3125
|
+
rescue Interrupt
|
|
3126
|
+
runtime&.finish!(status: 'cancelled')
|
|
3127
|
+
raise
|
|
3126
3128
|
ensure
|
|
3127
|
-
|
|
3129
|
+
begin
|
|
3130
|
+
runtime&.finish!(status: 'incomplete') unless runtime&.outcome
|
|
3131
|
+
previous_runtime.accept_child!(runtime: runtime) if previous_runtime && runtime && previous_runtime != runtime
|
|
3132
|
+
opts[:on_outcome]&.call(runtime.outcome) if runtime
|
|
3133
|
+
ensure
|
|
3134
|
+
Thread.current[:pwn_request_runtime] = previous_runtime
|
|
3135
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
3136
|
+
execution_lease&.close
|
|
3137
|
+
end
|
|
3138
|
+
end
|
|
3139
|
+
|
|
3140
|
+
# Exclusive execution ownership is distinct from the shared cleanup
|
|
3141
|
+
# lease. Acquire before loading/checkpointing; crash closes the fd.
|
|
3142
|
+
private_class_method def self.execution_lease(opts = {})
|
|
3143
|
+
sid = (opts[:runtime]&.session_id || opts[:session_id]).to_s
|
|
3144
|
+
raise ArgumentError, 'runtime session identity mismatch' if opts[:runtime] && opts[:session_id] && opts[:session_id].to_s != sid
|
|
3145
|
+
|
|
3146
|
+
return if sid.empty?
|
|
3147
|
+
|
|
3148
|
+
root = PWN::Sessions.sessions_dir
|
|
3149
|
+
FileUtils.mkdir_p(root)
|
|
3150
|
+
lock = File.open(File.join(root, ".#{Digest::SHA256.hexdigest(sid)}.execution.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) # rubocop:disable Style/FileOpen -- Loop owns lifetime
|
|
3151
|
+
lock.close_on_exec = true
|
|
3152
|
+
raise IOError, "Session #{sid} is already executing" unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
3153
|
+
|
|
3154
|
+
lock
|
|
3155
|
+
rescue Exception # rubocop:disable Lint/RescueException -- close untransferred descriptor on interrupt
|
|
3156
|
+
lock&.close
|
|
3157
|
+
raise
|
|
3158
|
+
end
|
|
3159
|
+
|
|
3160
|
+
private_class_method def self.engine_failure_text(opts = {})
|
|
3161
|
+
attempts = opts[:runtime]&.last_provider_attempts || opts[:attempts]
|
|
3162
|
+
"[pwn-ai] engine hop failed after #{attempts} tries: #{opts[:error].message.to_s[0, 240]}"
|
|
3163
|
+
end
|
|
3164
|
+
|
|
3165
|
+
private_class_method def self.engine_retry_exhausted?(opts = {})
|
|
3166
|
+
!opts[:runtime].nil? || opts[:attempts] >= 5
|
|
3167
|
+
end
|
|
3168
|
+
|
|
3169
|
+
private_class_method def self.scheduled_tool_calls(opts = {})
|
|
3170
|
+
opts[:steering]&.checkpoint(messages: opts[:messages], phase: :tool)
|
|
3171
|
+
eligible = lambda do |call|
|
|
3172
|
+
args = { name: call.dig(:function, :name), args: call.dig(:function, :arguments) }
|
|
3173
|
+
signature = payload_sig(args)
|
|
3174
|
+
!Thread.current[:pwn_extinguished]&.[](signature) && (read_like?(args) || Thread.current[:pwn_same_payload].to_h.fetch(signature, 0) < 2)
|
|
3175
|
+
end
|
|
3176
|
+
opts[:runtime].scheduled_calls(opts[:calls], session_id: opts[:session_id], eligible: eligible)
|
|
3177
|
+
end
|
|
3178
|
+
|
|
3179
|
+
private_class_method def self.dispatch_scheduled(opts = {})
|
|
3180
|
+
opts[:result] || Dispatch.call(tool_call: opts[:tool_call], session_id: opts[:session_id])
|
|
3181
|
+
end
|
|
3182
|
+
|
|
3183
|
+
private_class_method def self.request_runtime(opts = {})
|
|
3184
|
+
goal = OpenGoal.for_session(session_id: opts[:session_id]) if opts[:request].to_s.match?(OpenGoal::CONTINUE_RX)
|
|
3185
|
+
raise ArgumentError, 'selected session no longer has an unfinished request' if opts[:require_resumable] && !goal
|
|
3186
|
+
|
|
3187
|
+
resume = goal && goal[:request_id] && goal[:checkpoint_path]
|
|
3188
|
+
root = resume ? File.dirname(goal[:checkpoint_path], 2) : opts[:artifact_root]
|
|
3189
|
+
RequestRuntime.new(request: goal ? goal[:request] : opts[:request], session_id: opts[:session_id],
|
|
3190
|
+
request_id: resume ? goal[:request_id] : opts[:request_id], artifact_root: root,
|
|
3191
|
+
resume: !!resume, trace: false, retrieve: false, enabled_toolsets: opts[:enabled_toolsets], verification_contract: opts[:verification_contract],
|
|
3192
|
+
engine: opts[:engine], model: opts[:model], provider_config: opts[:provider_config], profile: opts[:profile], profiles: opts[:profiles],
|
|
3193
|
+
provider_attempts: opts.fetch(:provider_attempts, 5), cancel: opts[:cancel])
|
|
3194
|
+
end
|
|
3195
|
+
|
|
3196
|
+
private_class_method def self.cheap_returns_allowed?(opts = {})
|
|
3197
|
+
opts[:force_tools] != true && !opts[:verification_contract]
|
|
3198
|
+
end
|
|
3199
|
+
|
|
3200
|
+
private_class_method def self.final_verification(opts = {})
|
|
3201
|
+
Reward.run_verification(request: opts[:request], session_id: opts[:session_id], contract: opts[:contract])
|
|
3202
|
+
rescue StandardError => e
|
|
3203
|
+
{ status: :unknown, error: "#{e.class}: #{e.message}", checks: [] }
|
|
3128
3204
|
end
|
|
3129
3205
|
|
|
3130
3206
|
private_class_method def self.update_steered_open_goal!(opts = {})
|
|
@@ -3138,7 +3214,7 @@ module PWN
|
|
|
3138
3214
|
system = opts[:system_role_content]
|
|
3139
3215
|
history = opts[:steering_history]
|
|
3140
3216
|
if history
|
|
3141
|
-
authority = '
|
|
3217
|
+
authority = 'Steering adds constraints to the original request. Preserve all original obligations unless an explicit replace: amendment replaces the scope, or cancel ends it. Completed evidence remains valid for unchanged targets; never claim old work was undone.'
|
|
3142
3218
|
return [{ role: 'system', content: "#{system}\n#{authority}" }] + history.reject { |row| row[:role].to_s == 'system' }
|
|
3143
3219
|
end
|
|
3144
3220
|
request = opts[:request]
|
|
@@ -3184,11 +3260,7 @@ module PWN
|
|
|
3184
3260
|
opts[:messages] << {
|
|
3185
3261
|
role: 'tool', tool_call_id: tc[:id], name: name,
|
|
3186
3262
|
content: wrap_untrusted_tool(content: raw),
|
|
3187
|
-
artifact_observations:
|
|
3188
|
-
paths: opts[:paths], before: opts[:before],
|
|
3189
|
-
effect: Dispatch.effect(name: name, args: tc.dig(:function, :arguments)),
|
|
3190
|
-
success: opts[:success]
|
|
3191
|
-
)
|
|
3263
|
+
artifact_observations: opts[:observations] || {}
|
|
3192
3264
|
}
|
|
3193
3265
|
append_session(session_id: opts[:session_id], role: 'tool', content: "#{name} → #{raw}")
|
|
3194
3266
|
opts[:on_tool]&.call(name, tc.dig(:function, :arguments), raw)
|
|
@@ -3242,7 +3314,12 @@ module PWN
|
|
|
3242
3314
|
opts[:request] = request
|
|
3243
3315
|
intent = prepared[:intent]
|
|
3244
3316
|
Thread.current[:pwn_request_intent] = intent
|
|
3245
|
-
|
|
3317
|
+
runtime = Thread.current[:pwn_request_runtime]
|
|
3318
|
+
if runtime && runtime.request == request
|
|
3319
|
+
Thread.current[:pwn_artifact_contract] = runtime.contract
|
|
3320
|
+
else
|
|
3321
|
+
TurnFinalizer.commit_artifacts!(request: request, inherit: nested)
|
|
3322
|
+
end
|
|
3246
3323
|
required_artifacts = TurnFinalizer.required_artifacts(request: request)
|
|
3247
3324
|
Thread.current[:pwn_extinguished] = {}
|
|
3248
3325
|
Thread.current[:pwn_same_payload] = Hash.new(0)
|
|
@@ -3255,7 +3332,7 @@ module PWN
|
|
|
3255
3332
|
expose_current_session(session_id: session_id)
|
|
3256
3333
|
Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
|
|
3257
3334
|
|
|
3258
|
-
allow_text_only = steering.nil? && required_artifacts.empty? && opts
|
|
3335
|
+
allow_text_only = steering.nil? && required_artifacts.empty? && cheap_returns_allowed?(opts)
|
|
3259
3336
|
cheap = allow_text_only && %i[greeting howto recall].include?(intent)
|
|
3260
3337
|
|
|
3261
3338
|
if allow_text_only && intent == :greeting
|
|
@@ -3265,6 +3342,7 @@ module PWN
|
|
|
3265
3342
|
request: request,
|
|
3266
3343
|
session_id: session_id
|
|
3267
3344
|
)
|
|
3345
|
+
runtime&.finish!(status: 'complete')
|
|
3268
3346
|
final_chars = txt.to_s.length
|
|
3269
3347
|
debug_final_text!(text: txt, debug: opts[:debug])
|
|
3270
3348
|
return txt
|
|
@@ -3289,6 +3367,7 @@ module PWN
|
|
|
3289
3367
|
session_id: session_id,
|
|
3290
3368
|
system_role_content: system_role_content
|
|
3291
3369
|
)
|
|
3370
|
+
runtime&.finish!(status: 'complete')
|
|
3292
3371
|
final_chars = txt.to_s.length
|
|
3293
3372
|
debug_final_text!(text: txt, debug: opts[:debug])
|
|
3294
3373
|
return txt
|
|
@@ -3301,6 +3380,7 @@ module PWN
|
|
|
3301
3380
|
session_id: session_id,
|
|
3302
3381
|
system_role_content: system_role_content
|
|
3303
3382
|
)
|
|
3383
|
+
runtime&.finish!(status: 'complete')
|
|
3304
3384
|
final_chars = txt.to_s.length
|
|
3305
3385
|
debug_final_text!(text: txt, debug: opts[:debug])
|
|
3306
3386
|
return txt
|
|
@@ -3459,8 +3539,8 @@ module PWN
|
|
|
3459
3539
|
if engine_transient?(error: e)
|
|
3460
3540
|
engine_blips += 1
|
|
3461
3541
|
debug_progress(msg: "engine hop failed #{e.class}: #{e.message.to_s[0, 240]} blip=#{engine_blips}")
|
|
3462
|
-
if
|
|
3463
|
-
txt =
|
|
3542
|
+
if engine_retry_exhausted?(runtime: runtime, attempts: engine_blips)
|
|
3543
|
+
txt = engine_failure_text(runtime: runtime, attempts: engine_blips, error: e)
|
|
3464
3544
|
debug_final_text!(text: txt)
|
|
3465
3545
|
final_chars = txt.length
|
|
3466
3546
|
return txt
|
|
@@ -3542,53 +3622,48 @@ module PWN
|
|
|
3542
3622
|
return text if solve_handoff?(opts.merge(text: text, calls: calls))
|
|
3543
3623
|
|
|
3544
3624
|
if calls.empty?
|
|
3545
|
-
|
|
3546
|
-
if
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3625
|
+
verification_outcome = nil
|
|
3626
|
+
if opts[:verification_contract]
|
|
3627
|
+
report = final_verification(request: request, session_id: session_id, contract: opts[:verification_contract].merge(actions: verified_actions, answer: text))
|
|
3628
|
+
verification_outcome = Reward.resolve_outcome(outcome: { score: nil, source: :verification, verification: report })
|
|
3629
|
+
on_tool&.call('verification', {}, JSON.generate(report))
|
|
3630
|
+
unless report[:status].to_s == 'pass'
|
|
3631
|
+
status = report[:status].to_s == 'fail' ? 'incomplete' : 'verification_error'
|
|
3632
|
+
runtime&.finish!(status: status, unmet: [report])
|
|
3633
|
+
return "[pwn-ai] #{status}: explicit request verification did not pass."
|
|
3634
|
+
end
|
|
3555
3635
|
end
|
|
3636
|
+
# Completion is an evidence decision, never a prose classifier.
|
|
3556
3637
|
unless may_finalize?(
|
|
3557
3638
|
request: request,
|
|
3558
3639
|
messages: messages,
|
|
3559
3640
|
text: text
|
|
3560
3641
|
)
|
|
3561
3642
|
turn_fails['unsatisfied'] += 1
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
|
|
3567
|
-
debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
|
|
3568
|
-
missing = unmet.any? { |row| row.to_s.start_with?('deliverable_missing:') }
|
|
3569
|
-
nudge = if missing
|
|
3570
|
-
"[pwn-ai] artifact not written; write it now. unmet=#{unmet.join(',')} " \
|
|
3571
|
-
'Keep calling CORE_TOOLS (shell, pwn_eval) until that path exists and is non-empty.'
|
|
3572
|
-
else
|
|
3573
|
-
"[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
|
|
3574
|
-
'Keep calling CORE_TOOLS (shell, pwn_eval) until that request is ' \
|
|
3575
|
-
'done or a tool returned failure evidence. pwn-ai does not decide ' \
|
|
3576
|
-
'authorization. Do not declare completion from a listing or a refusal.'
|
|
3577
|
-
end
|
|
3578
|
-
messages << {
|
|
3579
|
-
role: 'user',
|
|
3580
|
-
content: nudge
|
|
3581
|
-
}
|
|
3582
|
-
next
|
|
3643
|
+
unmet = completion_unmet(request: request, messages: messages)
|
|
3644
|
+
if unmet.any? { |row| row.start_with?('verification_error:') }
|
|
3645
|
+
runtime&.finish!(status: 'verification_error', unmet: unmet)
|
|
3646
|
+
return "[pwn-ai] verification_error: #{unmet.join(',')}"
|
|
3583
3647
|
end
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3648
|
+
if no_tools
|
|
3649
|
+
runtime&.finish!(status: 'blocked', unmet: unmet + ['no_available_tools'])
|
|
3650
|
+
return '[pwn-ai] blocked: requested host work is unverified and no tools are available in this request.'
|
|
3651
|
+
end
|
|
3652
|
+
warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
|
|
3653
|
+
debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
|
|
3654
|
+
missing = unmet.any? { |row| row.to_s.start_with?('deliverable_missing:') }
|
|
3655
|
+
nudge = if missing
|
|
3656
|
+
"[pwn-ai] artifact not written; write it now. unmet=#{unmet.join(',')} " \
|
|
3657
|
+
'Keep calling CORE_TOOLS (shell, pwn_eval) until that path exists and is non-empty.'
|
|
3658
|
+
else
|
|
3659
|
+
"[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
|
|
3660
|
+
'Continue remaining work, or report a concrete tool-evidenced blocker. Do not declare completion from unrelated evidence.'
|
|
3661
|
+
end
|
|
3662
|
+
messages << { role: 'user', content: nudge }
|
|
3663
|
+
next
|
|
3590
3664
|
end
|
|
3591
3665
|
steering&.checkpoint(messages: messages, phase: :finished)
|
|
3666
|
+
runtime&.finish!(status: 'complete')
|
|
3592
3667
|
debug_progress(msg: "final accepted chars=#{text.to_s.length}")
|
|
3593
3668
|
quiet_debug_tui!(reason: 'final')
|
|
3594
3669
|
debug_final_text!(text: text)
|
|
@@ -3617,7 +3692,7 @@ module PWN
|
|
|
3617
3692
|
on_tool: on_tool
|
|
3618
3693
|
)
|
|
3619
3694
|
|
|
3620
|
-
calls.each do |tc|
|
|
3695
|
+
scheduled_tool_calls(runtime: runtime, calls: calls, session_id: session_id, steering: steering, messages: messages).each do |tc, dispatched|
|
|
3621
3696
|
steering&.checkpoint(messages: messages, phase: :tool)
|
|
3622
3697
|
name = tc.dig(:function, :name).to_s
|
|
3623
3698
|
args = tc.dig(:function, :arguments)
|
|
@@ -3638,11 +3713,10 @@ module PWN
|
|
|
3638
3713
|
raw = if same_n >= 3
|
|
3639
3714
|
checkpoint_result(name: name, args: args)
|
|
3640
3715
|
else
|
|
3641
|
-
|
|
3716
|
+
dispatch_scheduled(tool_call: tc, session_id: session_id, result: dispatched)
|
|
3642
3717
|
end
|
|
3643
3718
|
end
|
|
3644
3719
|
tools_called += 1
|
|
3645
|
-
steering_tool_checkpoint!(steering: steering, messages: messages, tool_call: tc, raw: raw, paths: watch_paths, before: before_host, session_id: session_id, on_tool: on_tool)
|
|
3646
3720
|
note_payload_result!(name: name, args: args, raw: raw)
|
|
3647
3721
|
if opts[:verification_contract]
|
|
3648
3722
|
after_artifacts = Verification.snapshot(opts[:verification_contract])
|
|
@@ -3652,8 +3726,12 @@ module PWN
|
|
|
3652
3726
|
tele = record_metrics(name: name, action_id: tc[:id], trusted_context: Policy.current_episode&.dig(:trusted_context) || trusted_context,
|
|
3653
3727
|
started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
|
|
3654
3728
|
result = Result.condition(content: raw, entry: entry, session_id: session_id)
|
|
3655
|
-
|
|
3656
|
-
|
|
3729
|
+
observations = TurnFinalizer.observe_artifacts(
|
|
3730
|
+
paths: watch_paths, before: before_host,
|
|
3731
|
+
effect: Dispatch.effect(name: name, args: args), success: tele[:ok] == true,
|
|
3732
|
+
action_id: tc[:id], tool: name
|
|
3733
|
+
)
|
|
3734
|
+
steering_tool_checkpoint!(steering: steering, messages: messages, tool_call: tc, raw: result, observations: observations, session_id: session_id, on_tool: on_tool)
|
|
3657
3735
|
|
|
3658
3736
|
unless tele[:ok]
|
|
3659
3737
|
fkey = Digest::SHA256.hexdigest("#{name}|#{args}")[0, 16]
|
|
@@ -3686,12 +3764,7 @@ module PWN
|
|
|
3686
3764
|
tool_call_id: tc[:id] || tc['id'] || "call_#{i}",
|
|
3687
3765
|
name: name,
|
|
3688
3766
|
content: wrap_untrusted_tool(content: result),
|
|
3689
|
-
artifact_observations:
|
|
3690
|
-
paths: watch_paths,
|
|
3691
|
-
before: before_host,
|
|
3692
|
-
effect: Dispatch.effect(name: name, args: args),
|
|
3693
|
-
success: tele[:ok] == true
|
|
3694
|
-
)
|
|
3767
|
+
artifact_observations: observations
|
|
3695
3768
|
}
|
|
3696
3769
|
append_session(
|
|
3697
3770
|
session_id: session_id,
|
|
@@ -3863,6 +3936,10 @@ module PWN
|
|
|
3863
3936
|
#{self}.run(
|
|
3864
3937
|
request: 'required - what the human typed',
|
|
3865
3938
|
session_id: 'optional - PWN::Sessions id (transcript is appended to it)',
|
|
3939
|
+
runtime: 'optional - request-owned RequestRuntime, including a restored checkpoint',
|
|
3940
|
+
request_id: 'optional - stable request identity for the durable evidence checkpoint',
|
|
3941
|
+
artifact_root: 'optional - private request checkpoint and context-spill directory',
|
|
3942
|
+
on_outcome: 'optional - callback receiving the persisted structured terminal outcome',
|
|
3866
3943
|
enabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all',
|
|
3867
3944
|
on_tool: 'optional - ->(name, args, result) callback for live UI',
|
|
3868
3945
|
system_role_content: 'optional - override default system prompt (built from session_id if not provided)',
|