kward 0.83.0 → 0.85.0
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/CHANGELOG.md +102 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
data/lib/kward/conversation.rb
CHANGED
|
@@ -3,7 +3,7 @@ require "set"
|
|
|
3
3
|
require_relative "context_budget_meter"
|
|
4
4
|
require_relative "image_attachments"
|
|
5
5
|
require_relative "message_access"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "plugins/registry"
|
|
7
7
|
require_relative "prompts"
|
|
8
8
|
|
|
9
9
|
# Namespace for the Kward CLI agent runtime.
|
|
@@ -176,9 +176,38 @@ module Kward
|
|
|
176
176
|
"toolout_#{digest}"
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
+
# Scopes plugin instructions to one host-owned turn, including retries.
|
|
180
|
+
# The overlay is deliberately not persisted or folded into the base prompt.
|
|
181
|
+
def with_system_turn(request)
|
|
182
|
+
previous = @system_turn
|
|
183
|
+
raise ArgumentError, "A system-instruction turn is already active" if previous
|
|
184
|
+
|
|
185
|
+
@system_turn = request
|
|
186
|
+
yield
|
|
187
|
+
ensure
|
|
188
|
+
@system_turn = previous
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def append_system_turn(request)
|
|
192
|
+
append_message(request.history_message)
|
|
193
|
+
end
|
|
194
|
+
|
|
179
195
|
# @return [Array<Hash>] provider request context: current system prompt plus durable transcript
|
|
180
196
|
def context_messages
|
|
181
|
-
|
|
197
|
+
instruction = { role: "system", content: @system_turn.system, system_turn: true } if @system_turn
|
|
198
|
+
inserted = false
|
|
199
|
+
messages = @messages.filter_map do |message|
|
|
200
|
+
record = message[:plugin_system_turn] || message["plugin_system_turn"]
|
|
201
|
+
next message unless record
|
|
202
|
+
next unless @system_turn && (record[:id] || record["id"]) == @system_turn.id
|
|
203
|
+
|
|
204
|
+
inserted = true
|
|
205
|
+
instruction
|
|
206
|
+
end
|
|
207
|
+
# Compaction can remove the history marker; reissue the current directive
|
|
208
|
+
# after the compacted context rather than demoting it to the base prompt.
|
|
209
|
+
messages << instruction if instruction && !inserted
|
|
210
|
+
@system_message ? [@system_message] + messages : messages
|
|
182
211
|
end
|
|
183
212
|
|
|
184
213
|
# Rebuilds the system message from current config, memory, plugins, and
|
data/lib/kward/model/client.rb
CHANGED
|
@@ -8,7 +8,7 @@ require_relative "../auth/openai_oauth"
|
|
|
8
8
|
require_relative "../cancellation"
|
|
9
9
|
require_relative "../config_files"
|
|
10
10
|
require_relative "../http"
|
|
11
|
-
require_relative "
|
|
11
|
+
require_relative "openrouter_model_cache"
|
|
12
12
|
require_relative "context_overflow"
|
|
13
13
|
require_relative "azure_openai_config"
|
|
14
14
|
require_relative "copilot_models"
|
|
@@ -37,12 +37,12 @@ module Kward
|
|
|
37
37
|
"lm_studio" => "http://127.0.0.1:1234/v1",
|
|
38
38
|
"llama_cpp" => "http://127.0.0.1:8080/v1"
|
|
39
39
|
}.freeze
|
|
40
|
-
AUTH_ERROR = "No OpenAI OAuth login found. Run `
|
|
41
|
-
OPENROUTER_AUTH_ERROR = "No OpenRouter API key found.
|
|
42
|
-
OPENAI_API_AUTH_ERROR = "No OpenAI API key found.
|
|
43
|
-
AZURE_OPENAI_AUTH_ERROR = "No Azure OpenAI API key found.
|
|
44
|
-
COPILOT_AUTH_ERROR = "No GitHub Copilot OAuth login found. Run `
|
|
45
|
-
ANTHROPIC_AUTH_ERROR = "No Anthropic OAuth login found. Run `
|
|
40
|
+
AUTH_ERROR = "No OpenAI OAuth login found. Run `kward login`, or set OPENAI_ACCESS_TOKEN or OPENROUTER_API_KEY."
|
|
41
|
+
OPENROUTER_AUTH_ERROR = "No OpenRouter API key found. Run `kward login openrouter`, or set OPENROUTER_API_KEY."
|
|
42
|
+
OPENAI_API_AUTH_ERROR = "No OpenAI API key found. Run `kward login`, or set OPENAI_API_KEY."
|
|
43
|
+
AZURE_OPENAI_AUTH_ERROR = "No Azure OpenAI API key found. Run `kward login`, or set AZURE_OPENAI_API_KEY."
|
|
44
|
+
COPILOT_AUTH_ERROR = "No GitHub Copilot OAuth login found. Run `kward login github`, or set COPILOT_GITHUB_TOKEN."
|
|
45
|
+
ANTHROPIC_AUTH_ERROR = "No Anthropic OAuth login found. Run `kward login anthropic`."
|
|
46
46
|
DEFAULT_OPENAI_MODEL = ModelInfo::DEFAULT_OPENAI_MODEL
|
|
47
47
|
DEFAULT_REASONING_EFFORT = ModelInfo::DEFAULT_REASONING_EFFORT
|
|
48
48
|
RETRY_DELAYS = [1, 2].freeze
|
|
@@ -61,6 +61,8 @@ module Kward
|
|
|
61
61
|
/(?:exceeded|reached).*(?:usage|quota|credit|budget|balance)/i
|
|
62
62
|
].freeze
|
|
63
63
|
|
|
64
|
+
AuthenticationError = Class.new(RuntimeError)
|
|
65
|
+
|
|
64
66
|
RequestError = Class.new(StandardError) do
|
|
65
67
|
attr_reader :provider, :code, :body
|
|
66
68
|
|
|
@@ -108,11 +110,12 @@ module Kward
|
|
|
108
110
|
@local_models = nil
|
|
109
111
|
end
|
|
110
112
|
|
|
111
|
-
def chat(messages, tools: [], on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, on_retry: nil, cancellation: nil, steering: nil, max_tokens: nil, provider: nil, model: nil, reasoning: nil)
|
|
113
|
+
def chat(messages, tools: [], on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, on_retry: nil, cancellation: nil, steering: nil, max_tokens: nil, provider: nil, model: nil, reasoning: nil, provider_required: false)
|
|
112
114
|
cancellation&.raise_if_cancelled!
|
|
113
115
|
requested_provider = provider
|
|
116
|
+
validate_required_provider!(requested_provider) if provider_required
|
|
114
117
|
url, token, resolved_provider, account_id = credentials(provider: requested_provider)
|
|
115
|
-
if token.to_s.empty? && authentication_required?(resolved_provider) && !requested_provider.to_s.empty? && resolved_provider != "OpenAI"
|
|
118
|
+
if token.to_s.empty? && authentication_required?(resolved_provider) && !provider_required && !requested_provider.to_s.empty? && resolved_provider != "OpenAI"
|
|
116
119
|
url, token, resolved_provider, account_id = credentials
|
|
117
120
|
model = nil
|
|
118
121
|
reasoning = nil
|
|
@@ -593,20 +596,22 @@ module Kward
|
|
|
593
596
|
end
|
|
594
597
|
|
|
595
598
|
def auth_error_for(provider)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
599
|
+
message =
|
|
600
|
+
case provider
|
|
601
|
+
when "Azure OpenAI"
|
|
602
|
+
AZURE_OPENAI_AUTH_ERROR
|
|
603
|
+
when "OpenAI"
|
|
604
|
+
OPENAI_API_AUTH_ERROR
|
|
605
|
+
when "OpenRouter"
|
|
606
|
+
OPENROUTER_AUTH_ERROR
|
|
607
|
+
when "Copilot"
|
|
608
|
+
COPILOT_AUTH_ERROR
|
|
609
|
+
when "Anthropic"
|
|
610
|
+
ANTHROPIC_AUTH_ERROR
|
|
611
|
+
else
|
|
612
|
+
AUTH_ERROR
|
|
613
|
+
end
|
|
614
|
+
AuthenticationError.new(message)
|
|
610
615
|
end
|
|
611
616
|
|
|
612
617
|
def with_retries(provider, model, request_bytes: nil, on_retry: nil, cancellation: nil)
|
|
@@ -707,7 +712,7 @@ module Kward
|
|
|
707
712
|
end
|
|
708
713
|
|
|
709
714
|
def copilot_responses_model?(model)
|
|
710
|
-
model.to_s.match?(/\Agpt-5(?:\.|-|\z)/)
|
|
715
|
+
model.to_s.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/)
|
|
711
716
|
end
|
|
712
717
|
|
|
713
718
|
def model_entry(provider, id, current: false, metadata: nil)
|
|
@@ -1063,6 +1068,13 @@ module Kward
|
|
|
1063
1068
|
ModelInfo.reasoning_effort(config: @config, provider: provider)
|
|
1064
1069
|
end
|
|
1065
1070
|
|
|
1071
|
+
def validate_required_provider!(provider)
|
|
1072
|
+
provider_id = provider.to_s.strip.downcase
|
|
1073
|
+
return if ProviderCatalog.configuration_ids.include?(provider_id)
|
|
1074
|
+
|
|
1075
|
+
raise ArgumentError, "Unknown provider: #{provider}"
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1066
1078
|
def authentication_required?(provider)
|
|
1067
1079
|
provider != "Local"
|
|
1068
1080
|
end
|
|
@@ -14,7 +14,7 @@ module Kward
|
|
|
14
14
|
|
|
15
15
|
def supported?(model)
|
|
16
16
|
text = model.to_s
|
|
17
|
-
text.match?(/\Agpt-5(?:\.|-|\z)/) || text.match?(/\A(?:gemini-|gpt-4\.1|oswe-)/)
|
|
17
|
+
text.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/) || text.match?(/\A(?:gemini-|gpt-4\.1|oswe-)/)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def supported_choices(choices)
|
|
@@ -24,7 +24,7 @@ module Kward
|
|
|
24
24
|
def resolved_chat_model(configured_model, choices)
|
|
25
25
|
return configured_model if choices.empty? || choices.include?(configured_model)
|
|
26
26
|
|
|
27
|
-
choices.find { |model| supported?(model) } || raise("No Copilot models supported by Kward are available for this account. Kward currently supports Copilot GPT-5 Responses and Gemini/GPT-4.1 chat models.")
|
|
27
|
+
choices.find { |model| supported?(model) } || raise("No Copilot models supported by Kward are available for this account. Kward currently supports Copilot GPT-5/GPT-6 Responses and Gemini/GPT-4.1 chat models.")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def catalog_entries(body)
|
|
@@ -9,7 +9,7 @@ module Kward
|
|
|
9
9
|
DEFAULT_COPILOT_MODEL = "gpt-5-mini"
|
|
10
10
|
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5"
|
|
11
11
|
DEFAULT_REASONING_EFFORT = "medium"
|
|
12
|
-
OPENAI_MODEL_CHOICES = %w[gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
12
|
+
OPENAI_MODEL_CHOICES = %w[gpt-6-astra gpt-6.1-sol gpt-6-luna gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
13
13
|
ANTHROPIC_MODEL_CHOICES = %w[
|
|
14
14
|
claude-fable-5
|
|
15
15
|
claude-opus-5
|
|
@@ -24,6 +24,7 @@ module Kward
|
|
|
24
24
|
].freeze
|
|
25
25
|
COPILOT_MODEL_CHOICES = %w[
|
|
26
26
|
gpt-5-mini
|
|
27
|
+
gpt-6-astra
|
|
27
28
|
gpt-5.3-codex
|
|
28
29
|
gpt-5.4
|
|
29
30
|
gpt-5.4-mini
|
|
@@ -54,6 +55,14 @@ module Kward
|
|
|
54
55
|
*OPENAI_REASONING_EFFORT_CHOICES,
|
|
55
56
|
["max", "Max"]
|
|
56
57
|
].freeze
|
|
58
|
+
GPT_6_REASONING_EFFORT_CHOICES = [
|
|
59
|
+
*REASONING_EFFORT_CHOICES,
|
|
60
|
+
["max", "Max"]
|
|
61
|
+
].freeze
|
|
62
|
+
GPT_6_LUNA_REASONING_EFFORT_CHOICES = [
|
|
63
|
+
*OPENAI_REASONING_EFFORT_CHOICES,
|
|
64
|
+
["max", "Max"]
|
|
65
|
+
].freeze
|
|
57
66
|
ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES = [
|
|
58
67
|
["low", "Low"],
|
|
59
68
|
["medium", "Medium"],
|
|
@@ -78,6 +87,7 @@ module Kward
|
|
|
78
87
|
].freeze
|
|
79
88
|
|
|
80
89
|
CODEX_CONTEXT_WINDOWS = [
|
|
90
|
+
[/\Agpt-6(?:\.1)?-(?:astra|sol|luna)(?:\z|-)/, 1_050_000],
|
|
81
91
|
[/\Agpt-5\.6/, 1_050_000],
|
|
82
92
|
[/\Agpt-5\.5/, 400_000],
|
|
83
93
|
[/\Agpt-5\.4-mini/, 400_000],
|
|
@@ -96,6 +106,7 @@ module Kward
|
|
|
96
106
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
97
107
|
].freeze
|
|
98
108
|
OPENAI_CONTEXT_WINDOWS = [
|
|
109
|
+
[/\Agpt-6(?:\.1)?-(?:astra|sol|luna)(?:\z|-)/, 1_050_000],
|
|
99
110
|
[/\Agpt-5\.6/, 1_050_000],
|
|
100
111
|
[/\Agpt-5\.5/, 1_050_000],
|
|
101
112
|
[/\Agpt-5\.4-mini/, 400_000],
|
|
@@ -423,10 +434,12 @@ module Kward
|
|
|
423
434
|
case provider
|
|
424
435
|
when "Codex", "OpenRouter"
|
|
425
436
|
true
|
|
437
|
+
when "OpenAI"
|
|
438
|
+
id.to_s.match?(/\Agpt-6(?:\.1)?-(?:astra|sol|luna)(?:\z|-)/)
|
|
426
439
|
when "Anthropic"
|
|
427
440
|
!reasoning_effort_choices(provider, id).empty?
|
|
428
441
|
when "Copilot"
|
|
429
|
-
id.to_s.match?(/\Agpt-5(?:\.|-|\z)/)
|
|
442
|
+
id.to_s.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/)
|
|
430
443
|
else
|
|
431
444
|
false
|
|
432
445
|
end
|
|
@@ -436,10 +449,12 @@ module Kward
|
|
|
436
449
|
case provider
|
|
437
450
|
when "Codex", "OpenRouter"
|
|
438
451
|
openai_reasoning_effort_choices(id)
|
|
452
|
+
when "OpenAI"
|
|
453
|
+
reasoning_supported?(provider, id) ? openai_reasoning_effort_choices(id) : []
|
|
439
454
|
when "Anthropic"
|
|
440
455
|
anthropic_reasoning_effort_choices(id)
|
|
441
456
|
when "Copilot"
|
|
442
|
-
id.to_s.match?(/\Agpt-5(?:\.|-|\z)/) ? openai_reasoning_effort_choices(id) : []
|
|
457
|
+
id.to_s.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/) ? openai_reasoning_effort_choices(id) : []
|
|
443
458
|
else
|
|
444
459
|
[]
|
|
445
460
|
end
|
|
@@ -447,6 +462,8 @@ module Kward
|
|
|
447
462
|
|
|
448
463
|
def openai_reasoning_effort_choices(id)
|
|
449
464
|
text = id.to_s.delete_prefix("openai/")
|
|
465
|
+
return GPT_6_LUNA_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-6-luna(?:\z|-)/)
|
|
466
|
+
return GPT_6_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-6\.1-sol(?:\z|-)|\Agpt-6-astra(?:\z|-)/)
|
|
450
467
|
return REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.[23]-codex/)
|
|
451
468
|
return OPENAI_MAX_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.6(?:\z|-)/)
|
|
452
469
|
|
|
@@ -3,9 +3,9 @@ require "json"
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "time"
|
|
5
5
|
require "uri"
|
|
6
|
-
require_relative "config_files"
|
|
7
|
-
require_relative "http"
|
|
8
|
-
require_relative "private_file"
|
|
6
|
+
require_relative "../config_files"
|
|
7
|
+
require_relative "../http"
|
|
8
|
+
require_relative "../private_file"
|
|
9
9
|
|
|
10
10
|
# Namespace for the Kward CLI agent runtime.
|
|
11
11
|
module Kward
|
data/lib/kward/model/payloads.rb
CHANGED
|
@@ -95,6 +95,8 @@ module Kward
|
|
|
95
95
|
|
|
96
96
|
def anthropic_payload(messages, tools, max_tokens: nil, model: nil, reasoning: nil)
|
|
97
97
|
parts = build_context_parts("Anthropic", messages, tools, model: model)
|
|
98
|
+
raise ArgumentError, "Anthropic requires conversation messages; a system-only turn is unavailable in an empty session" if parts[:messages].empty?
|
|
99
|
+
|
|
98
100
|
system = [{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }]
|
|
99
101
|
system << { type: "text", text: parts[:system] } unless parts[:system].to_s.empty?
|
|
100
102
|
payload = {
|
|
@@ -122,6 +124,8 @@ module Kward
|
|
|
122
124
|
contents: messages.filter_map { |message| gemini_message(message) },
|
|
123
125
|
tools: tools.empty? ? nil : [{ functionDeclarations: tools.map { |tool| gemini_tool_schema(tool) } }]
|
|
124
126
|
}.compact
|
|
127
|
+
raise ArgumentError, "Gemini requires conversation contents; a system-only turn is unavailable in an empty session" if payload[:contents].empty?
|
|
128
|
+
|
|
125
129
|
payload[:systemInstruction] = { parts: system_parts } unless system_parts.empty?
|
|
126
130
|
payload[:generationConfig] = { maxOutputTokens: max_tokens.to_i } if max_tokens.to_i.positive?
|
|
127
131
|
payload
|
|
@@ -219,7 +223,8 @@ module Kward
|
|
|
219
223
|
tools: tools.map { |tool| codex_tool_schema(tool) }
|
|
220
224
|
}
|
|
221
225
|
elsif ["Codex", "OpenAI"].include?(provider)
|
|
222
|
-
|
|
226
|
+
# The Codex subscription endpoint rejects inline system messages.
|
|
227
|
+
instructions, input = codex_messages(messages, system_turn_role: provider == "Codex" ? "developer" : "system")
|
|
223
228
|
{
|
|
224
229
|
provider: provider,
|
|
225
230
|
model: model_for(provider, override_model: model),
|
|
@@ -335,7 +340,7 @@ module Kward
|
|
|
335
340
|
}
|
|
336
341
|
end
|
|
337
342
|
|
|
338
|
-
def codex_messages(messages)
|
|
343
|
+
def codex_messages(messages, system_turn_role: "system")
|
|
339
344
|
instructions = []
|
|
340
345
|
input = []
|
|
341
346
|
|
|
@@ -344,7 +349,11 @@ module Kward
|
|
|
344
349
|
content = MessageAccess.content(message) || ""
|
|
345
350
|
case role.to_s
|
|
346
351
|
when "system"
|
|
347
|
-
|
|
352
|
+
if MessageAccess.value(message, :system_turn)
|
|
353
|
+
input << codex_message(system_turn_role, plain_content(content).to_s)
|
|
354
|
+
else
|
|
355
|
+
instructions << plain_content(content).to_s
|
|
356
|
+
end
|
|
348
357
|
when "tool", "toolResult"
|
|
349
358
|
input << {
|
|
350
359
|
type: "function_call_output",
|
|
@@ -71,9 +71,14 @@ module Kward
|
|
|
71
71
|
Definition.new(id: "xai", name: "xAI", api_key_env: ["XAI_API_KEY"], protocol: "openai_chat", oauth_name: "xAI Grok"),
|
|
72
72
|
Definition.new(id: "copilot", name: "GitHub Copilot", api_key_env: [], protocol: "copilot", oauth_name: "GitHub Copilot")
|
|
73
73
|
].freeze
|
|
74
|
+
CONFIGURATION_IDS = (["codex", "openai_api", "local"] + DEFINITIONS.map(&:id).reject { |id| id == "openai" }).freeze
|
|
74
75
|
|
|
75
76
|
module_function
|
|
76
77
|
|
|
78
|
+
def configuration_ids
|
|
79
|
+
CONFIGURATION_IDS
|
|
80
|
+
end
|
|
81
|
+
|
|
77
82
|
def all
|
|
78
83
|
DEFINITIONS
|
|
79
84
|
end
|
|
@@ -347,7 +347,7 @@ module Kward
|
|
|
347
347
|
when "response.content_part.added"
|
|
348
348
|
codex_content_part_added(state, event["part"])
|
|
349
349
|
when "response.output_text.delta", "response.refusal.delta"
|
|
350
|
-
codex_output_text_delta(state, event["delta"], on_assistant_delta: on_assistant_delta)
|
|
350
|
+
codex_output_text_delta(state, event["delta"], on_reasoning_delta: on_reasoning_delta, on_assistant_delta: on_assistant_delta)
|
|
351
351
|
when "response.reasoning_summary_part.added"
|
|
352
352
|
codex_reasoning_summary_part_added(state, event["part"])
|
|
353
353
|
when "response.reasoning_summary_text.delta"
|
|
@@ -417,7 +417,7 @@ module Kward
|
|
|
417
417
|
state[:current_text_content_part] = item["content"].last
|
|
418
418
|
end
|
|
419
419
|
|
|
420
|
-
def codex_output_text_delta(state, delta, on_assistant_delta: nil)
|
|
420
|
+
def codex_output_text_delta(state, delta, on_reasoning_delta: nil, on_assistant_delta: nil)
|
|
421
421
|
text = delta.to_s
|
|
422
422
|
return if text.empty?
|
|
423
423
|
|
|
@@ -432,6 +432,9 @@ module Kward
|
|
|
432
432
|
if codex_streamable_message_item?(item)
|
|
433
433
|
on_assistant_delta&.call(text)
|
|
434
434
|
state[:emitted_message_keys] << codex_item_key(item)
|
|
435
|
+
elsif codex_commentary_message_item?(item)
|
|
436
|
+
on_reasoning_delta&.call(text)
|
|
437
|
+
state[:emitted_message_keys] << codex_item_key(item)
|
|
435
438
|
end
|
|
436
439
|
end
|
|
437
440
|
state[:raw_content] << text
|
|
@@ -647,10 +650,19 @@ module Kward
|
|
|
647
650
|
case item["type"]
|
|
648
651
|
when "message"
|
|
649
652
|
text = text_from_codex_items([item])
|
|
650
|
-
return if text.empty?
|
|
653
|
+
return if text.empty?
|
|
651
654
|
|
|
652
|
-
state[:content] << text
|
|
653
655
|
key = codex_item_key(item)
|
|
656
|
+
if codex_commentary_message_item?(item)
|
|
657
|
+
unless state[:emitted_message_keys].include?(key)
|
|
658
|
+
on_reasoning_delta&.call(text)
|
|
659
|
+
state[:emitted_message_keys] << key
|
|
660
|
+
end
|
|
661
|
+
return
|
|
662
|
+
end
|
|
663
|
+
return unless codex_streamable_message_item?(item)
|
|
664
|
+
|
|
665
|
+
state[:content] << text
|
|
654
666
|
unless state[:emitted_message_keys].include?(key)
|
|
655
667
|
on_assistant_delta&.call(text)
|
|
656
668
|
state[:emitted_message_keys] << key
|
|
@@ -774,6 +786,10 @@ module Kward
|
|
|
774
786
|
phase.empty? || phase == "final_answer"
|
|
775
787
|
end
|
|
776
788
|
|
|
789
|
+
def codex_commentary_message_item?(item)
|
|
790
|
+
codex_message_phase(item) == "commentary"
|
|
791
|
+
end
|
|
792
|
+
|
|
777
793
|
def codex_message_phase(item)
|
|
778
794
|
item["phase"].to_s
|
|
779
795
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "../cancellation"
|
|
5
|
+
require_relative "../http"
|
|
6
|
+
|
|
7
|
+
module Kward
|
|
8
|
+
# HTTP client for TypeSafe's structured System One evaluation endpoint.
|
|
9
|
+
class TypeSafeClient
|
|
10
|
+
DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
|
|
11
|
+
DEFAULT_MODEL = "jev-latest"
|
|
12
|
+
MAX_STATE_BYTES = 100_000
|
|
13
|
+
MAX_QUESTIONS = 100
|
|
14
|
+
HTTP_TIMEOUT_SECONDS = 30
|
|
15
|
+
|
|
16
|
+
class RequestError < StandardError
|
|
17
|
+
attr_reader :status
|
|
18
|
+
|
|
19
|
+
def initialize(status:, message:)
|
|
20
|
+
@status = status.to_i
|
|
21
|
+
super(message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(api_key: ENV["TYPESAFE_API_KEY"], endpoint: DEFAULT_ENDPOINT, http: Net::HTTP)
|
|
26
|
+
@api_key = api_key.to_s.strip
|
|
27
|
+
@endpoint = URI(endpoint)
|
|
28
|
+
@http = http
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def available?
|
|
32
|
+
!@api_key.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def evaluate(state:, questions:, model: DEFAULT_MODEL, cancellation: nil)
|
|
36
|
+
raise ArgumentError, "TypeSafe API key is not configured" unless available?
|
|
37
|
+
|
|
38
|
+
cancellation&.raise_if_cancelled!
|
|
39
|
+
request = Net::HTTP::Post.new(@endpoint)
|
|
40
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
41
|
+
request["Content-Type"] = "application/json"
|
|
42
|
+
request["Accept"] = "application/json"
|
|
43
|
+
Http.apply_user_agent(request)
|
|
44
|
+
request.body = JSON.dump({ state: state, model: model, questions: questions })
|
|
45
|
+
|
|
46
|
+
response_body = nil
|
|
47
|
+
@http.start(@endpoint.hostname, @endpoint.port, use_ssl: @endpoint.scheme == "https", read_timeout: HTTP_TIMEOUT_SECONDS) do |http|
|
|
48
|
+
cancellation&.on_cancel { close_http(http) }
|
|
49
|
+
cancellation&.raise_if_cancelled!
|
|
50
|
+
response = http.request(request)
|
|
51
|
+
response_body = response.body.to_s
|
|
52
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
53
|
+
raise RequestError.new(status: response.code, message: error_message(response.code, response_body))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
cancellation&.raise_if_cancelled!
|
|
57
|
+
JSON.parse(response_body)
|
|
58
|
+
rescue JSON::ParserError
|
|
59
|
+
raise RequestError.new(status: 200, message: "TypeSafe returned invalid JSON")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def error_message(status, body)
|
|
65
|
+
detail = JSON.parse(body).fetch("error", nil) rescue nil
|
|
66
|
+
detail = detail["message"] if detail.is_a?(Hash)
|
|
67
|
+
detail = detail.to_s unless detail.nil?
|
|
68
|
+
detail = "request failed" if detail.nil? || detail.empty?
|
|
69
|
+
"TypeSafe request failed (#{status}): #{detail[0, 500]}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def close_http(http)
|
|
73
|
+
http.finish if http&.started?
|
|
74
|
+
rescue IOError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
<button class="icon-button" id="open-sessions" type="button" aria-label="Open sessions">☰</button>
|
|
163
163
|
<div class="title-block">
|
|
164
164
|
<div class="title-row"><span class="live-dot"></span><h1 id="conversation-title">New session</h1></div>
|
|
165
|
-
<div class="top-meta" id="session-meta">
|
|
165
|
+
<div class="top-meta" id="session-meta">Authenticated HTTP workspace</div>
|
|
166
166
|
</div>
|
|
167
167
|
<button class="text-button" id="capture-skill" type="button">Capture skill</button>
|
|
168
168
|
<button class="text-button" id="rename-session" type="button">Rename</button>
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
<div class="composer-shell">
|
|
177
177
|
<form id="prompt-form">
|
|
178
178
|
<div class="composer">
|
|
179
|
-
<textarea id="prompt" name="prompt" rows="1" autocomplete="off" enterkeyhint="send" aria-label="Prompt" placeholder="
|
|
179
|
+
<textarea id="prompt" name="prompt" rows="1" autocomplete="off" enterkeyhint="send" aria-label="Prompt" placeholder="Ask <%= ERB::Util.html_escape(@assistant_label) %> to inspect, explain, or change code…"></textarea>
|
|
180
180
|
<button class="send" id="send" type="submit" aria-label="Send prompt">↑</button>
|
|
181
181
|
</div>
|
|
182
182
|
<div class="composer-meta"><span id="status">Connecting…</span><span>Return to send · Shift Return for a new line</span></div>
|
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
if (!activeSession) return;
|
|
438
438
|
titleEl.textContent = activeSession.title || activeSession.name || 'New session';
|
|
439
439
|
const details = [activeSession.model, activeSession.messageCount != null ? `${activeSession.messageCount} messages` : null].filter(Boolean);
|
|
440
|
-
sessionMeta.textContent = details.join(' · ') || '
|
|
440
|
+
sessionMeta.textContent = details.join(' · ') || 'Authenticated HTTP workspace';
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
async function changeSession(action, values = {}) {
|
data/lib/kward/pan/server.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require "erb"
|
|
3
|
+
require "ipaddr"
|
|
3
4
|
require "json"
|
|
4
5
|
require "socket"
|
|
5
6
|
require "thread"
|
|
@@ -9,22 +10,22 @@ require_relative "../agent"
|
|
|
9
10
|
require_relative "../config_files"
|
|
10
11
|
require_relative "../events"
|
|
11
12
|
require_relative "../model/retry_message"
|
|
12
|
-
require_relative "../
|
|
13
|
+
require_relative "../plugins/registry"
|
|
13
14
|
require_relative "../prompts/commands"
|
|
14
15
|
require_relative "../rpc/transcript_normalizer"
|
|
15
|
-
require_relative "../
|
|
16
|
+
require_relative "../sessions/store"
|
|
16
17
|
require_relative "../skills/capture"
|
|
17
|
-
require_relative "../
|
|
18
|
+
require_relative "../sessions/trash"
|
|
18
19
|
require_relative "../tools/tool_call"
|
|
19
20
|
require_relative "../tools/registry"
|
|
20
21
|
require_relative "../version"
|
|
21
|
-
require_relative "../
|
|
22
|
+
require_relative "../workspace/factory"
|
|
22
23
|
|
|
23
24
|
# Namespace for the Kward CLI agent runtime.
|
|
24
25
|
module Kward
|
|
25
26
|
# Local HTTP server for the mobile-friendly Pan web UI.
|
|
26
27
|
class PanServer
|
|
27
|
-
DEFAULT_HOST = "
|
|
28
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
28
29
|
DEFAULT_PORT = 8765
|
|
29
30
|
MAX_REQUEST_BODY_BYTES = 64 * 1024
|
|
30
31
|
ROUTING_PROBE_ADDRESS = "8.8.8.8"
|
|
@@ -44,8 +45,11 @@ module Kward
|
|
|
44
45
|
@host = @config.fetch("host", DEFAULT_HOST).to_s
|
|
45
46
|
@port = positive_port(@config.fetch("port", DEFAULT_PORT))
|
|
46
47
|
@username = @config["username"].to_s
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
environment_password = ENV["KWARD_PAN_PASSWORD"].to_s
|
|
49
|
+
@password = environment_password.empty? ? @config["password"].to_s : environment_password
|
|
50
|
+
if @username.empty? || @password.empty?
|
|
51
|
+
raise "Pan mode requires pan_mode.username and either pan_mode.password or KWARD_PAN_PASSWORD"
|
|
52
|
+
end
|
|
49
53
|
|
|
50
54
|
@session_store = SessionStore.new(config_dir: config_dir, cwd: @workspace.root.to_s)
|
|
51
55
|
@session = @session_store.create(
|
|
@@ -69,6 +73,7 @@ module Kward
|
|
|
69
73
|
attr_reader :host, :port, :session, :workspace
|
|
70
74
|
|
|
71
75
|
def run
|
|
76
|
+
warn_if_exposed
|
|
72
77
|
start_worker
|
|
73
78
|
@server = TCPServer.new(@host, @port)
|
|
74
79
|
actual_port = @server.addr[1]
|
|
@@ -90,6 +95,8 @@ module Kward
|
|
|
90
95
|
@server&.close unless @server&.closed?
|
|
91
96
|
rescue IOError
|
|
92
97
|
nil
|
|
98
|
+
ensure
|
|
99
|
+
@plugin_registry&.shutdown!
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
def enqueue_prompt(prompt)
|
|
@@ -125,7 +132,7 @@ module Kward
|
|
|
125
132
|
hook_manager = lifecycle_hook_manager
|
|
126
133
|
Agent.new(
|
|
127
134
|
client: @client,
|
|
128
|
-
tool_registry: ToolRegistry.new(workspace: @workspace, ask_user_question_enabled: false, hook_manager: hook_manager, hook_context: hook_context),
|
|
135
|
+
tool_registry: ToolRegistry.new(workspace: @workspace, ask_user_question_enabled: false, plugin_tools: plugin_registry.tools, hook_manager: hook_manager, hook_context: hook_context),
|
|
129
136
|
conversation: @conversation,
|
|
130
137
|
hook_manager: hook_manager,
|
|
131
138
|
hook_context: hook_context
|
|
@@ -172,7 +179,7 @@ module Kward
|
|
|
172
179
|
end
|
|
173
180
|
|
|
174
181
|
def plugin_registry
|
|
175
|
-
@plugin_registry ||= PluginRegistry.load(reserved_commands: PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES)
|
|
182
|
+
@plugin_registry ||= PluginRegistry.load(reserved_commands: PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES).tap(&:start!)
|
|
176
183
|
end
|
|
177
184
|
|
|
178
185
|
def start_worker
|
|
@@ -236,7 +243,8 @@ module Kward
|
|
|
236
243
|
when ["GET", "/kward-logo.png"]
|
|
237
244
|
write_response(socket, 200, { "Content-Type" => "image/png", "Cache-Control" => "public, max-age=86400" }, File.binread(File.join(__dir__, "kward_logo.png")))
|
|
238
245
|
when ["GET", "/transcript"]
|
|
239
|
-
write_json(socket, 200, transcript: transcript_items, session: active_session_payload, workspace: @workspace.root.to_s
|
|
246
|
+
write_json(socket, 200, transcript: transcript_items, session: active_session_payload, workspace: @workspace.root.to_s,
|
|
247
|
+
capabilities: { pluginCommandTurns: { supported: false, reason: "Pan has no plugin slash-command dispatcher; use the TUI or RPC turns/start." } })
|
|
240
248
|
when ["GET", "/sessions"]
|
|
241
249
|
write_json(socket, 200, sessions: session_payloads, activeSessionId: @session.id)
|
|
242
250
|
when ["GET", "/skill-capture/sessions"]
|
|
@@ -595,6 +603,21 @@ module Kward
|
|
|
595
603
|
lan_address || "<lan-address>"
|
|
596
604
|
end
|
|
597
605
|
|
|
606
|
+
def warn_if_exposed
|
|
607
|
+
return if loopback_host?
|
|
608
|
+
|
|
609
|
+
@output.puts "Warning: Pan is exposed over plain HTTP on #{@host}."
|
|
610
|
+
@output.puts "Use it only on a trusted network, or bind pan_mode.host to 127.0.0.1."
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def loopback_host?
|
|
614
|
+
return true if @host.casecmp?("localhost")
|
|
615
|
+
|
|
616
|
+
IPAddr.new(@host).loopback?
|
|
617
|
+
rescue IPAddr::InvalidAddressError
|
|
618
|
+
false
|
|
619
|
+
end
|
|
620
|
+
|
|
598
621
|
def lan_address
|
|
599
622
|
socket = @udp_socket_class.new
|
|
600
623
|
socket.connect(ROUTING_PROBE_ADDRESS, ROUTING_PROBE_PORT)
|
|
@@ -160,11 +160,15 @@ module Kward
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
def network?(request)
|
|
163
|
-
NETWORK_TOOLS.include?(request.fetch("tool"))
|
|
163
|
+
NETWORK_TOOLS.include?(request.fetch("tool"))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def extension_tool?(request)
|
|
167
|
+
%w[mcp plugin].include?(request["source"])
|
|
164
168
|
end
|
|
165
169
|
|
|
166
170
|
def risky?(request)
|
|
167
|
-
mutating?(request) || request.fetch("tool") == "run_shell_command" || network?(request)
|
|
171
|
+
mutating?(request) || request.fetch("tool") == "run_shell_command" || network?(request) || extension_tool?(request)
|
|
168
172
|
end
|
|
169
173
|
|
|
170
174
|
def outside_write_scopes?(request)
|