kward 0.80.0 → 0.81.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 +40 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/doc/composer.md +5 -8
- data/doc/configuration.md +13 -3
- data/doc/permissions.md +1 -1
- data/doc/rpc.md +2 -1
- data/doc/sandboxing.md +4 -4
- data/doc/security.md +6 -9
- data/doc/shell.md +161 -196
- data/doc/skills.md +19 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/ansi.rb +9 -1
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/interactive_turn.rb +1 -1
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/project_skills.rb +99 -0
- data/lib/kward/cli/project_skills_commands.rb +87 -0
- data/lib/kward/cli/prompt_interface.rb +54 -4
- data/lib/kward/cli/rendering.rb +28 -2
- data/lib/kward/cli/runtime_helpers.rb +157 -19
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +1 -1
- data/lib/kward/cli/slash_commands.rb +7 -1
- data/lib/kward/cli/tabs.rb +4 -1
- data/lib/kward/cli.rb +21 -1
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +30 -9
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/interactive_pty_runner.rb +88 -22
- data/lib/kward/plugin_registry.rb +20 -13
- data/lib/kward/prompt_interface/composer_controller.rb +13 -1
- data/lib/kward/prompt_interface/editor/controller.rb +4 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +35 -10
- data/lib/kward/prompt_interface/key_handler.rb +78 -43
- data/lib/kward/prompt_interface/overlay_renderer.rb +12 -0
- data/lib/kward/prompt_interface/screen.rb +5 -0
- data/lib/kward/prompt_interface.rb +93 -21
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/prompts.rb +6 -6
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/session_store.rb +3 -2
- data/lib/kward/skills/registry.rb +67 -12
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/version.rb +1 -1
- metadata +5 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "pathname"
|
|
2
|
+
require_relative "trust_store"
|
|
2
3
|
|
|
3
4
|
# Namespace for the Kward CLI agent runtime.
|
|
4
5
|
module Kward
|
|
@@ -13,8 +14,9 @@ module Kward
|
|
|
13
14
|
# @api public
|
|
14
15
|
class Registry
|
|
15
16
|
SkillSource = Struct.new(:root, :label, :scope, :precedence, keyword_init: true)
|
|
17
|
+
SkillCandidate = Struct.new(:path, :root, :label, :scope, :digest, keyword_init: true)
|
|
16
18
|
|
|
17
|
-
def initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:)
|
|
19
|
+
def initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:, warning_sink: nil, project_skill_paths: nil)
|
|
18
20
|
@config_dir = config_dir
|
|
19
21
|
@workspace_root = workspace_root
|
|
20
22
|
@project_skills_trusted = project_skills_trusted
|
|
@@ -22,6 +24,33 @@ module Kward
|
|
|
22
24
|
@max_file_bytes = max_file_bytes
|
|
23
25
|
@markdown_parser = markdown_parser
|
|
24
26
|
@inside_directory = inside_directory
|
|
27
|
+
@warning_sink = warning_sink
|
|
28
|
+
@project_skill_paths = project_skill_paths&.map do |path|
|
|
29
|
+
File.realpath(path)
|
|
30
|
+
rescue SystemCallError
|
|
31
|
+
File.expand_path(path)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns project skill files without activating their instructions.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<SkillCandidate>]
|
|
38
|
+
# @api public
|
|
39
|
+
def project_skill_candidates
|
|
40
|
+
skill_sources.select { |source| source.scope == :project }.flat_map do |source|
|
|
41
|
+
discover_source(source).filter_map do |path|
|
|
42
|
+
SkillCandidate.new(
|
|
43
|
+
path: path,
|
|
44
|
+
root: source.root,
|
|
45
|
+
label: source.label,
|
|
46
|
+
scope: source.scope,
|
|
47
|
+
digest: skill_digest(path)
|
|
48
|
+
)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
emit_warning "Warning: skipping Kward skill #{path}: #{e.message}"
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
25
54
|
end
|
|
26
55
|
|
|
27
56
|
# Returns discovered, validated skills in precedence order.
|
|
@@ -36,7 +65,7 @@ module Kward
|
|
|
36
65
|
next unless skill
|
|
37
66
|
|
|
38
67
|
if seen[skill.name]
|
|
39
|
-
|
|
68
|
+
emit_warning "Warning: skipping duplicate Kward skill #{skill.name.inspect}: #{path}"
|
|
40
69
|
next
|
|
41
70
|
end
|
|
42
71
|
|
|
@@ -45,7 +74,7 @@ module Kward
|
|
|
45
74
|
end
|
|
46
75
|
end
|
|
47
76
|
rescue StandardError => e
|
|
48
|
-
|
|
77
|
+
emit_warning "Warning: skipping Kward skills: #{e.message}"
|
|
49
78
|
[]
|
|
50
79
|
end
|
|
51
80
|
|
|
@@ -98,17 +127,39 @@ module Kward
|
|
|
98
127
|
|
|
99
128
|
def scan_source(source)
|
|
100
129
|
return [] unless Dir.exist?(source.root)
|
|
101
|
-
if source.scope == :project && !@project_skills_trusted
|
|
102
|
-
|
|
130
|
+
if source.scope == :project && @project_skill_paths.nil? && !@project_skills_trusted
|
|
131
|
+
emit_warning "Warning: skipping #{source.label} in #{source.root}: project skills are not trusted"
|
|
103
132
|
return []
|
|
104
133
|
end
|
|
105
134
|
|
|
135
|
+
paths = discover_source(source)
|
|
136
|
+
return paths unless source.scope == :project && @project_skill_paths
|
|
137
|
+
|
|
138
|
+
paths.select { |path| @project_skill_paths.include?(canonical_path(path)) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def canonical_path(path)
|
|
142
|
+
File.realpath(path)
|
|
143
|
+
rescue SystemCallError
|
|
144
|
+
File.expand_path(path)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def discover_source(source)
|
|
106
148
|
Dir.glob(File.join(source.root, "*", "SKILL.md")).sort
|
|
107
149
|
rescue StandardError => e
|
|
108
|
-
|
|
150
|
+
emit_warning "Warning: skipping #{source.label} in #{source.root}: #{e.message}"
|
|
109
151
|
[]
|
|
110
152
|
end
|
|
111
153
|
|
|
154
|
+
def skill_digest(path)
|
|
155
|
+
TrustStore.digest_files(skill_files(path), root: @workspace_root)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def skill_files(path)
|
|
159
|
+
folder = File.dirname(path)
|
|
160
|
+
[path] + skill_resources(folder).map { |resource| File.join(folder, resource) }
|
|
161
|
+
end
|
|
162
|
+
|
|
112
163
|
def skill_content(skill, content)
|
|
113
164
|
lines = [
|
|
114
165
|
%(<skill_content name="#{xml_escape(skill.name)}">),
|
|
@@ -151,12 +202,12 @@ module Kward
|
|
|
151
202
|
return warn_skip(path, "missing description") if description.empty?
|
|
152
203
|
return warn_skip(path, "description exceeds 1024 characters") if description.length > 1024
|
|
153
204
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
emit_warning "Warning: Kward skill #{path}: name does not match parent directory" if name != File.basename(File.dirname(path))
|
|
206
|
+
emit_warning "Warning: Kward skill #{path}: name exceeds 64 characters" if name.length > 64
|
|
207
|
+
emit_warning "Warning: Kward skill #{path}: name contains invalid characters" unless valid_name?(name)
|
|
157
208
|
|
|
158
209
|
compatibility = optional_text(frontmatter["compatibility"])
|
|
159
|
-
|
|
210
|
+
emit_warning "Warning: Kward skill #{path}: compatibility exceeds 500 characters" if compatibility && compatibility.length > 500
|
|
160
211
|
|
|
161
212
|
@skill_class.new(
|
|
162
213
|
name: name,
|
|
@@ -169,7 +220,7 @@ module Kward
|
|
|
169
220
|
allowed_tools: optional_text(frontmatter["allowed-tools"])
|
|
170
221
|
)
|
|
171
222
|
rescue StandardError => e
|
|
172
|
-
|
|
223
|
+
emit_warning "Warning: skipping Kward skill #{path}: #{e.message}"
|
|
173
224
|
nil
|
|
174
225
|
end
|
|
175
226
|
|
|
@@ -183,9 +234,13 @@ module Kward
|
|
|
183
234
|
end
|
|
184
235
|
|
|
185
236
|
def warn_skip(path, reason)
|
|
186
|
-
|
|
237
|
+
emit_warning "Warning: skipping Kward skill #{path}: #{reason}"
|
|
187
238
|
nil
|
|
188
239
|
end
|
|
240
|
+
|
|
241
|
+
def emit_warning(message)
|
|
242
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
243
|
+
end
|
|
189
244
|
end
|
|
190
245
|
end
|
|
191
246
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative "trust_store"
|
|
2
|
+
|
|
3
|
+
# Namespace for Agent Skills discovery and trust management.
|
|
4
|
+
module Kward
|
|
5
|
+
module Skills
|
|
6
|
+
# Resolves which discovered project skills need a user decision.
|
|
7
|
+
class TrustCoordinator
|
|
8
|
+
def initialize(workspace_root:, trust_store:)
|
|
9
|
+
@workspace_root = workspace_root
|
|
10
|
+
@trust_store = trust_store
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def decision(candidate)
|
|
14
|
+
@trust_store.decision(
|
|
15
|
+
workspace_root: @workspace_root,
|
|
16
|
+
skill_path: candidate.path,
|
|
17
|
+
digest: candidate.digest
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def pending(candidates)
|
|
22
|
+
candidates.reject { |candidate| decision(candidate) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def allowed_paths(candidates)
|
|
26
|
+
candidates.filter_map { |candidate| candidate.path if decision(candidate) == "allow" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove_workspace!
|
|
30
|
+
@trust_store.remove_workspace!(workspace_root: @workspace_root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def record!(candidates, decision)
|
|
34
|
+
candidates.each do |candidate|
|
|
35
|
+
@trust_store.set_decision!(
|
|
36
|
+
workspace_root: @workspace_root,
|
|
37
|
+
skill_path: candidate.path,
|
|
38
|
+
digest: candidate.digest,
|
|
39
|
+
decision: decision
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "json"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "../path_guard"
|
|
5
|
+
require_relative "../private_file"
|
|
6
|
+
|
|
7
|
+
# Namespace for Agent Skills discovery and trust management.
|
|
8
|
+
module Kward
|
|
9
|
+
module Skills
|
|
10
|
+
# Persists workspace-scoped decisions for project skill snapshots.
|
|
11
|
+
class TrustStore
|
|
12
|
+
DECISIONS = %w[allow deny].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :path
|
|
15
|
+
|
|
16
|
+
def initialize(config_dir:)
|
|
17
|
+
@path = File.join(File.expand_path(config_dir), "trusted_project_skills.json")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def decision(workspace_root:, skill_path:, digest:)
|
|
21
|
+
record = skill_record(workspace_root: workspace_root, skill_path: skill_path)
|
|
22
|
+
return unless record
|
|
23
|
+
return unless record["digest"] == digest.to_s
|
|
24
|
+
|
|
25
|
+
decision = record["decision"]
|
|
26
|
+
decision if DECISIONS.include?(decision)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_decision!(workspace_root:, skill_path:, digest:, decision:)
|
|
30
|
+
decision = decision.to_s
|
|
31
|
+
raise ArgumentError, "invalid project skill trust decision: #{decision}" unless DECISIONS.include?(decision)
|
|
32
|
+
|
|
33
|
+
config = read_config
|
|
34
|
+
workspace = workspace_key(workspace_root)
|
|
35
|
+
skill = skill_key(workspace_root, skill_path)
|
|
36
|
+
config["workspaces"] ||= {}
|
|
37
|
+
config["workspaces"][workspace] ||= { "skills" => {} }
|
|
38
|
+
config["workspaces"][workspace]["skills"] ||= {}
|
|
39
|
+
config["workspaces"][workspace]["skills"][skill] = {
|
|
40
|
+
"digest" => digest.to_s,
|
|
41
|
+
"decision" => decision
|
|
42
|
+
}
|
|
43
|
+
PrivateFile.write_json(path, config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove_workspace!(workspace_root:)
|
|
47
|
+
config = read_config
|
|
48
|
+
config.fetch("workspaces", {}).delete(workspace_key(workspace_root))
|
|
49
|
+
PrivateFile.write_json(path, config)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def remove_skill!(workspace_root:, skill_path:)
|
|
53
|
+
config = read_config
|
|
54
|
+
workspace = config.fetch("workspaces", {})[workspace_key(workspace_root)]
|
|
55
|
+
workspace&.fetch("skills", {})&.delete(skill_key(workspace_root, skill_path))
|
|
56
|
+
PrivateFile.write_json(path, config)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.digest_files(paths, root: nil)
|
|
60
|
+
root = File.realpath(root) if root
|
|
61
|
+
digest = Digest::SHA256.new
|
|
62
|
+
paths.map { |path| File.realpath(path) }.sort.each do |path|
|
|
63
|
+
raise ArgumentError, "file is outside digest root" if root && !PathGuard.inside?(path, root)
|
|
64
|
+
|
|
65
|
+
relative_path = root ? Pathname.new(path).relative_path_from(Pathname.new(root)).to_s : path
|
|
66
|
+
digest.update(relative_path)
|
|
67
|
+
digest.update("\0")
|
|
68
|
+
digest.update(File.binread(path))
|
|
69
|
+
digest.update("\0")
|
|
70
|
+
end
|
|
71
|
+
digest.hexdigest
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def skill_record(workspace_root:, skill_path:)
|
|
77
|
+
workspace = read_config.fetch("workspaces", {})[workspace_key(workspace_root)]
|
|
78
|
+
workspace&.fetch("skills", {})&.[](skill_key(workspace_root, skill_path))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def read_config
|
|
82
|
+
return {} unless File.file?(path)
|
|
83
|
+
|
|
84
|
+
config = JSON.parse(File.read(path))
|
|
85
|
+
config.is_a?(Hash) ? config : {}
|
|
86
|
+
rescue JSON::ParserError, SystemCallError
|
|
87
|
+
{}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def workspace_key(workspace_root)
|
|
91
|
+
File.realpath(workspace_root)
|
|
92
|
+
rescue SystemCallError
|
|
93
|
+
File.expand_path(workspace_root)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def skill_key(workspace_root, skill_path)
|
|
97
|
+
workspace = workspace_key(workspace_root)
|
|
98
|
+
skill = File.realpath(skill_path)
|
|
99
|
+
raise ArgumentError, "project skill is outside workspace" unless PathGuard.inside?(skill, workspace)
|
|
100
|
+
|
|
101
|
+
Pathname.new(skill).relative_path_from(Pathname.new(workspace)).to_s
|
|
102
|
+
rescue SystemCallError
|
|
103
|
+
raise ArgumentError, "project skill path does not exist"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -20,13 +20,14 @@ module Kward
|
|
|
20
20
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
|
|
21
21
|
|
|
22
22
|
# Creates an object for telemetry event logging.
|
|
23
|
-
def initialize(config_path: ConfigFiles.config_path, log_dir: nil, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr)
|
|
23
|
+
def initialize(config_path: ConfigFiles.config_path, log_dir: nil, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr, warning_sink: nil)
|
|
24
24
|
@config_path = config_path
|
|
25
25
|
@log_dir = log_dir
|
|
26
26
|
@max_bytes = max_bytes.to_i.positive? ? max_bytes.to_i : DEFAULT_MAX_BYTES
|
|
27
27
|
@clock = clock
|
|
28
28
|
@monotonic_clock = monotonic_clock
|
|
29
29
|
@error_output = error_output
|
|
30
|
+
@warning_sink = warning_sink
|
|
30
31
|
@mutex = Mutex.new
|
|
31
32
|
@warned = false
|
|
32
33
|
end
|
|
@@ -192,7 +193,9 @@ module Kward
|
|
|
192
193
|
return if @warned
|
|
193
194
|
|
|
194
195
|
@warned = true
|
|
195
|
-
|
|
196
|
+
message = "Warning: telemetry logging failed: #{error.message}"
|
|
197
|
+
sink = @warning_sink || ConfigFiles.warning_sink
|
|
198
|
+
sink ? sink.call(message) : @error_output&.puts(message)
|
|
196
199
|
rescue StandardError
|
|
197
200
|
nil
|
|
198
201
|
end
|
data/lib/kward/version.rb
CHANGED
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.81.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kai Wood
|
|
@@ -191,6 +191,8 @@ files:
|
|
|
191
191
|
- lib/kward/cli/memory_commands.rb
|
|
192
192
|
- lib/kward/cli/openrouter_commands.rb
|
|
193
193
|
- lib/kward/cli/plugins.rb
|
|
194
|
+
- lib/kward/cli/project_skills.rb
|
|
195
|
+
- lib/kward/cli/project_skills_commands.rb
|
|
194
196
|
- lib/kward/cli/prompt_interface.rb
|
|
195
197
|
- lib/kward/cli/rendering.rb
|
|
196
198
|
- lib/kward/cli/runtime_helpers.rb
|
|
@@ -353,6 +355,8 @@ files:
|
|
|
353
355
|
- lib/kward/session_tree_tool_display.rb
|
|
354
356
|
- lib/kward/skills/capture.rb
|
|
355
357
|
- lib/kward/skills/registry.rb
|
|
358
|
+
- lib/kward/skills/trust_coordinator.rb
|
|
359
|
+
- lib/kward/skills/trust_store.rb
|
|
356
360
|
- lib/kward/starter_pack_installer.rb
|
|
357
361
|
- lib/kward/steering.rb
|
|
358
362
|
- lib/kward/tab_driver.rb
|