rosett-ai 1.6.1 → 1.8.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8968373d949471c707db0f0bddbe526797d8e47fdaf80b53a17d65e7fb5cb198
4
- data.tar.gz: efdd68439a73eb12e2f6b503e999c2d0a40c9a6d9c73693da59cbacd9c3dab0c
3
+ metadata.gz: 95d279ef7cfbcfabd17980dcc444d353b1af0b31a1d19db87e5eaef99e59b4ab
4
+ data.tar.gz: a14e73c818b37af55a2eaefcb2ace6f9574a375faf24b8c30daa4aeabd8e5c57
5
5
  SHA512:
6
- metadata.gz: 82e690044b7a4b4a54d3bf5f562c1b27711cb462b973d5d3d0078c3c0c8e84ca20dec5f027949d4cd0c233c26d4d321fe42cb20cfe9bb8ab7bdb068613c7787f
7
- data.tar.gz: 4680e074d51a261d6ad4a98ce477d990b2ddf4187b30f90883c3d26cb86b1c7cfe6502b0ea7c5a07434f692111dc296e9a5eebb4ecb6477913bb5e6516b111d8
6
+ metadata.gz: edbf2bdf9f239fd288e55bf403b98dec1f5ae804a5437ff82e170c68a8f042b63426f19e4bade372e8425e3bda0b6d51d969cb47cf18c01d553f161feda0dc95
7
+ data.tar.gz: 9ac58d88a91de6ae5024c29acc51013ead935f63485adf128e07fdf9a2edd07d293e857023fc2c8bb8680114c62305e974709313ba2fa4bed1c77d54251a4a1b
data/CHANGELOG.md CHANGED
@@ -6,6 +6,74 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.8.1] - 2026-08-14
10
+
11
+ ### Fixed
12
+
13
+ - **`HookGenerator#format_regex` regex-literal syntax error** — the generator
14
+ previously emitted patterns as `/regex/` literals, which produced a Ruby
15
+ `SyntaxError` when the pattern contained a forward slash inside a character
16
+ class (e.g. `[^\/\s]`) or as a path separator (e.g. `\/tmp\/`). Ruby parses
17
+ the first unescaped `/` as the closing regex delimiter, silently breaking the
18
+ hook. Both the `pattern` and `path_scope` fields now emit `Regexp.new("...")`
19
+ calls, which are immune to delimiter conflicts. The bug affected every
20
+ generated hook since the feature was introduced; no hook had ever parsed
21
+ cleanly on the real rule set.
22
+
23
+ - **`rai_hook_install` parse gate** — `HookInstallTool#write_hook` now runs
24
+ `ruby -c` on the generated script before writing it to disk. A `SyntaxError`
25
+ in the generated hook now raises immediately with a descriptive message rather
26
+ than silently installing a broken file.
27
+
28
+ - **Regression specs** — `hook_generator_spec.rb` now includes contexts for
29
+ patterns containing `/` inside character classes and path separators, with
30
+ assertions that `RubyVM::InstructionSequence.compile` succeeds and that the
31
+ emitted form is `Regexp.new(...)` rather than a `/regex/` literal.
32
+
33
+ ## [1.8.0] - 2026-08-13
34
+
35
+ ### Added
36
+
37
+ - **Enforcement rule scoping** — `HookGenerator` now supports per-rule `match_target`
38
+ (`path` | `content` | `both`) and optional `path_scope` (regex guard on the target
39
+ file path) fields in generated PreToolUse hook scripts. Rules without these fields
40
+ default to `both`, preserving backward-compatible behaviour. This eliminates false
41
+ positives that occurred when security-documentation text mentioning a forbidden
42
+ pattern triggered the rule against the file that documents it (e.g. sec_018
43
+ documentation was itself blocked).
44
+
45
+ - **Behaviour schema v1.5.0** — two new optional fields on the `enforcement` block:
46
+ - `match_target: "path" | "content" | "both"` — selects which part of the tool
47
+ input the rule pattern is tested against. Default: `"both"`.
48
+ - `path_scope: string` — regex; when set, the rule only applies when the target
49
+ file path matches. Ignored when `match_target` is `"path"`.
50
+
51
+ - **`Validator#build_valid_entry`** — passes `match_target` and `path_scope` through
52
+ to the rule hash so `HookGenerator` can embed them in RULES entries.
53
+
54
+ ## [1.7.0] - 2026-08-02
55
+
56
+ ### Added
57
+
58
+ - **sec_018 bypass vector enforcement** — `HookGenerator` now uses per-tool field
59
+ extraction when generating PreToolUse hook scripts, preventing false positives
60
+ from raw hash stringification. Bash hooks match against the `command` field only;
61
+ Write/Edit hooks match against `file_path` and content fields. Covers all
62
+ sec_018 bypass vectors: `--no-verify`, `OVERCOMMIT_DISABLE`, `core.hooksPath=`,
63
+ `ruby-audit --ignore`, `SKIP=`, `allow_failure: true` on CI gates, and writes
64
+ to the bundler-audit ignore file. Red-team spec suite added covering all
65
+ known bypass vectors.
66
+
67
+ ## [1.6.2] - 2026-07-25
68
+
69
+ ### Fixed
70
+
71
+ - **`raictl config compile` / `config show` crash with older engine plugins** — `resolve_config_compiler`
72
+ unconditionally passed `strict:` to the engine's `ConfigCompiler#initialize`, causing
73
+ `ArgumentError: unknown keyword: :strict` with engine gems whose constructor predates the
74
+ `strict` parameter. The fix introspects the constructor's parameter list at runtime and
75
+ only includes `strict:` when the engine supports it.
76
+
9
77
  ## [1.6.1] - 2026-07-23
10
78
 
11
79
  ### Security
@@ -4,8 +4,8 @@
4
4
  "title": "NeatNerds Behaviour Configuration",
5
5
  "description": "Schema for validating behaviour configuration files used by the NeatNerds Code Companion (rosett-ai). Behaviour files define operational rules for Claude Code workflows.",
6
6
  "$comment": {
7
- "date": "2026-07-19",
8
- "version": "1.4.0",
7
+ "date": "2026-08-13",
8
+ "version": "1.5.0",
9
9
  "source": "rosett-ai",
10
10
  "compatible_claude_code_versions": [
11
11
  ">=0.1.0"
@@ -142,6 +142,16 @@
142
142
  "message": {
143
143
  "type": "string",
144
144
  "description": "Human-readable message shown when the rule triggers"
145
+ },
146
+ "match_target": {
147
+ "type": "string",
148
+ "enum": ["path", "content", "both"],
149
+ "description": "Which part of the tool input the pattern is tested against. Default: both (concatenated path + content, current behaviour)."
150
+ },
151
+ "path_scope": {
152
+ "type": "string",
153
+ "minLength": 1,
154
+ "description": "Regex string. When set, rule applies only when the target path matches. Ignored when match_target is path."
145
155
  }
146
156
  },
147
157
  "required": [
@@ -8,7 +8,7 @@ module RosettAi
8
8
  # Resolves the correct executor for an engine by reading its manifest
9
9
  # to determine the API type and constructor kwargs.
10
10
  #
11
- # SDK-based engines (api_gem) receive api_key: and base_url:.
11
+ # SDK-based engines (api_gem) receive api_key: and base_url:. # gitleaks:allow
12
12
  # HTTP-based engines (api_type: http|openai_compatible) receive
13
13
  # endpoint: and model: from the manifest defaults.
14
14
  # CLI-based engines (api_type: cli) receive binary: from the manifest.
@@ -5,7 +5,7 @@
5
5
 
6
6
  module RosettAi
7
7
  module Config
8
- # Resolves ${secret:backend:key} references in configuration values.
8
+ # Resolves ${secret:backend:key} references in configuration values. # gitleaks:allow
9
9
  #
10
10
  # Security constraints:
11
11
  # - NO regex anywhere — deterministic string parsing only (ReDoS prevention)
@@ -16,7 +16,7 @@ module RosettAi
16
16
  # is held server-side and NEVER distributed.
17
17
  class LicenseKey
18
18
  # @return [String] Hex-encoded public key for license verification.
19
- PUBLIC_KEY_HEX = '91f54336c0d42a7c88642b8d7f8bacab3adba4a4d633a33002fc15f676bfd8f0'
19
+ PUBLIC_KEY_HEX = '91f54336c0d42a7c88642b8d7f8bacab3adba4a4d633a33002fc15f676bfd8f0' # gitleaks:allow
20
20
  # @return [String] Prefix for rosett-ai license key strings.
21
21
  RAI_PREFIX = 'NNCC-'
22
22
  # @return [Integer] Days of grace period after license expiry.
@@ -163,17 +163,23 @@ module RosettAi
163
163
  " pattern: #{format_regex(rule[:pattern])},",
164
164
  " applies_to: [#{tools}],",
165
165
  " action: :#{rule[:action]},",
166
+ " match_target: :#{rule.fetch(:match_target, 'both')},",
167
+ " path_scope: #{rule[:path_scope] ? format_regex(rule[:path_scope]) : 'nil'},",
166
168
  " message: '#{msg}',",
167
169
  " description: '#{desc}' }"
168
170
  ].join("\n ")
169
171
  end
170
172
 
171
- # Formats a pattern string as a Regexp literal for the generated script.
173
+ # Formats a pattern string as a Regexp.new() call for the generated script.
174
+ #
175
+ # Uses Regexp.new("...") rather than /regex/ literals to avoid delimiter
176
+ # conflicts when patterns contain forward slashes (e.g. inside character
177
+ # classes like [^\/\s] or path patterns like \/tmp\/).
172
178
  #
173
179
  # @param pattern [String]
174
180
  # @return [String]
175
181
  def format_regex(pattern)
176
- "/#{pattern.gsub('/', '\\/')}/"
182
+ "Regexp.new(#{pattern.inspect})"
177
183
  end
178
184
 
179
185
  # Sanitizes a string for safe embedding in single-quoted Ruby strings.
@@ -189,6 +195,10 @@ module RosettAi
189
195
  # Generates the runtime enforcement logic body.
190
196
  # Uses non-interpolating heredoc — this code runs at hook time, not generation time.
191
197
  #
198
+ # Per-tool field extraction prevents false positives from raw hash stringification.
199
+ # Each tool has a known input schema; we extract only the fields that carry
200
+ # user-controlled content that sec_018 patterns must match against.
201
+ #
192
202
  # @return [String]
193
203
  def enforcement_body
194
204
  <<~'RUBY'
@@ -196,10 +206,39 @@ module RosettAi
196
206
 
197
207
  input = JSON.parse($stdin.read)
198
208
  tool_name = input['tool_name'].to_s
199
- tool_input = input['tool_input'].to_s
209
+
210
+ target_path, target_content = case tool_name
211
+ when 'Bash'
212
+ cmd = input.dig('tool_input', 'command').to_s
213
+ [cmd, cmd]
214
+ when 'Write'
215
+ [input.dig('tool_input', 'file_path').to_s,
216
+ input.dig('tool_input', 'content').to_s]
217
+ when 'Edit'
218
+ content = [input.dig('tool_input', 'old_string'),
219
+ input.dig('tool_input', 'new_string')].compact.join(' ')
220
+ [input.dig('tool_input', 'file_path').to_s, content]
221
+ when 'Agent'
222
+ ['', input.dig('tool_input', 'prompt').to_s]
223
+ else
224
+ raw = input.fetch('tool_input', '').to_s
225
+ [raw, raw]
226
+ end
200
227
 
201
228
  violations = RULES.select do |rule|
202
- rule[:applies_to].include?(tool_name) && rule[:pattern].match?(tool_input)
229
+ next false unless rule[:applies_to].include?(tool_name)
230
+
231
+ if rule[:path_scope] && rule[:match_target] != :path
232
+ next false unless rule[:path_scope].match?(target_path)
233
+ end
234
+
235
+ match_text = case rule[:match_target]
236
+ when :path then target_path
237
+ when :content then target_content
238
+ else "#{target_path} #{target_content}"
239
+ end
240
+
241
+ rule[:pattern].match?(match_text)
203
242
  end
204
243
 
205
244
  exit 0 if violations.empty?
@@ -209,7 +248,6 @@ module RosettAi
209
248
  "(P#{v[:priority]}, #{v[:action]}): #{v[:message].empty? ? v[:description] : v[:message]}"
210
249
  end
211
250
 
212
- # Block if any violation has action :block
213
251
  blocked = violations.any? { |v| v[:action] == :block }
214
252
  exit(blocked ? 2 : 0)
215
253
  RUBY
@@ -228,7 +228,9 @@ module RosettAi
228
228
  { rule_id: ctx.rule_id, behaviour: ctx.behaviour, type: 'enforceable',
229
229
  pattern: enforcement['pattern'], applies_to: enforcement['applies_to'],
230
230
  action: derive_action(enforcement, priority), message: enforcement['message'],
231
- priority: priority, description: rule['description'].to_s.strip }
231
+ priority: priority, description: rule['description'].to_s.strip,
232
+ match_target: (enforcement['match_target'] || 'both').to_s,
233
+ path_scope: enforcement['path_scope'] }
232
234
  end
233
235
  end
234
236
  end
@@ -142,12 +142,26 @@ module RosettAi
142
142
 
143
143
  # @param hook_path [Pathname]
144
144
  # @param content [String]
145
+ # @raise [RuntimeError] if the generated script fails Ruby syntax check
145
146
  def write_hook(hook_path, content)
147
+ verify_ruby_syntax!(content)
146
148
  FileUtils.mkdir_p(hook_path.dirname)
147
149
  File.write(hook_path, content)
148
150
  FileUtils.chmod(0o755, hook_path)
149
151
  end
150
152
 
153
+ # Verifies generated hook content parses as valid Ruby before writing.
154
+ # Uses RubyVM::InstructionSequence.compile so no subprocess, Tempfile,
155
+ # or writable /tmp directory is required — the check works in CI containers.
156
+ #
157
+ # @param content [String] generated script content
158
+ # @raise [RuntimeError] if syntax check fails
159
+ def verify_ruby_syntax!(content)
160
+ RubyVM::InstructionSequence.compile(content)
161
+ rescue SyntaxError => e
162
+ raise "Generated hook failed Ruby syntax check: #{e.message}"
163
+ end
164
+
151
165
  # Registers the hook in settings.json if not already present.
152
166
  #
153
167
  # @param hook_path [Pathname]
@@ -6,7 +6,7 @@
6
6
  module RosettAi
7
7
  module Retrofit
8
8
  # Detects sensitive values in parsed configuration data and replaces
9
- # them with ${secret:env:NAME} references.
9
+ # them with ${secret:env:NAME} references. # gitleaks:allow
10
10
  #
11
11
  # Recognises API keys, tokens, passwords, and other secrets by
12
12
  # key name patterns and value heuristics (length, entropy, prefixes).
@@ -69,7 +69,7 @@ module RosettAi
69
69
  env_name = full_key.upcase.gsub(/[^A-Z0-9]/, '_').squeeze('_')
70
70
  @warnings << "Sensitive value detected for '#{key}' — replaced with secret reference. " \
71
71
  "Set #{env_name} in your environment."
72
- "${secret:env:#{env_name}}"
72
+ "${secret:env:#{env_name}}" # gitleaks:allow
73
73
  end
74
74
 
75
75
  def sensitive?(key, value)
@@ -143,8 +143,12 @@ module RosettAi
143
143
  end
144
144
 
145
145
  secret_resolver = masking ? RosettAi::Config::MaskingSecretResolver.new : nil
146
- plugin.config_compiler_class.new(simulate: simulate, secret_resolver: secret_resolver,
147
- strict: strict)
146
+ klass = plugin.config_compiler_class
147
+ params = { simulate: simulate, secret_resolver: secret_resolver }
148
+ init_params = klass.instance_method(:initialize).parameters
149
+ supports_strict = init_params.any? { |type, name| name == :strict && [:key, :keyreq].include?(type) }
150
+ params[:strict] = strict if supports_strict
151
+ klass.new(**params)
148
152
  end
149
153
 
150
154
  def engine_hint(available, engine_name)
@@ -5,5 +5,5 @@
5
5
 
6
6
  module RosettAi
7
7
  # @return [String] Current version of the rosett-ai gem.
8
- VERSION = '1.6.1'
8
+ VERSION = '1.8.1'
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rosett-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hugo Antonio Sepulveda Manriquez