pwn 0.5.783 → 0.5.785
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +8 -8
- data/README.md +37 -0
- data/Rakefile +19 -1
- data/bin/pwn +2 -1
- data/bin/pwn_setup +1 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +82 -0
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +64 -35
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +7 -6
- data/documentation/Installation.md +254 -10
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +17 -0
- data/documentation/Persistence.md +8 -6
- data/documentation/Plugin-Degradation.md +12 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Reinforcement-Learning.md +5 -6
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +183 -0
- data/documentation/YubiKey.md +339 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -128
- data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +1245 -22
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/extrospection.rb +6 -7
- data/lib/pwn/ai/agent/learning.rb +62 -25
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy.rb +5 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +35 -16
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +53 -3
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +347 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/cron.rb +120 -49
- data/lib/pwn/migrate.rb +150 -68
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/binary_parser.rb +5 -4
- data/lib/pwn/plugins/ghidra.rb +4 -3
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +21 -3
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +305 -27
- data/lib/pwn/plugins/repl/ai_console.rb +273 -89
- data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
- data/lib/pwn/plugins/repl/mesh.rb +3 -16
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +1 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/sessions.rb +58 -16
- data/lib/pwn/setup.rb +123 -41
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/scripts/build_gate.rb +58 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/build_gate_spec.rb +73 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/persistence_roundtrip_spec.rb +1 -7
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +6 -9
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +31 -3
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +114 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/cron_spec.rb +113 -6
- data/spec/lib/pwn/migrate_spec.rb +108 -5
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_spec.rb +7 -0
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_spec.rb +13 -10
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
- data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
- data/spec/lib/pwn/sessions_spec.rb +112 -0
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_spec.rb +3 -1
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/gate_failure_formatter.rb +14 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +599 -15
- metadata +140 -17
data/lib/pwn/ai/cli.rb
CHANGED
|
@@ -109,12 +109,13 @@ module PWN
|
|
|
109
109
|
prompt = (opts[:input] || $stdin).read if prompt == '-'
|
|
110
110
|
raise ArgumentError, '--ai requires a non-empty prompt' if prompt.strip.empty?
|
|
111
111
|
|
|
112
|
-
outcome = nil
|
|
113
112
|
contract = JSON.parse(File.read(parsed[:verification_contract]), symbolize_names: true) if parsed[:verification_contract]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
response = mission_request(request: prompt, session_id: session[:id], enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
114
|
+
verification_contract: contract, require_resumable: parsed.key?(:resume_session))
|
|
115
|
+
return response[:exit_status] if response[:exit_status]
|
|
116
|
+
|
|
117
|
+
output.puts(response[:final])
|
|
118
|
+
outcome = response[:outcome]
|
|
118
119
|
return outcome && outcome[:status] == 'complete' ? 0 : 1
|
|
119
120
|
else
|
|
120
121
|
PWN::Plugins::REPL.start(ai_session_id: session[:id], ai_session_lease: session[:lease])
|
|
@@ -124,6 +125,34 @@ module PWN
|
|
|
124
125
|
PWN::Sessions.deactivate(lease: session[:lease]) if session
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
private_class_method def self.mission_request(opts = {})
|
|
129
|
+
reader, writer = IO.pipe
|
|
130
|
+
mission = PWN::AI::Agent::MissionNamespace.new(descriptor: { operation: :loop, options: opts }, fds: { 4 => writer })
|
|
131
|
+
writer.close
|
|
132
|
+
raw = reader.read
|
|
133
|
+
mission.wait
|
|
134
|
+
return { exit_status: mission.status.exitstatus || 1 } if raw.empty?
|
|
135
|
+
|
|
136
|
+
response = JSON.parse(raw, symbolize_names: true)
|
|
137
|
+
raise IOError, response[:error] if response[:error]
|
|
138
|
+
|
|
139
|
+
response
|
|
140
|
+
ensure
|
|
141
|
+
mission&.close
|
|
142
|
+
[reader, writer].compact.each { |io| io.close unless io.closed? }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private_class_method def self.mission_entry(opts = {})
|
|
146
|
+
writer = PWN::AI::Agent::MissionNamespace.io(4)
|
|
147
|
+
outcome = nil
|
|
148
|
+
final = PWN::AI::Agent::Loop.run(opts.fetch(:options).merge(on_outcome: ->(row) { outcome = row }))
|
|
149
|
+
writer.write(JSON.generate(final: final, outcome: outcome))
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
writer.write(JSON.generate(error: "#{e.class}: #{e.message}"))
|
|
152
|
+
ensure
|
|
153
|
+
writer.close
|
|
154
|
+
end
|
|
155
|
+
|
|
127
156
|
private_class_method def self.validate_request_options(opts = {})
|
|
128
157
|
parsed = opts[:parsed]
|
|
129
158
|
selected = parsed[:resume_session] || parsed[:replace_session]
|
data/lib/pwn/ai/gemini.rb
CHANGED
|
@@ -238,6 +238,7 @@ module PWN
|
|
|
238
238
|
}]
|
|
239
239
|
http_body[:toolConfig] = gemini_tool_config(choice: opts[:tool_choice]) if opts[:tool_choice]
|
|
240
240
|
end
|
|
241
|
+
http_body = PWN::AI::ServerTools.attach(engine: :gemini, http_body: http_body, request: opts)
|
|
241
242
|
|
|
242
243
|
response = gemini_rest_call(
|
|
243
244
|
http_method: :post,
|
data/lib/pwn/ai/grok.rb
CHANGED
|
@@ -167,27 +167,13 @@ module PWN
|
|
|
167
167
|
dec_path = dec_path.to_s.strip
|
|
168
168
|
dec_path = "#{env_path}.decryptor" if dec_path.empty?
|
|
169
169
|
|
|
170
|
-
unless File.exist?(env_path)
|
|
170
|
+
unless File.exist?(env_path)
|
|
171
171
|
puts '[*] INFO: Grok OAuth tokens updated in this session only; ' \
|
|
172
|
-
"persistence to #{env_path} skipped (missing
|
|
173
|
-
"#{" at #{dec_path}" unless dec_path.empty?})."
|
|
172
|
+
"persistence to #{env_path} skipped (missing vault)."
|
|
174
173
|
return false
|
|
175
174
|
end
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
key = decryptor.is_a?(Hash) ? decryptor[:key] : nil
|
|
179
|
-
iv = decryptor.is_a?(Hash) ? decryptor[:iv] : nil
|
|
180
|
-
unless real_config_value?(value: key) && real_config_value?(value: iv)
|
|
181
|
-
puts '[*] INFO: Grok OAuth tokens updated in this session only; ' \
|
|
182
|
-
"persistence to #{env_path} skipped (decryptor at #{dec_path} " \
|
|
183
|
-
'has no usable key/iv).'
|
|
184
|
-
return false
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
PWN::Plugins::Vault.decrypt(file: env_path, key: key, iv: iv)
|
|
188
|
-
begin
|
|
189
|
-
cfg = YAML.load_file(env_path, symbolize_names: true)
|
|
190
|
-
cfg = {} unless cfg.is_a?(Hash)
|
|
176
|
+
PWN::Plugins::Vault.update(file: env_path, decryptor_file: dec_path) do |cfg|
|
|
191
177
|
cfg[:ai] = {} unless cfg[:ai].is_a?(Hash)
|
|
192
178
|
cfg[:ai][:grok] = {} unless cfg[:ai][:grok].is_a?(Hash)
|
|
193
179
|
vault_oauth = cfg[:ai][:grok][:oauth]
|
|
@@ -196,19 +182,11 @@ module PWN
|
|
|
196
182
|
vault_oauth[:bearer_token] = oauth[:bearer_token]
|
|
197
183
|
vault_oauth[:refresh_token] = oauth[:refresh_token] if real_config_value?(value: oauth[:refresh_token])
|
|
198
184
|
vault_oauth[:expires_at] = oauth[:expires_at] if oauth.key?(:expires_at) && !oauth[:expires_at].nil?
|
|
199
|
-
|
|
200
|
-
# Match PWN::Config.default_env YAML style (string keys, no leading ':').
|
|
201
|
-
yaml_env = YAML.dump(cfg).gsub(/^(\s*):/, '\1')
|
|
202
|
-
File.write(env_path, yaml_env)
|
|
203
|
-
File.chmod(0o600, env_path)
|
|
204
|
-
ensure
|
|
205
|
-
# Always re-encrypt with the IDENTICAL key + iv — never rotate.
|
|
206
|
-
PWN::Plugins::Vault.encrypt(file: env_path, key: key, iv: iv)
|
|
207
185
|
end
|
|
208
186
|
|
|
209
187
|
true
|
|
210
188
|
rescue StandardError => e
|
|
211
|
-
warn "[!] Grok OAuth vault persistence failed (session tokens still updated): #{e.class}
|
|
189
|
+
warn "[!] Grok OAuth vault persistence failed (session tokens still updated): #{e.class}"
|
|
212
190
|
false
|
|
213
191
|
end
|
|
214
192
|
|
|
@@ -496,7 +474,7 @@ module PWN
|
|
|
496
474
|
end
|
|
497
475
|
retry if retry_count < max_attempts
|
|
498
476
|
|
|
499
|
-
raise e if Thread.current[:pwn_provider_attempt_owner]
|
|
477
|
+
raise e if Thread.current[:pwn_provider_attempt_owner] || Thread.current[:pwn_session_title]
|
|
500
478
|
|
|
501
479
|
nil
|
|
502
480
|
end
|
|
@@ -554,6 +532,30 @@ module PWN
|
|
|
554
532
|
nil
|
|
555
533
|
end
|
|
556
534
|
|
|
535
|
+
private_class_method def self.hosted_responses(opts = {})
|
|
536
|
+
input = Array(opts[:messages]).filter_map do |msg|
|
|
537
|
+
next unless msg.is_a?(Hash)
|
|
538
|
+
|
|
539
|
+
role = (msg[:role] || msg['role']).to_s
|
|
540
|
+
content = msg[:content] || msg['content']
|
|
541
|
+
next if content.to_s.empty?
|
|
542
|
+
|
|
543
|
+
{ role: role, content: content.to_s }
|
|
544
|
+
end
|
|
545
|
+
raw = grok_rest_call(
|
|
546
|
+
http_method: :post,
|
|
547
|
+
rest_call: 'responses',
|
|
548
|
+
http_body: { model: opts[:model], input: input, tools: PWN::AI::ServerTools.wire(engine: :grok, names: opts[:names]), stream: false },
|
|
549
|
+
timeout: opts[:timeout],
|
|
550
|
+
spinner: opts[:spinner],
|
|
551
|
+
quiet: opts[:quiet]
|
|
552
|
+
)
|
|
553
|
+
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
554
|
+
text = parsed[:output_text].to_s
|
|
555
|
+
message = { role: 'assistant', content: text }
|
|
556
|
+
{ choices: [{ message: message }], assistant_message: message }
|
|
557
|
+
end
|
|
558
|
+
|
|
557
559
|
# Supported Method Parameters::
|
|
558
560
|
# response = PWN::AI::Grok.chat_with_tools(
|
|
559
561
|
# messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
|
|
@@ -611,6 +613,12 @@ module PWN
|
|
|
611
613
|
http_body[:tool_choice] = opts[:tool_choice] if opts[:tool_choice]
|
|
612
614
|
effort = opts[:reasoning_effort].to_s
|
|
613
615
|
http_body[:reasoning_effort] = effort unless effort.empty? || effort == 'none'
|
|
616
|
+
hosted = Array(opts[:server_tools]).map(&:to_s).reject(&:empty?)
|
|
617
|
+
if hosted.any?
|
|
618
|
+
raise ArgumentError, 'xAI hosted tools use the Responses API and are not mixed into chat/completions client tools' if opts[:tools] && !opts[:tools].empty?
|
|
619
|
+
|
|
620
|
+
return hosted_responses(model: model, messages: messages, names: hosted, timeout: timeout, spinner: spinner, quiet: opts[:quiet])
|
|
621
|
+
end
|
|
614
622
|
|
|
615
623
|
response = grok_rest_call(
|
|
616
624
|
http_method: :post,
|
|
@@ -690,6 +698,8 @@ module PWN
|
|
|
690
698
|
temperature: temp,
|
|
691
699
|
stream: false
|
|
692
700
|
}
|
|
701
|
+
effort = opts[:reasoning_effort].to_s
|
|
702
|
+
http_body[:reasoning_effort] = effort unless effort.empty?
|
|
693
703
|
|
|
694
704
|
if response_history[:choices].length > 1
|
|
695
705
|
response_history[:choices][1..-1].each do |message|
|
|
@@ -787,7 +797,8 @@ module PWN
|
|
|
787
797
|
timeout: 'optional - seconds (default 180)',
|
|
788
798
|
spinner: 'optional - display spinner (default false)',
|
|
789
799
|
quiet: 'optional - quiet value consumed by #chat_with_tools',
|
|
790
|
-
reasoning_effort: 'optional - Grok reasoning effort (default medium; none disables thinking)'
|
|
800
|
+
reasoning_effort: 'optional - Grok reasoning effort (default medium; none disables thinking)',
|
|
801
|
+
server_tools: 'optional - hosted tool names such as web_search; sent on the Responses API only when client tools are omitted'
|
|
791
802
|
)
|
|
792
803
|
|
|
793
804
|
# Run chat and return its result
|
data/lib/pwn/ai/ollama.rb
CHANGED
|
@@ -198,7 +198,7 @@ module PWN
|
|
|
198
198
|
end
|
|
199
199
|
retry if retry_count < max_attempts
|
|
200
200
|
|
|
201
|
-
raise e if Thread.current[:pwn_provider_attempt_owner]
|
|
201
|
+
raise e if Thread.current[:pwn_provider_attempt_owner] || Thread.current[:pwn_session_title]
|
|
202
202
|
|
|
203
203
|
nil
|
|
204
204
|
rescue StandardError => e
|
|
@@ -671,6 +671,7 @@ module PWN
|
|
|
671
671
|
end
|
|
672
672
|
http_body[:tool_choice] = opts[:tool_choice] if opts[:tool_choice]
|
|
673
673
|
http_body[:think] = think_wire(reasoning_effort: opts[:reasoning_effort], think: opts[:think])
|
|
674
|
+
raise ArgumentError, 'Ollama has no server-side tool workspace. Its tools are client-executed.' if Array(opts[:server_tools]).any?
|
|
674
675
|
|
|
675
676
|
response = ollama_rest_call(
|
|
676
677
|
http_method: :post,
|
|
@@ -837,7 +838,8 @@ module PWN
|
|
|
837
838
|
timeout: 'optional - seconds (default 900)',
|
|
838
839
|
spinner: 'optional - display spinner (default false)',
|
|
839
840
|
think: 'optional - true enables Ollama think (default true; false disables)',
|
|
840
|
-
reasoning_effort: 'optional - think level from /api/show thinking.values'
|
|
841
|
+
reasoning_effort: 'optional - think level from /api/show thinking.values',
|
|
842
|
+
server_tools: 'optional - rejected; Ollama has no server-side tool workspace'
|
|
841
843
|
)
|
|
842
844
|
|
|
843
845
|
# Run chat and return its result
|
data/lib/pwn/ai/open_ai.rb
CHANGED
|
@@ -172,7 +172,6 @@ module PWN
|
|
|
172
172
|
# bearer lives in-session only.
|
|
173
173
|
private_class_method def self.persist_oauth_to_vault(opts = {})
|
|
174
174
|
oauth = opts[:oauth]
|
|
175
|
-
return false if Thread.current[:pwn_request_runtime]
|
|
176
175
|
return false unless oauth.is_a?(Hash)
|
|
177
176
|
return false unless real_config_value?(value: oauth[:bearer_token])
|
|
178
177
|
|
|
@@ -187,32 +186,15 @@ module PWN
|
|
|
187
186
|
dec_path = dec_path.to_s.strip
|
|
188
187
|
dec_path = "#{env_path}.decryptor" if dec_path.empty?
|
|
189
188
|
|
|
190
|
-
unless File.exist?(env_path)
|
|
189
|
+
unless File.exist?(env_path)
|
|
191
190
|
puts '[*] INFO: OpenAI OAuth tokens updated in this session only; ' \
|
|
192
|
-
"persistence to #{env_path} skipped (missing
|
|
193
|
-
"#{" at #{dec_path}" unless dec_path.empty?})."
|
|
191
|
+
"persistence to #{env_path} skipped (missing vault)."
|
|
194
192
|
return false
|
|
195
193
|
end
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
unless real_config_value?(value: key) && real_config_value?(value: iv)
|
|
201
|
-
puts '[*] INFO: OpenAI OAuth tokens updated in this session only; ' \
|
|
202
|
-
"persistence to #{env_path} skipped (decryptor at #{dec_path} " \
|
|
203
|
-
'has no usable key/iv).'
|
|
204
|
-
return false
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Work on a private sibling; never decrypt the live vault in place.
|
|
208
|
-
# Rename only after encryption succeeds, preserving the old vault on
|
|
209
|
-
# write/encryption errors and keeping the existing key + iv intact.
|
|
210
|
-
Tempfile.create(['.pwn-openai-oauth-', '.yaml'], File.dirname(env_path)) do |temp|
|
|
211
|
-
temp.write(File.binread(env_path))
|
|
212
|
-
temp.flush
|
|
213
|
-
PWN::Plugins::Vault.decrypt(file: temp.path, key: key, iv: iv)
|
|
214
|
-
cfg = YAML.load_file(temp.path, symbolize_names: true)
|
|
215
|
-
cfg = {} unless cfg.is_a?(Hash)
|
|
195
|
+
# The shared updater resolves hardware or legacy credentials, keeps
|
|
196
|
+
# plaintext in memory and publishes only encrypted bytes atomically.
|
|
197
|
+
PWN::Plugins::Vault.update(file: env_path, decryptor_file: dec_path) do |cfg|
|
|
216
198
|
cfg[:ai] = {} unless cfg[:ai].is_a?(Hash)
|
|
217
199
|
cfg[:ai][:openai] = {} unless cfg[:ai][:openai].is_a?(Hash)
|
|
218
200
|
vault_oauth = cfg[:ai][:openai][:oauth]
|
|
@@ -223,13 +205,6 @@ module PWN
|
|
|
223
205
|
vault_oauth[:id_token] = oauth[:id_token] if real_config_value?(value: oauth[:id_token])
|
|
224
206
|
vault_oauth[:account_id] = oauth[:account_id] if real_config_value?(value: oauth[:account_id])
|
|
225
207
|
vault_oauth[:expires_at] = oauth[:expires_at] if oauth.key?(:expires_at) && !oauth[:expires_at].nil?
|
|
226
|
-
|
|
227
|
-
# Match PWN::Config.default_env YAML style (string keys, no leading ':').
|
|
228
|
-
yaml_env = YAML.dump(cfg).gsub(/^(\s*):/, '\1')
|
|
229
|
-
File.write(temp.path, yaml_env)
|
|
230
|
-
PWN::Plugins::Vault.encrypt(file: temp.path, key: key, iv: iv)
|
|
231
|
-
temp.fsync
|
|
232
|
-
File.rename(temp.path, env_path)
|
|
233
208
|
end
|
|
234
209
|
|
|
235
210
|
true
|
|
@@ -563,7 +538,7 @@ module PWN
|
|
|
563
538
|
end
|
|
564
539
|
retry if retry_count < max_attempts
|
|
565
540
|
|
|
566
|
-
raise e if oauth_selected || Thread.current[:pwn_provider_attempt_owner]
|
|
541
|
+
raise e if oauth_selected || Thread.current[:pwn_provider_attempt_owner] || Thread.current[:pwn_session_title]
|
|
567
542
|
|
|
568
543
|
nil
|
|
569
544
|
end
|
|
@@ -717,6 +692,7 @@ module PWN
|
|
|
717
692
|
reasoning_effort: effort
|
|
718
693
|
)
|
|
719
694
|
end
|
|
695
|
+
http_body = PWN::AI::ServerTools.attach(engine: :openai, http_body: http_body, request: opts)
|
|
720
696
|
|
|
721
697
|
response = open_ai_rest_call(
|
|
722
698
|
http_method: :post,
|
|
@@ -1039,7 +1015,10 @@ module PWN
|
|
|
1039
1015
|
}
|
|
1040
1016
|
# Reasoning models reject sampler params (temperature, top_p, etc.)
|
|
1041
1017
|
http_body[:temperature] = temp unless reasoning
|
|
1042
|
-
|
|
1018
|
+
effort = opts[:reasoning_effort].to_s
|
|
1019
|
+
if reasoning && !effort.empty?
|
|
1020
|
+
http_body[:reasoning_effort] = effort == 'none' ? 'minimal' : effort
|
|
1021
|
+
end
|
|
1043
1022
|
|
|
1044
1023
|
response = open_ai_rest_call(
|
|
1045
1024
|
http_method: :post,
|
data/lib/pwn/ai/open_web_ui.rb
CHANGED
|
@@ -223,7 +223,7 @@ module PWN
|
|
|
223
223
|
end
|
|
224
224
|
retry if retry_count < max_attempts
|
|
225
225
|
|
|
226
|
-
raise e if Thread.current[:pwn_provider_attempt_owner]
|
|
226
|
+
raise e if Thread.current[:pwn_provider_attempt_owner] || Thread.current[:pwn_session_title]
|
|
227
227
|
|
|
228
228
|
nil
|
|
229
229
|
end
|
|
@@ -650,6 +650,47 @@ module PWN
|
|
|
650
650
|
effort
|
|
651
651
|
end
|
|
652
652
|
|
|
653
|
+
# Supported Method Parameters::
|
|
654
|
+
# rows = PWN::AI::OpenWebUI.workspace_tools(
|
|
655
|
+
# refresh: 'optional - true ignores a cached workspace tool list',
|
|
656
|
+
# timeout: 'optional - seconds for the tool list request (default 30)'
|
|
657
|
+
# )
|
|
658
|
+
public_class_method def self.workspace_tools(opts = {})
|
|
659
|
+
refresh = opts[:refresh]
|
|
660
|
+
cached = Thread.current[:pwn_openwebui_workspace_tools]
|
|
661
|
+
return cached if cached && !refresh
|
|
662
|
+
|
|
663
|
+
raw = openwebui_rest_call(http_method: :get, rest_call: 'api/v1/tools/', timeout: opts[:timeout] || 30, quiet: true)
|
|
664
|
+
text = raw.is_a?(String) ? raw : raw.to_s
|
|
665
|
+
parsed = text.strip.empty? ? [] : JSON.parse(text)
|
|
666
|
+
rows = parsed.is_a?(Hash) ? (parsed['data'] || parsed[:data] || []) : parsed
|
|
667
|
+
listed = Array(rows).filter_map do |row|
|
|
668
|
+
next unless row.is_a?(Hash)
|
|
669
|
+
|
|
670
|
+
id = row['id'] || row[:id]
|
|
671
|
+
name = row['name'] || row[:name]
|
|
672
|
+
next if id.to_s.empty?
|
|
673
|
+
|
|
674
|
+
meta = row['meta'] || row[:meta] || {}
|
|
675
|
+
{ id: id.to_s, name: name.to_s, description: meta['description'] || meta[:description] || row['description'] || row[:description] }
|
|
676
|
+
end
|
|
677
|
+
Thread.current[:pwn_openwebui_workspace_tools] = listed
|
|
678
|
+
listed
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
private_class_method def self.server_tool_ids(opts = {})
|
|
682
|
+
names = Array(opts[:names]).map(&:to_s).reject(&:empty?)
|
|
683
|
+
return [] if names.empty?
|
|
684
|
+
|
|
685
|
+
listed = workspace_tools
|
|
686
|
+
names.map do |name|
|
|
687
|
+
row = listed.find { |item| item[:id] == name || item[:name].casecmp?(name) }
|
|
688
|
+
raise ArgumentError, "Open WebUI workspace has no tool #{name}" unless row
|
|
689
|
+
|
|
690
|
+
row[:id]
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
653
694
|
# Supported Method Parameters::
|
|
654
695
|
# response = PWN::AI::OpenWebUI.chat_with_tools(
|
|
655
696
|
# messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
|
|
@@ -704,6 +745,7 @@ module PWN
|
|
|
704
745
|
end
|
|
705
746
|
http_body[:tool_choice] = opts[:tool_choice] if opts[:tool_choice]
|
|
706
747
|
http_body[:think] = think_wire(reasoning_effort: opts[:reasoning_effort], think: opts[:think])
|
|
748
|
+
raise ArgumentError, 'Open WebUI workspace tools do not run on the proxied Ollama chat path. Use chat(server_tools:) or workspace_tool.' if Array(opts[:server_tools]).any?
|
|
707
749
|
|
|
708
750
|
response = openwebui_rest_call(
|
|
709
751
|
http_method: :post,
|
|
@@ -797,6 +839,13 @@ module PWN
|
|
|
797
839
|
messages: http_body[:messages],
|
|
798
840
|
fold_into_user: strict_system_template?(model: model)
|
|
799
841
|
)
|
|
842
|
+
server_ids = server_tool_ids(names: opts[:server_tools])
|
|
843
|
+
if server_ids.any?
|
|
844
|
+
http_body[:stream] = false
|
|
845
|
+
http_body[:tool_ids] = server_ids
|
|
846
|
+
http_body[:params] = { function_calling: 'legacy' }
|
|
847
|
+
http_body.delete(:tools)
|
|
848
|
+
end
|
|
800
849
|
|
|
801
850
|
timeout = opts[:timeout]
|
|
802
851
|
spinner = opts[:spinner]
|
|
@@ -809,7 +858,8 @@ module PWN
|
|
|
809
858
|
spinner: spinner
|
|
810
859
|
)
|
|
811
860
|
|
|
812
|
-
|
|
861
|
+
raw = response.is_a?(String) ? response : response.to_s
|
|
862
|
+
json_resp = JSON.parse(raw, symbolize_names: true)
|
|
813
863
|
assistant_resp = json_resp[:choices].first[:message]
|
|
814
864
|
if assistant_resp.is_a?(Hash)
|
|
815
865
|
content = assistant_resp[:content].to_s
|
|
@@ -857,6 +907,12 @@ module PWN
|
|
|
857
907
|
fallback: 'optional - false skips the catalog list when the direct route misses'
|
|
858
908
|
)
|
|
859
909
|
|
|
910
|
+
# List workspace tools installed for this Open WebUI user, including Ask Jev.
|
|
911
|
+
#{self}.workspace_tools(
|
|
912
|
+
refresh: 'optional - true ignores a cached workspace tool list',
|
|
913
|
+
timeout: 'optional - seconds for the tool list request (default 30)'
|
|
914
|
+
)
|
|
915
|
+
|
|
860
916
|
# Run chat with tools and return its result
|
|
861
917
|
#{self}.chat_with_tools(
|
|
862
918
|
messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
|
|
@@ -867,7 +923,8 @@ module PWN
|
|
|
867
923
|
timeout: 'optional - seconds (default 900)',
|
|
868
924
|
spinner: 'optional - display spinner (default false)',
|
|
869
925
|
think: 'optional - true enables backend think (default true; false disables)',
|
|
870
|
-
reasoning_effort: 'optional - think level when the proxied model publishes named levels'
|
|
926
|
+
reasoning_effort: 'optional - think level when the proxied model publishes named levels',
|
|
927
|
+
server_tools: 'optional - rejected here; workspace tools use chat(server_tools:) or workspace_tool'
|
|
871
928
|
)
|
|
872
929
|
|
|
873
930
|
# Run chat and return its result
|
|
@@ -879,7 +936,8 @@ module PWN
|
|
|
879
936
|
response_history: 'optional - pass response back in to have a conversation',
|
|
880
937
|
speak_answer: 'optional - optional speak answer using PWN::Plugins::Voice.text_to_speech (Default: nil)',
|
|
881
938
|
timeout: 'optional - optional timeout in seconds (defaults to 900)',
|
|
882
|
-
spinner: 'optional - display spinner (defaults to false)'
|
|
939
|
+
spinner: 'optional - display spinner (defaults to false)',
|
|
940
|
+
server_tools: 'optional - workspace tool names or ids, such as Ask Jev; the server executes them and this request omits client tools'
|
|
883
941
|
)
|
|
884
942
|
|
|
885
943
|
# Print the AUTHOR(S) string for this module.
|
data/lib/pwn/ai/router.rb
CHANGED
|
@@ -59,7 +59,7 @@ module PWN
|
|
|
59
59
|
|
|
60
60
|
extractive(text: text)
|
|
61
61
|
rescue StandardError
|
|
62
|
-
|
|
62
|
+
raise if session
|
|
63
63
|
|
|
64
64
|
extractive(text: text)
|
|
65
65
|
end
|
|
@@ -162,15 +162,65 @@ module PWN
|
|
|
162
162
|
mod = Object.const_get(const)
|
|
163
163
|
return nil unless mod.respond_to?(:chat)
|
|
164
164
|
|
|
165
|
-
resp = mod.chat(
|
|
166
|
-
|
|
165
|
+
resp = mod.chat(
|
|
166
|
+
request: opts[:request],
|
|
167
|
+
model: endpoint[:model],
|
|
168
|
+
spinner: false,
|
|
169
|
+
quiet: true,
|
|
170
|
+
timeout: opts[:session] ? 90 : 8,
|
|
171
|
+
reasoning_effort: (opts[:session] ? 'none' : nil)
|
|
172
|
+
)
|
|
173
|
+
text = response_text(response: resp)
|
|
167
174
|
text.strip.empty? ? nil : text
|
|
168
175
|
rescue StandardError
|
|
176
|
+
raise if opts[:session]
|
|
177
|
+
|
|
169
178
|
nil
|
|
170
179
|
ensure
|
|
171
180
|
Thread.current[:pwn_session_title] = previous_title_worker
|
|
172
181
|
end
|
|
173
182
|
|
|
183
|
+
# Only decode documented assistant-content locations, never container inspect
|
|
184
|
+
# strings, reasoning blocks, tool arguments or echoed user messages.
|
|
185
|
+
private_class_method def self.response_text(opts = {})
|
|
186
|
+
response = opts[:response]
|
|
187
|
+
return response if response.is_a?(String)
|
|
188
|
+
return '' unless response.is_a?(Hash)
|
|
189
|
+
|
|
190
|
+
row = response.transform_keys(&:to_s)
|
|
191
|
+
return JSON.generate(row) if row['title'].is_a?(String) && row['summary'].is_a?(String)
|
|
192
|
+
return content_text(content: row['content']) if row.key?('content')
|
|
193
|
+
|
|
194
|
+
choice = Array(row['choices']).last
|
|
195
|
+
if choice.is_a?(Hash)
|
|
196
|
+
choice = choice.transform_keys(&:to_s)
|
|
197
|
+
message = choice['message'] || choice
|
|
198
|
+
if message.is_a?(Hash)
|
|
199
|
+
message = message.transform_keys(&:to_s)
|
|
200
|
+
return '' if message['role'] && message['role'] != 'assistant'
|
|
201
|
+
|
|
202
|
+
return content_text(content: message['content'])
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
message = row['message']
|
|
206
|
+
return content_text(content: message[:content] || message['content']) if message.is_a?(Hash)
|
|
207
|
+
|
|
208
|
+
''
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private_class_method def self.content_text(opts = {})
|
|
212
|
+
content = opts[:content]
|
|
213
|
+
return content if content.is_a?(String)
|
|
214
|
+
return '' unless content.is_a?(Array)
|
|
215
|
+
|
|
216
|
+
content.filter_map do |block|
|
|
217
|
+
next unless block.is_a?(Hash)
|
|
218
|
+
|
|
219
|
+
block = block.transform_keys(&:to_s)
|
|
220
|
+
block['text'] if %w[text output_text].include?(block['type']) && block['text'].is_a?(String)
|
|
221
|
+
end.join
|
|
222
|
+
end
|
|
223
|
+
|
|
174
224
|
private_class_method def self.active_session_endpoint
|
|
175
225
|
engine = env_dig(keys: %i[ai active]).to_s.downcase
|
|
176
226
|
{ engine: engine, model: env_dig(keys: [:ai, engine.to_sym, :model]) }
|