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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -2
  3. data/.github/workflows/pages.yml +1 -1
  4. data/CHANGELOG.md +82 -1
  5. data/Gemfile.lock +2 -2
  6. data/README.md +33 -38
  7. data/doc/agent-tools.md +10 -11
  8. data/doc/api.md +3 -3
  9. data/doc/authentication.md +79 -110
  10. data/doc/code-search.md +1 -1
  11. data/doc/composer.md +1 -1
  12. data/doc/configuration.md +136 -27
  13. data/doc/context-budgeting.md +6 -6
  14. data/doc/context-tools.md +2 -2
  15. data/doc/editor.md +7 -7
  16. data/doc/extensibility.md +9 -29
  17. data/doc/files.md +6 -6
  18. data/doc/getting-started.md +3 -3
  19. data/doc/git.md +8 -2
  20. data/doc/lifecycle-hooks.md +5 -4
  21. data/doc/local-models.md +130 -0
  22. data/doc/mcp.md +5 -5
  23. data/doc/permissions.md +15 -5
  24. data/doc/plugins.md +39 -4
  25. data/doc/prompt-templates.md +105 -0
  26. data/doc/providers.md +140 -0
  27. data/doc/releasing.md +11 -3
  28. data/doc/rpc.md +65 -31
  29. data/doc/sandboxing.md +142 -0
  30. data/doc/security.md +31 -10
  31. data/doc/session-management.md +2 -4
  32. data/doc/skills.md +10 -0
  33. data/doc/tabs.md +37 -0
  34. data/doc/telegram.md +138 -0
  35. data/doc/transports.md +209 -0
  36. data/doc/usage.md +11 -3
  37. data/doc/web-search.md +3 -3
  38. data/doc/workspace-tools.md +4 -4
  39. data/examples/plugins/stardate_footer.rb +10 -0
  40. data/examples/plugins/telegram/plugin.rb +39 -0
  41. data/examples/plugins/telegram/telegram_api.rb +136 -0
  42. data/examples/plugins/telegram/telegram_transport.rb +304 -0
  43. data/kward.gemspec +1 -1
  44. data/lib/kward/ansi.rb +1 -0
  45. data/lib/kward/auth/api_key_store.rb +99 -0
  46. data/lib/kward/cli/auth_commands.rb +72 -13
  47. data/lib/kward/cli/commands.rb +7 -0
  48. data/lib/kward/cli/doctor.rb +21 -0
  49. data/lib/kward/cli/git.rb +31 -8
  50. data/lib/kward/cli/plugins.rb +1 -0
  51. data/lib/kward/cli/prompt_interface.rb +7 -0
  52. data/lib/kward/cli/rendering.rb +6 -2
  53. data/lib/kward/cli/runtime_helpers.rb +4 -2
  54. data/lib/kward/cli/sessions.rb +1 -1
  55. data/lib/kward/cli/settings.rb +162 -36
  56. data/lib/kward/cli/slash_commands.rb +109 -1
  57. data/lib/kward/cli/tabs.rb +102 -37
  58. data/lib/kward/cli/transports.rb +67 -0
  59. data/lib/kward/cli/worktrees.rb +360 -0
  60. data/lib/kward/cli.rb +22 -1
  61. data/lib/kward/config_files.rb +106 -4
  62. data/lib/kward/conversation.rb +27 -8
  63. data/lib/kward/git_worktree_manager.rb +291 -0
  64. data/lib/kward/local_command_runner.rb +2 -2
  65. data/lib/kward/model/azure_openai_config.rb +54 -0
  66. data/lib/kward/model/catalog.rb +113 -0
  67. data/lib/kward/model/client.rb +364 -23
  68. data/lib/kward/model/model_info.rb +60 -1
  69. data/lib/kward/model/payloads.rb +64 -4
  70. data/lib/kward/model/provider_catalog.rb +114 -0
  71. data/lib/kward/model/sources.rb +70 -0
  72. data/lib/kward/model/stream_parser.rb +141 -39
  73. data/lib/kward/pan/index.html.erb +50 -0
  74. data/lib/kward/pan/server.rb +49 -2
  75. data/lib/kward/permissions/policy.rb +18 -4
  76. data/lib/kward/plugin_chat_runtime.rb +374 -0
  77. data/lib/kward/plugin_registry.rb +81 -8
  78. data/lib/kward/private_file.rb +9 -3
  79. data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
  80. data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
  81. data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
  82. data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
  83. data/lib/kward/prompt_interface/editor/controller.rb +43 -10
  84. data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
  85. data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
  86. data/lib/kward/prompt_interface/editor/state.rb +3 -9
  87. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
  88. data/lib/kward/prompt_interface/file_overlay.rb +2 -2
  89. data/lib/kward/prompt_interface/git_prompt.rb +7 -3
  90. data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
  91. data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
  92. data/lib/kward/prompt_interface/interactive/state.rb +2 -2
  93. data/lib/kward/prompt_interface/project_browser.rb +25 -6
  94. data/lib/kward/prompt_interface/runtime_state.rb +22 -6
  95. data/lib/kward/prompt_interface/screen.rb +3 -0
  96. data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
  97. data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
  98. data/lib/kward/prompt_interface.rb +71 -12
  99. data/lib/kward/prompts/commands.rb +3 -1
  100. data/lib/kward/prompts.rb +22 -10
  101. data/lib/kward/rpc/auth_manager.rb +112 -152
  102. data/lib/kward/rpc/config_manager.rb +46 -6
  103. data/lib/kward/rpc/plugin_chat_manager.rb +56 -194
  104. data/lib/kward/rpc/prompt_bridge.rb +8 -4
  105. data/lib/kward/rpc/redactor.rb +1 -1
  106. data/lib/kward/rpc/server.rb +102 -14
  107. data/lib/kward/rpc/session_manager.rb +151 -38
  108. data/lib/kward/sandbox/capabilities.rb +39 -0
  109. data/lib/kward/sandbox/command_runner.rb +28 -0
  110. data/lib/kward/sandbox/environment.rb +24 -0
  111. data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
  112. data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
  113. data/lib/kward/sandbox/passthrough_runner.rb +13 -0
  114. data/lib/kward/sandbox/policy.rb +74 -0
  115. data/lib/kward/sandbox/runner_factory.rb +55 -0
  116. data/lib/kward/sandbox/unavailable_runner.rb +21 -0
  117. data/lib/kward/sandbox.rb +9 -0
  118. data/lib/kward/session_store.rb +110 -24
  119. data/lib/kward/skills/capture.rb +144 -0
  120. data/lib/kward/tab_driver.rb +6 -3
  121. data/lib/kward/tools/git_commit.rb +39 -0
  122. data/lib/kward/tools/registry.rb +26 -6
  123. data/lib/kward/tools/tool_call.rb +1 -0
  124. data/lib/kward/transport/gateway.rb +253 -0
  125. data/lib/kward/transport/host.rb +343 -0
  126. data/lib/kward/transport/manager.rb +179 -0
  127. data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
  128. data/lib/kward/transport/runtime.rb +41 -0
  129. data/lib/kward/transport/store.rb +101 -0
  130. data/lib/kward/transport.rb +204 -0
  131. data/lib/kward/version.rb +1 -1
  132. data/lib/kward/workspace.rb +18 -3
  133. data/lib/kward/workspace_factory.rb +34 -0
  134. data/templates/default/fulldoc/html/js/kward.js +3 -0
  135. data/templates/default/kward_navigation.rb +8 -1
  136. data/templates/default/layout/html/setup.rb +5 -0
  137. 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
@@ -1,5 +1,5 @@
1
1
  # Namespace for the Kward CLI agent runtime.
2
2
  module Kward
3
3
  # Current gem version.
4
- VERSION = "0.78.0"
4
+ VERSION = "0.80.0"
5
5
  end
@@ -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 = LocalCommandRunner.new(timeout_seconds: timeout_seconds, max_output_bytes: @max_command_output_bytes).run(command, cwd: @root.to_s, cancellation: cancellation)
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.78.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.2'
428
+ version: '3.4'
390
429
  required_rubygems_version: !ruby/object:Gem::Requirement
391
430
  requirements:
392
431
  - - ">="