kward 0.78.0 → 0.80.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/.github/workflows/ci.yml +13 -2
- data/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +82 -1
- data/Gemfile.lock +2 -2
- data/README.md +33 -38
- data/doc/agent-tools.md +10 -11
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +136 -27
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +9 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +3 -3
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +130 -0
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +15 -5
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +11 -3
- data/doc/rpc.md +65 -31
- data/doc/sandboxing.md +142 -0
- data/doc/security.md +31 -10
- data/doc/session-management.md +2 -4
- data/doc/skills.md +10 -0
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +4 -4
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/kward.gemspec +1 -1
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/doctor.rb +21 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +7 -0
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +4 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +162 -36
- data/lib/kward/cli/slash_commands.rb +109 -1
- data/lib/kward/cli/tabs.rb +102 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +22 -1
- data/lib/kward/config_files.rb +106 -4
- data/lib/kward/conversation.rb +27 -8
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/local_command_runner.rb +2 -2
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +364 -23
- data/lib/kward/model/model_info.rb +60 -1
- data/lib/kward/model/payloads.rb +64 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +141 -39
- data/lib/kward/pan/index.html.erb +50 -0
- data/lib/kward/pan/server.rb +49 -2
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +81 -8
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +43 -10
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +71 -12
- data/lib/kward/prompts/commands.rb +3 -1
- data/lib/kward/prompts.rb +22 -10
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +56 -194
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +102 -14
- data/lib/kward/rpc/session_manager.rb +151 -38
- data/lib/kward/sandbox/capabilities.rb +39 -0
- data/lib/kward/sandbox/command_runner.rb +28 -0
- data/lib/kward/sandbox/environment.rb +24 -0
- data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
- data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
- data/lib/kward/sandbox/passthrough_runner.rb +13 -0
- data/lib/kward/sandbox/policy.rb +74 -0
- data/lib/kward/sandbox/runner_factory.rb +55 -0
- data/lib/kward/sandbox/unavailable_runner.rb +21 -0
- data/lib/kward/sandbox.rb +9 -0
- data/lib/kward/session_store.rb +110 -24
- data/lib/kward/skills/capture.rb +144 -0
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +26 -6
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +18 -3
- data/lib/kward/workspace_factory.rb +34 -0
- data/templates/default/fulldoc/html/js/kward.js +3 -0
- data/templates/default/kward_navigation.rb +8 -1
- data/templates/default/layout/html/setup.rb +5 -0
- metadata +41 -2
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "stringio"
|
|
2
|
+
require_relative "../config_files"
|
|
3
|
+
require_relative "../rpc/server"
|
|
4
|
+
require_relative "gateway"
|
|
5
|
+
require_relative "manager"
|
|
6
|
+
require_relative "plugin_chat_gateway"
|
|
7
|
+
|
|
8
|
+
module Kward
|
|
9
|
+
module Transport
|
|
10
|
+
# Builds the Kward runtime needed by foreground transport processes.
|
|
11
|
+
class Runtime
|
|
12
|
+
attr_reader :manager
|
|
13
|
+
|
|
14
|
+
def initialize(client:)
|
|
15
|
+
@server = RPC::Server.new(
|
|
16
|
+
input: StringIO.new,
|
|
17
|
+
output: StringIO.new,
|
|
18
|
+
error_output: $stderr,
|
|
19
|
+
client: client
|
|
20
|
+
)
|
|
21
|
+
@manager = Manager.new(
|
|
22
|
+
registry: @server.session_manager.plugin_registry,
|
|
23
|
+
gateway: lambda { |transport_id|
|
|
24
|
+
Gateway.new(session_manager: @server.session_manager, transport_id: transport_id)
|
|
25
|
+
},
|
|
26
|
+
plugin_chat_gateway: lambda { |transport_id|
|
|
27
|
+
PluginChatGateway.new(runtime: @server.plugin_chat_manager.runtime, transport_id: transport_id)
|
|
28
|
+
},
|
|
29
|
+
config_root: ConfigFiles.config_dir,
|
|
30
|
+
config_provider: ->(transport_id) { ConfigFiles.transport_config(transport_id) }
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def shutdown
|
|
35
|
+
@manager.shutdown
|
|
36
|
+
@server.shutdown
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "json"
|
|
3
|
+
require "thread"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../config_files"
|
|
6
|
+
require_relative "../deep_copy"
|
|
7
|
+
require_relative "../private_file"
|
|
8
|
+
|
|
9
|
+
module Kward
|
|
10
|
+
module Transport
|
|
11
|
+
# Small private JSON store scoped to one transport plugin.
|
|
12
|
+
class Store
|
|
13
|
+
KEY_PATTERN = /\A[A-Za-z0-9][A-Za-z0-9:._-]*\z/.freeze
|
|
14
|
+
DEFAULT_MAX_PROCESSED_KEYS = 10_000
|
|
15
|
+
|
|
16
|
+
attr_reader :transport_id
|
|
17
|
+
|
|
18
|
+
def initialize(transport_id, root: ConfigFiles.config_dir)
|
|
19
|
+
@transport_id = validate_key(transport_id, "transport id")
|
|
20
|
+
@path = File.join(File.expand_path(root), "transports", @transport_id, "state.json")
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@values, @processed = load_state
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get(key)
|
|
26
|
+
key = validate_key(key, "storage key")
|
|
27
|
+
@mutex.synchronize { copy(@values[key]) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def put(key, value)
|
|
31
|
+
key = validate_key(key, "storage key")
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@values[key] = copy(value)
|
|
34
|
+
persist
|
|
35
|
+
end
|
|
36
|
+
value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def delete(key)
|
|
40
|
+
key = validate_key(key, "storage key")
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
present = @values.key?(key)
|
|
43
|
+
value = @values.delete(key)
|
|
44
|
+
persist if present
|
|
45
|
+
copy(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Claims an external event key. Returns false when the key was already
|
|
50
|
+
# claimed, making duplicate webhook and polling deliveries harmless.
|
|
51
|
+
def claim(key, max_keys: DEFAULT_MAX_PROCESSED_KEYS)
|
|
52
|
+
key = validate_key(key, "idempotency key")
|
|
53
|
+
max_keys = Integer(max_keys)
|
|
54
|
+
raise ArgumentError, "max_keys must be positive" unless max_keys.positive?
|
|
55
|
+
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
return false if @processed.key?(key)
|
|
58
|
+
|
|
59
|
+
@processed[key] = Time.now.utc.iso8601
|
|
60
|
+
@processed.shift while @processed.length > max_keys
|
|
61
|
+
persist
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def load_state
|
|
69
|
+
return [{}, {}] unless File.file?(@path)
|
|
70
|
+
|
|
71
|
+
state = JSON.parse(File.read(@path))
|
|
72
|
+
values = state.fetch("values", {})
|
|
73
|
+
processed = state.fetch("processed", {})
|
|
74
|
+
unless values.is_a?(Hash) && processed.is_a?(Hash)
|
|
75
|
+
raise JSON::ParserError, "transport state must contain JSON objects"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
[values, processed]
|
|
79
|
+
rescue JSON::ParserError => e
|
|
80
|
+
raise "Invalid transport state #{@path}: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def persist
|
|
84
|
+
PrivateFile.write_json(@path, "values" => @values, "processed" => @processed)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_key(value, name)
|
|
88
|
+
value = value.to_s
|
|
89
|
+
raise ArgumentError, "#{name} is required" unless value.match?(KEY_PATTERN)
|
|
90
|
+
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def copy(value)
|
|
95
|
+
return nil if value.nil?
|
|
96
|
+
|
|
97
|
+
DeepCopy.dup(value)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
require_relative "deep_copy"
|
|
3
|
+
|
|
4
|
+
# Namespace for frontend-neutral external transport contracts.
|
|
5
|
+
module Kward
|
|
6
|
+
module Transport
|
|
7
|
+
STREAMING_MODES = %i[native edit aggregate none].freeze
|
|
8
|
+
|
|
9
|
+
Actor = Data.define(:id, :display_name, :metadata)
|
|
10
|
+
Attachment = Data.define(:name, :mime_type, :data, :url, :metadata)
|
|
11
|
+
ConversationKey = Data.define(:transport_id, :external_id)
|
|
12
|
+
InboundMessage = Data.define(
|
|
13
|
+
:conversation,
|
|
14
|
+
:message_id,
|
|
15
|
+
:actor,
|
|
16
|
+
:text,
|
|
17
|
+
:attachments,
|
|
18
|
+
:reply_context,
|
|
19
|
+
:idempotency_key,
|
|
20
|
+
:metadata
|
|
21
|
+
)
|
|
22
|
+
TurnEvent = Data.define(:type, :session_id, :turn_id, :sequence, :payload)
|
|
23
|
+
InteractionRequest = Data.define(
|
|
24
|
+
:id,
|
|
25
|
+
:session_id,
|
|
26
|
+
:turn_id,
|
|
27
|
+
:kind,
|
|
28
|
+
:prompt,
|
|
29
|
+
:choices,
|
|
30
|
+
:expires_at,
|
|
31
|
+
:metadata
|
|
32
|
+
)
|
|
33
|
+
Capabilities = Data.define(:inbound, :outbound, :streaming, :limits)
|
|
34
|
+
ExecutionProfile = Data.define(
|
|
35
|
+
:id,
|
|
36
|
+
:tool_mode,
|
|
37
|
+
:allowed_tools,
|
|
38
|
+
:disabled_tools,
|
|
39
|
+
:plugin_commands,
|
|
40
|
+
:approval_mode,
|
|
41
|
+
:memory,
|
|
42
|
+
:attachments,
|
|
43
|
+
:workspace_mode,
|
|
44
|
+
:prompt_context
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
TOOL_MODES = %i[all none allowlist].freeze
|
|
48
|
+
APPROVAL_MODES = %i[default ask deny].freeze
|
|
49
|
+
MEMORY_MODES = %i[default none].freeze
|
|
50
|
+
WORKSPACE_MODES = %i[session fixed].freeze
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
def actor(id:, display_name: nil, metadata: {})
|
|
55
|
+
Actor.new(
|
|
56
|
+
id: required_string(id, "actor id"),
|
|
57
|
+
display_name: optional_string(display_name),
|
|
58
|
+
metadata: frozen_copy(metadata)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def attachment(name: nil, mime_type:, data: nil, url: nil, metadata: {})
|
|
63
|
+
raise ArgumentError, "attachment requires data or url" if data.nil? && url.nil?
|
|
64
|
+
raise ArgumentError, "attachment cannot contain both data and url" unless data.nil? || url.nil?
|
|
65
|
+
|
|
66
|
+
Attachment.new(
|
|
67
|
+
name: optional_string(name),
|
|
68
|
+
mime_type: required_string(mime_type, "attachment MIME type"),
|
|
69
|
+
data: data&.dup&.freeze,
|
|
70
|
+
url: url && required_string(url, "attachment URL"),
|
|
71
|
+
metadata: frozen_copy(metadata)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def conversation_key(transport_id:, external_id:)
|
|
76
|
+
ConversationKey.new(
|
|
77
|
+
transport_id: required_string(transport_id, "transport id"),
|
|
78
|
+
external_id: required_string(external_id, "external conversation id")
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def inbound_message(conversation:, message_id:, actor:, text: "", attachments: [], reply_context: {}, idempotency_key:, metadata: {})
|
|
83
|
+
unless conversation.is_a?(ConversationKey)
|
|
84
|
+
raise ArgumentError, "conversation must be a Transport::ConversationKey"
|
|
85
|
+
end
|
|
86
|
+
unless actor.is_a?(Actor)
|
|
87
|
+
raise ArgumentError, "actor must be a Transport::Actor"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
InboundMessage.new(
|
|
91
|
+
conversation: conversation,
|
|
92
|
+
message_id: required_string(message_id, "message id"),
|
|
93
|
+
actor: actor,
|
|
94
|
+
text: text.to_s.freeze,
|
|
95
|
+
attachments: frozen_copy(Array(attachments)),
|
|
96
|
+
reply_context: frozen_copy(reply_context),
|
|
97
|
+
idempotency_key: required_string(idempotency_key, "idempotency key"),
|
|
98
|
+
metadata: frozen_copy(metadata)
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def turn_event(type:, session_id:, turn_id:, sequence:, payload: {})
|
|
103
|
+
TurnEvent.new(
|
|
104
|
+
type: required_string(type, "turn event type"),
|
|
105
|
+
session_id: required_string(session_id, "session id"),
|
|
106
|
+
turn_id: required_string(turn_id, "turn id"),
|
|
107
|
+
sequence: Integer(sequence),
|
|
108
|
+
payload: frozen_copy(payload)
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def interaction_request(id:, session_id:, turn_id:, kind:, prompt:, choices: [], expires_at: nil, metadata: {})
|
|
113
|
+
InteractionRequest.new(
|
|
114
|
+
id: required_string(id, "interaction id"),
|
|
115
|
+
session_id: required_string(session_id, "session id"),
|
|
116
|
+
turn_id: required_string(turn_id, "turn id"),
|
|
117
|
+
kind: required_string(kind, "interaction kind"),
|
|
118
|
+
prompt: required_string(prompt, "interaction prompt"),
|
|
119
|
+
choices: frozen_copy(Array(choices)),
|
|
120
|
+
expires_at: expires_at,
|
|
121
|
+
metadata: frozen_copy(metadata)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def execution_profile(
|
|
126
|
+
id:,
|
|
127
|
+
tool_mode: :all,
|
|
128
|
+
allowed_tools: nil,
|
|
129
|
+
disabled_tools: nil,
|
|
130
|
+
plugin_commands: true,
|
|
131
|
+
approval_mode: :default,
|
|
132
|
+
memory: :default,
|
|
133
|
+
attachments: true,
|
|
134
|
+
workspace_mode: :session,
|
|
135
|
+
prompt_context: nil
|
|
136
|
+
)
|
|
137
|
+
id = required_string(id, "execution profile id")
|
|
138
|
+
tool_mode = tool_mode.to_sym
|
|
139
|
+
approval_mode = approval_mode.to_sym
|
|
140
|
+
memory = memory.to_sym
|
|
141
|
+
workspace_mode = workspace_mode.to_sym
|
|
142
|
+
raise ArgumentError, "unsupported tool mode: #{tool_mode}" unless TOOL_MODES.include?(tool_mode)
|
|
143
|
+
raise ArgumentError, "unsupported approval mode: #{approval_mode}" unless APPROVAL_MODES.include?(approval_mode)
|
|
144
|
+
raise ArgumentError, "unsupported memory mode: #{memory}" unless MEMORY_MODES.include?(memory)
|
|
145
|
+
raise ArgumentError, "unsupported workspace mode: #{workspace_mode}" unless WORKSPACE_MODES.include?(workspace_mode)
|
|
146
|
+
raise ArgumentError, "allowlist tool mode requires allowed_tools" if tool_mode == :allowlist && Array(allowed_tools).empty?
|
|
147
|
+
raise ArgumentError, "none tool mode cannot define allowed_tools" if tool_mode == :none && allowed_tools
|
|
148
|
+
raise ArgumentError, "allowed_tools and disabled_tools cannot both be set" if allowed_tools && disabled_tools
|
|
149
|
+
|
|
150
|
+
ExecutionProfile.new(
|
|
151
|
+
id: id,
|
|
152
|
+
tool_mode: tool_mode,
|
|
153
|
+
allowed_tools: frozen_symbols(allowed_tools),
|
|
154
|
+
disabled_tools: frozen_symbols(disabled_tools),
|
|
155
|
+
plugin_commands: !!plugin_commands,
|
|
156
|
+
approval_mode: approval_mode,
|
|
157
|
+
memory: memory,
|
|
158
|
+
attachments: !!attachments,
|
|
159
|
+
workspace_mode: workspace_mode,
|
|
160
|
+
prompt_context: prompt_context.nil? ? nil : prompt_context.to_s.freeze
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def capabilities(inbound: [], outbound: [], streaming: :none, limits: {})
|
|
165
|
+
streaming = streaming.to_sym
|
|
166
|
+
raise ArgumentError, "unsupported streaming mode: #{streaming}" unless STREAMING_MODES.include?(streaming)
|
|
167
|
+
|
|
168
|
+
Capabilities.new(
|
|
169
|
+
inbound: frozen_symbols(inbound),
|
|
170
|
+
outbound: frozen_symbols(outbound),
|
|
171
|
+
streaming: streaming,
|
|
172
|
+
limits: frozen_copy(limits)
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def required_string(value, name)
|
|
177
|
+
value = value.to_s
|
|
178
|
+
raise ArgumentError, "#{name} is required" if value.empty?
|
|
179
|
+
|
|
180
|
+
value.freeze
|
|
181
|
+
end
|
|
182
|
+
private_class_method :required_string
|
|
183
|
+
|
|
184
|
+
def optional_string(value)
|
|
185
|
+
value.nil? ? nil : value.to_s.freeze
|
|
186
|
+
end
|
|
187
|
+
private_class_method :optional_string
|
|
188
|
+
|
|
189
|
+
def frozen_symbols(values)
|
|
190
|
+
Array(values).map { |value| value.to_sym }.uniq.freeze
|
|
191
|
+
end
|
|
192
|
+
private_class_method :frozen_symbols
|
|
193
|
+
|
|
194
|
+
def frozen_copy(value)
|
|
195
|
+
DeepCopy.freeze(DeepCopy.dup(value))
|
|
196
|
+
end
|
|
197
|
+
private_class_method :frozen_copy
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
require_relative "transport/store"
|
|
202
|
+
require_relative "transport/host"
|
|
203
|
+
require_relative "transport/manager"
|
|
204
|
+
require_relative "transport/gateway"
|
data/lib/kward/version.rb
CHANGED
data/lib/kward/workspace.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "pathname"
|
|
2
2
|
require_relative "local_command_runner"
|
|
3
|
+
require_relative "sandbox"
|
|
3
4
|
require_relative "session_diff"
|
|
4
5
|
|
|
5
6
|
require_relative "path_guard"
|
|
@@ -27,13 +28,14 @@ module Kward
|
|
|
27
28
|
EXPECTED_FILE_ERRORS = [SecurityError, Errno::ENOENT, Errno::EACCES, Errno::EPERM, Errno::EISDIR, Errno::ENOTDIR].freeze
|
|
28
29
|
|
|
29
30
|
# Creates an object for workspace filesystem and shell operations.
|
|
30
|
-
def initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES, guardrails: true)
|
|
31
|
+
def initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES, guardrails: true, command_runner: nil)
|
|
31
32
|
@root = Pathname.new(root).realpath
|
|
32
33
|
@guardrails = guardrails
|
|
33
34
|
@max_file_bytes = max_file_bytes
|
|
34
35
|
@max_read_output_bytes = max_read_output_bytes
|
|
35
36
|
@max_read_output_lines = max_read_output_lines
|
|
36
37
|
@max_command_output_bytes = max_command_output_bytes
|
|
38
|
+
@command_runner = command_runner
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
# @return [Pathname] canonical workspace root used as the base for file and shell tools
|
|
@@ -162,7 +164,13 @@ module Kward
|
|
|
162
164
|
timeout_seconds = DEFAULT_COMMAND_TIMEOUT_SECONDS if timeout_seconds <= 0
|
|
163
165
|
cancellation&.raise_if_cancelled!
|
|
164
166
|
|
|
165
|
-
result =
|
|
167
|
+
result = command_runner.run(
|
|
168
|
+
command,
|
|
169
|
+
cwd: @root.to_s,
|
|
170
|
+
timeout_seconds: timeout_seconds,
|
|
171
|
+
max_output_bytes: @max_command_output_bytes,
|
|
172
|
+
cancellation: cancellation
|
|
173
|
+
)
|
|
166
174
|
return "Error: command timed out after #{timeout_seconds} seconds" if result.timed_out
|
|
167
175
|
|
|
168
176
|
output = +"Exit status: #{result.exit_status}\n"
|
|
@@ -170,7 +178,7 @@ module Kward
|
|
|
170
178
|
output << "\nSTDERR:\n#{result.stderr}" unless result.stderr.empty?
|
|
171
179
|
output << "\n... truncated to #{@max_command_output_bytes} bytes" if result.truncated
|
|
172
180
|
truncate_output(output)
|
|
173
|
-
rescue Errno::ENOENT, ArgumentError => e
|
|
181
|
+
rescue Errno::ENOENT, ArgumentError, Sandbox::UnavailableError => e
|
|
174
182
|
"Error: #{e.message}"
|
|
175
183
|
end
|
|
176
184
|
|
|
@@ -181,6 +189,13 @@ module Kward
|
|
|
181
189
|
|
|
182
190
|
private
|
|
183
191
|
|
|
192
|
+
def command_runner
|
|
193
|
+
@command_runner ||= Sandbox::PassthroughRunner.new(
|
|
194
|
+
policy: Sandbox::Policy.new(workspace_root: @root),
|
|
195
|
+
capabilities: Sandbox::RunnerFactory.off_capabilities
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
184
199
|
def workspace_path(path)
|
|
185
200
|
target = Pathname.new(path.to_s)
|
|
186
201
|
target = @root.join(target) unless target.absolute?
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative "config_files"
|
|
2
|
+
require_relative "sandbox"
|
|
3
|
+
require_relative "workspace"
|
|
4
|
+
|
|
5
|
+
# Namespace for the Kward CLI agent runtime.
|
|
6
|
+
module Kward
|
|
7
|
+
# Builds workspaces with the same user-configured command sandbox in every frontend.
|
|
8
|
+
module WorkspaceFactory
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def build(root:, guardrails: true, config: ConfigFiles.read_config, strict: false)
|
|
12
|
+
policy = ConfigFiles.sandbox_policy(root, config)
|
|
13
|
+
policy = strict_policy(root, policy) if strict
|
|
14
|
+
runner = Sandbox::RunnerFactory.build(policy)
|
|
15
|
+
if strict && !runner.capabilities.filesystem_enforced?
|
|
16
|
+
reason = runner.capabilities.reason || "filesystem enforcement is unavailable"
|
|
17
|
+
raise Sandbox::UnavailableError, "Strict workspace sandbox is unavailable: #{reason}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Workspace.new(root:, guardrails: strict ? true : guardrails, command_runner: runner)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def strict_policy(root, configured_policy)
|
|
24
|
+
Sandbox::Policy.new(
|
|
25
|
+
mode: "workspace_write",
|
|
26
|
+
network: configured_policy.network,
|
|
27
|
+
workspace_root: root,
|
|
28
|
+
writable_roots: [],
|
|
29
|
+
protect_git_metadata: configured_policy.protect_git_metadata?
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
private_class_method :strict_policy
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -4,6 +4,8 @@ const guideLinks = {
|
|
|
4
4
|
'doc/usage.md': 'file.usage.html',
|
|
5
5
|
'doc/configuration.md': 'file.configuration.html',
|
|
6
6
|
'doc/authentication.md': 'file.authentication.html',
|
|
7
|
+
'doc/providers.md': 'file.providers.html',
|
|
8
|
+
'doc/sandboxing.md': 'file.sandboxing.html',
|
|
7
9
|
'doc/troubleshooting.md': 'file.troubleshooting.html',
|
|
8
10
|
'doc/session-management.md': 'file.session-management.html',
|
|
9
11
|
'doc/memory.md': 'file.memory.html',
|
|
@@ -18,6 +20,7 @@ const guideLinks = {
|
|
|
18
20
|
'doc/code-search.md': 'file.code-search.html',
|
|
19
21
|
'doc/context-tools.md': 'file.context-tools.html',
|
|
20
22
|
'doc/rpc.md': 'file.rpc.html',
|
|
23
|
+
'doc/transports.md': 'file.transports.html',
|
|
21
24
|
'doc/releasing.md': 'file.releasing.html'
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -5,10 +5,12 @@ module KwardDocsNavigationData
|
|
|
5
5
|
[
|
|
6
6
|
"Start here",
|
|
7
7
|
[
|
|
8
|
+
["Overview", "file.README.html"],
|
|
8
9
|
["Getting started", "file.getting-started.html"],
|
|
9
10
|
["Usage", "file.usage.html"],
|
|
10
11
|
["Configuration", "file.configuration.html"],
|
|
11
12
|
["Authentication", "file.authentication.html"],
|
|
13
|
+
["Model providers", "file.providers.html"],
|
|
12
14
|
["Security and trust", "file.security.html"],
|
|
13
15
|
["Troubleshooting", "file.troubleshooting.html"]
|
|
14
16
|
]
|
|
@@ -22,6 +24,7 @@ module KwardDocsNavigationData
|
|
|
22
24
|
["Memory", "file.memory.html"],
|
|
23
25
|
["Personas", "file.personas.html"],
|
|
24
26
|
["Skills", "file.skills.html"],
|
|
27
|
+
["Prompt templates", "file.prompt-templates.html"],
|
|
25
28
|
["Pan mode", "file.pan.html"]
|
|
26
29
|
]
|
|
27
30
|
],
|
|
@@ -43,14 +46,18 @@ module KwardDocsNavigationData
|
|
|
43
46
|
["Extensibility", "file.extensibility.html"],
|
|
44
47
|
["Plugins", "file.plugins.html"],
|
|
45
48
|
["Lifecycle hooks", "file.lifecycle-hooks.html"],
|
|
46
|
-
["Permissions", "file.permissions.html"]
|
|
49
|
+
["Permissions", "file.permissions.html"],
|
|
50
|
+
["Command sandboxing", "file.sandboxing.html"]
|
|
47
51
|
]
|
|
48
52
|
],
|
|
49
53
|
[
|
|
50
54
|
"Integrate",
|
|
51
55
|
[
|
|
52
56
|
["MCP servers", "file.mcp.html"],
|
|
57
|
+
["Local models", "file.local-models.html"],
|
|
53
58
|
["RPC protocol", "file.rpc.html"],
|
|
59
|
+
["Transports", "file.transports.html"],
|
|
60
|
+
["Telegram transport", "file.telegram.html"],
|
|
54
61
|
["Releasing", "file.releasing.html"]
|
|
55
62
|
]
|
|
56
63
|
],
|
|
@@ -29,7 +29,9 @@ module KwardDocsNavigation
|
|
|
29
29
|
"doc/usage.md" => "file.usage.html",
|
|
30
30
|
"doc/configuration.md" => "file.configuration.html",
|
|
31
31
|
"doc/authentication.md" => "file.authentication.html",
|
|
32
|
+
"doc/providers.md" => "file.providers.html",
|
|
32
33
|
"doc/permissions.md" => "file.permissions.html",
|
|
34
|
+
"doc/sandboxing.md" => "file.sandboxing.html",
|
|
33
35
|
"doc/troubleshooting.md" => "file.troubleshooting.html",
|
|
34
36
|
"doc/session-management.md" => "file.session-management.html",
|
|
35
37
|
"doc/tabs.md" => "file.tabs.html",
|
|
@@ -40,6 +42,7 @@ module KwardDocsNavigation
|
|
|
40
42
|
"doc/memory.md" => "file.memory.html",
|
|
41
43
|
"doc/personas.md" => "file.personas.html",
|
|
42
44
|
"doc/skills.md" => "file.skills.html",
|
|
45
|
+
"doc/local-models.md" => "file.local-models.html",
|
|
43
46
|
"doc/extensibility.md" => "file.extensibility.html",
|
|
44
47
|
"doc/plugins.md" => "file.plugins.html",
|
|
45
48
|
"doc/lifecycle-hooks.md" => "file.lifecycle-hooks.html",
|
|
@@ -50,6 +53,8 @@ module KwardDocsNavigation
|
|
|
50
53
|
"doc/code-search.md" => "file.code-search.html",
|
|
51
54
|
"doc/context-tools.md" => "file.context-tools.html",
|
|
52
55
|
"doc/rpc.md" => "file.rpc.html",
|
|
56
|
+
"doc/transports.md" => "file.transports.html",
|
|
57
|
+
"doc/telegram.md" => "file.telegram.html",
|
|
53
58
|
"doc/releasing.md" => "file.releasing.html"
|
|
54
59
|
}.freeze
|
|
55
60
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kward
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.80.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kai Wood
|
|
@@ -139,30 +139,41 @@ files:
|
|
|
139
139
|
- doc/getting-started.md
|
|
140
140
|
- doc/git.md
|
|
141
141
|
- doc/lifecycle-hooks.md
|
|
142
|
+
- doc/local-models.md
|
|
142
143
|
- doc/mcp.md
|
|
143
144
|
- doc/memory.md
|
|
144
145
|
- doc/pan.md
|
|
145
146
|
- doc/permissions.md
|
|
146
147
|
- doc/personas.md
|
|
147
148
|
- doc/plugins.md
|
|
149
|
+
- doc/prompt-templates.md
|
|
150
|
+
- doc/providers.md
|
|
148
151
|
- doc/releasing.md
|
|
149
152
|
- doc/rpc.md
|
|
153
|
+
- doc/sandboxing.md
|
|
150
154
|
- doc/security.md
|
|
151
155
|
- doc/session-management.md
|
|
152
156
|
- doc/shell.md
|
|
153
157
|
- doc/skills.md
|
|
154
158
|
- doc/tabs.md
|
|
159
|
+
- doc/telegram.md
|
|
160
|
+
- doc/transports.md
|
|
155
161
|
- doc/troubleshooting.md
|
|
156
162
|
- doc/usage.md
|
|
157
163
|
- doc/web-search.md
|
|
158
164
|
- doc/workspace-tools.md
|
|
159
165
|
- examples/plugins/space_invaders.rb
|
|
166
|
+
- examples/plugins/stardate_footer.rb
|
|
167
|
+
- examples/plugins/telegram/plugin.rb
|
|
168
|
+
- examples/plugins/telegram/telegram_api.rb
|
|
169
|
+
- examples/plugins/telegram/telegram_transport.rb
|
|
160
170
|
- exe/kward
|
|
161
171
|
- kward.gemspec
|
|
162
172
|
- lib/kward.rb
|
|
163
173
|
- lib/kward/agent.rb
|
|
164
174
|
- lib/kward/ansi.rb
|
|
165
175
|
- lib/kward/auth/anthropic_oauth.rb
|
|
176
|
+
- lib/kward/auth/api_key_store.rb
|
|
166
177
|
- lib/kward/auth/file.rb
|
|
167
178
|
- lib/kward/auth/github_oauth.rb
|
|
168
179
|
- lib/kward/auth/oauth_helpers.rb
|
|
@@ -190,6 +201,8 @@ files:
|
|
|
190
201
|
- lib/kward/cli/sysprompt.rb
|
|
191
202
|
- lib/kward/cli/tabs.rb
|
|
192
203
|
- lib/kward/cli/tool_summaries.rb
|
|
204
|
+
- lib/kward/cli/transports.rb
|
|
205
|
+
- lib/kward/cli/worktrees.rb
|
|
193
206
|
- lib/kward/cli_transcript_formatter.rb
|
|
194
207
|
- lib/kward/clipboard.rb
|
|
195
208
|
- lib/kward/compaction/file_operation_tracker.rb
|
|
@@ -205,6 +218,7 @@ files:
|
|
|
205
218
|
- lib/kward/events.rb
|
|
206
219
|
- lib/kward/export_path.rb
|
|
207
220
|
- lib/kward/frontmatter.rb
|
|
221
|
+
- lib/kward/git_worktree_manager.rb
|
|
208
222
|
- lib/kward/hooks.rb
|
|
209
223
|
- lib/kward/hooks/audit_log.rb
|
|
210
224
|
- lib/kward/hooks/catalog.rb
|
|
@@ -228,6 +242,8 @@ files:
|
|
|
228
242
|
- lib/kward/memory/turn_context.rb
|
|
229
243
|
- lib/kward/message_access.rb
|
|
230
244
|
- lib/kward/message_text.rb
|
|
245
|
+
- lib/kward/model/azure_openai_config.rb
|
|
246
|
+
- lib/kward/model/catalog.rb
|
|
231
247
|
- lib/kward/model/chat_invocation.rb
|
|
232
248
|
- lib/kward/model/client.rb
|
|
233
249
|
- lib/kward/model/context_overflow.rb
|
|
@@ -235,7 +251,9 @@ files:
|
|
|
235
251
|
- lib/kward/model/copilot_models.rb
|
|
236
252
|
- lib/kward/model/model_info.rb
|
|
237
253
|
- lib/kward/model/payloads.rb
|
|
254
|
+
- lib/kward/model/provider_catalog.rb
|
|
238
255
|
- lib/kward/model/retry_message.rb
|
|
256
|
+
- lib/kward/model/sources.rb
|
|
239
257
|
- lib/kward/model/stream_parser.rb
|
|
240
258
|
- lib/kward/openrouter_model_cache.rb
|
|
241
259
|
- lib/kward/pan/index.html.erb
|
|
@@ -243,6 +261,7 @@ files:
|
|
|
243
261
|
- lib/kward/pan/server.rb
|
|
244
262
|
- lib/kward/path_guard.rb
|
|
245
263
|
- lib/kward/permissions/policy.rb
|
|
264
|
+
- lib/kward/plugin_chat_runtime.rb
|
|
246
265
|
- lib/kward/plugin_registry.rb
|
|
247
266
|
- lib/kward/private_file.rb
|
|
248
267
|
- lib/kward/project_files.rb
|
|
@@ -314,6 +333,16 @@ files:
|
|
|
314
333
|
- lib/kward/rpc/transcript_normalizer.rb
|
|
315
334
|
- lib/kward/rpc/transport.rb
|
|
316
335
|
- lib/kward/rpc/turn_context.rb
|
|
336
|
+
- lib/kward/sandbox.rb
|
|
337
|
+
- lib/kward/sandbox/capabilities.rb
|
|
338
|
+
- lib/kward/sandbox/command_runner.rb
|
|
339
|
+
- lib/kward/sandbox/environment.rb
|
|
340
|
+
- lib/kward/sandbox/linux_bubblewrap_runner.rb
|
|
341
|
+
- lib/kward/sandbox/macos_seatbelt_runner.rb
|
|
342
|
+
- lib/kward/sandbox/passthrough_runner.rb
|
|
343
|
+
- lib/kward/sandbox/policy.rb
|
|
344
|
+
- lib/kward/sandbox/runner_factory.rb
|
|
345
|
+
- lib/kward/sandbox/unavailable_runner.rb
|
|
317
346
|
- lib/kward/scratchpad_runner.rb
|
|
318
347
|
- lib/kward/session_diff.rb
|
|
319
348
|
- lib/kward/session_naming.rb
|
|
@@ -322,6 +351,7 @@ files:
|
|
|
322
351
|
- lib/kward/session_tree_nodes.rb
|
|
323
352
|
- lib/kward/session_tree_renderer.rb
|
|
324
353
|
- lib/kward/session_tree_tool_display.rb
|
|
354
|
+
- lib/kward/skills/capture.rb
|
|
325
355
|
- lib/kward/skills/registry.rb
|
|
326
356
|
- lib/kward/starter_pack_installer.rb
|
|
327
357
|
- lib/kward/steering.rb
|
|
@@ -341,6 +371,7 @@ files:
|
|
|
341
371
|
- lib/kward/tools/edit_file.rb
|
|
342
372
|
- lib/kward/tools/fetch_content.rb
|
|
343
373
|
- lib/kward/tools/fetch_raw.rb
|
|
374
|
+
- lib/kward/tools/git_commit.rb
|
|
344
375
|
- lib/kward/tools/list_directory.rb
|
|
345
376
|
- lib/kward/tools/mcp_tool.rb
|
|
346
377
|
- lib/kward/tools/read_file.rb
|
|
@@ -356,9 +387,17 @@ files:
|
|
|
356
387
|
- lib/kward/tools/web_search.rb
|
|
357
388
|
- lib/kward/tools/write_file.rb
|
|
358
389
|
- lib/kward/transcript_export.rb
|
|
390
|
+
- lib/kward/transport.rb
|
|
391
|
+
- lib/kward/transport/gateway.rb
|
|
392
|
+
- lib/kward/transport/host.rb
|
|
393
|
+
- lib/kward/transport/manager.rb
|
|
394
|
+
- lib/kward/transport/plugin_chat_gateway.rb
|
|
395
|
+
- lib/kward/transport/runtime.rb
|
|
396
|
+
- lib/kward/transport/store.rb
|
|
359
397
|
- lib/kward/update_check.rb
|
|
360
398
|
- lib/kward/version.rb
|
|
361
399
|
- lib/kward/workspace.rb
|
|
400
|
+
- lib/kward/workspace_factory.rb
|
|
362
401
|
- lib/main.rb
|
|
363
402
|
- templates/default/fulldoc/html/css/kward.css
|
|
364
403
|
- templates/default/fulldoc/html/full_list.erb
|
|
@@ -386,7 +425,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
386
425
|
requirements:
|
|
387
426
|
- - ">="
|
|
388
427
|
- !ruby/object:Gem::Version
|
|
389
|
-
version: '3.
|
|
428
|
+
version: '3.4'
|
|
390
429
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
391
430
|
requirements:
|
|
392
431
|
- - ">="
|