rosett-ai 1.5.12 → 1.8.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 +73 -0
- data/conf/schemas/behaviour_schema.json +36 -2
- data/lib/rosett_ai/adopter/executor_resolver.rb +1 -1
- data/lib/rosett_ai/config/secret_resolver.rb +1 -1
- data/lib/rosett_ai/licensing/license_key.rb +1 -1
- data/lib/rosett_ai/mcp/enforcement/hook_generator.rb +37 -3
- data/lib/rosett_ai/mcp/enforcement/validator.rb +3 -1
- data/lib/rosett_ai/mcp/governance.rb +21 -2
- data/lib/rosett_ai/retrofit/secret_detector.rb +2 -2
- data/lib/rosett_ai/thor/tasks/config.rb +6 -2
- data/lib/rosett_ai/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4199d3afb89e1e5378dad08e0f989c085375643d7f48436a4786c7b7c615f646
|
|
4
|
+
data.tar.gz: 3af391ea23387c019d6247cb8e77ca78586f5e0e9626cca37b1946d3f4dff31f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0db1b2103c8b5334786442adea3cb254ff81389354a6dba39a0a38c38d41ba0f337dab6be90758b75c8ae51e6ada6e391befe8f671502ecd08634133067e1d23
|
|
7
|
+
data.tar.gz: edb943d87180a8656fdd5a33a5d20e5cb3bf05201824fbfc581c8c13b31102ec9449d3e346bb3fe05612d63333abe89331eadb0fb116c626821d5f64b6d1eadb
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,79 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.8.0] - 2026-08-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Enforcement rule scoping** — `HookGenerator` now supports per-rule `match_target`
|
|
14
|
+
(`path` | `content` | `both`) and optional `path_scope` (regex guard on the target
|
|
15
|
+
file path) fields in generated PreToolUse hook scripts. Rules without these fields
|
|
16
|
+
default to `both`, preserving backward-compatible behaviour. This eliminates false
|
|
17
|
+
positives that occurred when security-documentation text mentioning a forbidden
|
|
18
|
+
pattern triggered the rule against the file that documents it (e.g. sec_018
|
|
19
|
+
documentation was itself blocked).
|
|
20
|
+
|
|
21
|
+
- **Behaviour schema v1.5.0** — two new optional fields on the `enforcement` block:
|
|
22
|
+
- `match_target: "path" | "content" | "both"` — selects which part of the tool
|
|
23
|
+
input the rule pattern is tested against. Default: `"both"`.
|
|
24
|
+
- `path_scope: string` — regex; when set, the rule only applies when the target
|
|
25
|
+
file path matches. Ignored when `match_target` is `"path"`.
|
|
26
|
+
|
|
27
|
+
- **`Validator#build_valid_entry`** — passes `match_target` and `path_scope` through
|
|
28
|
+
to the rule hash so `HookGenerator` can embed them in RULES entries.
|
|
29
|
+
|
|
30
|
+
## [1.7.0] - 2026-08-02
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **sec_018 bypass vector enforcement** — `HookGenerator` now uses per-tool field
|
|
35
|
+
extraction when generating PreToolUse hook scripts, preventing false positives
|
|
36
|
+
from raw hash stringification. Bash hooks match against the `command` field only;
|
|
37
|
+
Write/Edit hooks match against `file_path` and content fields. Covers all
|
|
38
|
+
sec_018 bypass vectors: `--no-verify`, `OVERCOMMIT_DISABLE`, `core.hooksPath=`,
|
|
39
|
+
`ruby-audit --ignore`, `SKIP=`, `allow_failure: true` on CI gates, and writes
|
|
40
|
+
to the bundler-audit ignore file. Red-team spec suite added covering all
|
|
41
|
+
known bypass vectors.
|
|
42
|
+
|
|
43
|
+
## [1.6.2] - 2026-07-25
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`raictl config compile` / `config show` crash with older engine plugins** — `resolve_config_compiler`
|
|
48
|
+
unconditionally passed `strict:` to the engine's `ConfigCompiler#initialize`, causing
|
|
49
|
+
`ArgumentError: unknown keyword: :strict` with engine gems whose constructor predates the
|
|
50
|
+
`strict` parameter. The fix introspects the constructor's parameter list at runtime and
|
|
51
|
+
only includes `strict:` when the engine supports it.
|
|
52
|
+
|
|
53
|
+
## [1.6.1] - 2026-07-23
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
|
|
57
|
+
- **CVE-2026-41316**: pin `erb` to `>= 6.0.6`; update Gemfile.lock to erb 6.0.6 —
|
|
58
|
+
resolves the gem-level vector of the ERB deserialization bypass advisory.
|
|
59
|
+
|
|
60
|
+
## [1.6.0] - 2026-07-19
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **Behaviour-rule schema v1.4.0** — three new optional, backward-compatible fields on rule items,
|
|
65
|
+
laying the foundation for mechanical context-load reduction:
|
|
66
|
+
- `enforced: boolean` — marks rules whose behaviour is deterministically enforced by a live
|
|
67
|
+
PreToolUse/other hook; the C2 lean-context emitter will omit these from loaded bundles.
|
|
68
|
+
- `applies_to: [enum]` — one or more of `worker-impl`, `worker-research`, `orchestrator`,
|
|
69
|
+
`interactive`, `all`; the C3 role-scoped emitter uses this to produce per-role bundles.
|
|
70
|
+
Absence is treated as `["all"]`.
|
|
71
|
+
- `evidence: string` — the "Why / On \<date\> / incident" provenance narrative, separated from
|
|
72
|
+
the imperative rule body; the C2 lean emitter strips this field to reduce context size.
|
|
73
|
+
All three fields are optional; existing behaviour YAML files validate unchanged.
|
|
74
|
+
|
|
75
|
+
### Security
|
|
76
|
+
|
|
77
|
+
- **`mcp` gem upgraded 0.9.2 → 0.25.0** — fixes five CVEs in the MCP ruby-sdk:
|
|
78
|
+
GHSA-52jp-gj8w-j6xh, GHSA-5p9g-j988-pcwv, GHSA-7683-3w9x-ch42, GHSA-h669-8m4g-r2hc,
|
|
79
|
+
GHSA-rjr6-rcgv-9m7m (memory exhaustion, session poisoning, DNS-rebinding; all fixed in 0.23.0).
|
|
80
|
+
Dependency constraint widened from `~> 0.9.2` to `>= 0.23.0`.
|
|
81
|
+
|
|
9
82
|
## [1.5.12] - 2026-07-15
|
|
10
83
|
|
|
11
84
|
### Fixed
|
|
@@ -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-
|
|
8
|
-
"version": "1.
|
|
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,12 +142,46 @@
|
|
|
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": [
|
|
148
158
|
"type"
|
|
149
159
|
],
|
|
150
160
|
"additionalProperties": false
|
|
161
|
+
},
|
|
162
|
+
"enforced": {
|
|
163
|
+
"type": "boolean",
|
|
164
|
+
"description": "True when a live PreToolUse or other hook deterministically enforces this rule. Hook-enforced rules can be omitted from lean context bundles (C2 emitter). Absence is treated as false."
|
|
165
|
+
},
|
|
166
|
+
"applies_to": {
|
|
167
|
+
"type": "array",
|
|
168
|
+
"description": "Session roles this rule applies to. Used by the role-scoped bundle emitter (C3) to include only relevant rules per role. Absence is treated as [\"all\"].",
|
|
169
|
+
"items": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"enum": [
|
|
172
|
+
"worker-impl",
|
|
173
|
+
"worker-research",
|
|
174
|
+
"orchestrator",
|
|
175
|
+
"interactive",
|
|
176
|
+
"all"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"minItems": 1,
|
|
180
|
+
"uniqueItems": true
|
|
181
|
+
},
|
|
182
|
+
"evidence": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"description": "Provenance narrative — the 'Why / On <date> / incident' context separated from the imperative rule body. Stripped from lean bundles (C2 emitter). Absence is treated as no separate evidence (provenance stays inline in description)."
|
|
151
185
|
}
|
|
152
186
|
}
|
|
153
187
|
}
|
|
@@ -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,6 +163,8 @@ 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 ")
|
|
@@ -189,6 +191,10 @@ module RosettAi
|
|
|
189
191
|
# Generates the runtime enforcement logic body.
|
|
190
192
|
# Uses non-interpolating heredoc — this code runs at hook time, not generation time.
|
|
191
193
|
#
|
|
194
|
+
# Per-tool field extraction prevents false positives from raw hash stringification.
|
|
195
|
+
# Each tool has a known input schema; we extract only the fields that carry
|
|
196
|
+
# user-controlled content that sec_018 patterns must match against.
|
|
197
|
+
#
|
|
192
198
|
# @return [String]
|
|
193
199
|
def enforcement_body
|
|
194
200
|
<<~'RUBY'
|
|
@@ -196,10 +202,39 @@ module RosettAi
|
|
|
196
202
|
|
|
197
203
|
input = JSON.parse($stdin.read)
|
|
198
204
|
tool_name = input['tool_name'].to_s
|
|
199
|
-
|
|
205
|
+
|
|
206
|
+
target_path, target_content = case tool_name
|
|
207
|
+
when 'Bash'
|
|
208
|
+
cmd = input.dig('tool_input', 'command').to_s
|
|
209
|
+
[cmd, cmd]
|
|
210
|
+
when 'Write'
|
|
211
|
+
[input.dig('tool_input', 'file_path').to_s,
|
|
212
|
+
input.dig('tool_input', 'content').to_s]
|
|
213
|
+
when 'Edit'
|
|
214
|
+
content = [input.dig('tool_input', 'old_string'),
|
|
215
|
+
input.dig('tool_input', 'new_string')].compact.join(' ')
|
|
216
|
+
[input.dig('tool_input', 'file_path').to_s, content]
|
|
217
|
+
when 'Agent'
|
|
218
|
+
['', input.dig('tool_input', 'prompt').to_s]
|
|
219
|
+
else
|
|
220
|
+
raw = input.fetch('tool_input', '').to_s
|
|
221
|
+
[raw, raw]
|
|
222
|
+
end
|
|
200
223
|
|
|
201
224
|
violations = RULES.select do |rule|
|
|
202
|
-
rule[:applies_to].include?(tool_name)
|
|
225
|
+
next false unless rule[:applies_to].include?(tool_name)
|
|
226
|
+
|
|
227
|
+
if rule[:path_scope] && rule[:match_target] != :path
|
|
228
|
+
next false unless rule[:path_scope].match?(target_path)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
match_text = case rule[:match_target]
|
|
232
|
+
when :path then target_path
|
|
233
|
+
when :content then target_content
|
|
234
|
+
else "#{target_path} #{target_content}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
rule[:pattern].match?(match_text)
|
|
203
238
|
end
|
|
204
239
|
|
|
205
240
|
exit 0 if violations.empty?
|
|
@@ -209,7 +244,6 @@ module RosettAi
|
|
|
209
244
|
"(P#{v[:priority]}, #{v[:action]}): #{v[:message].empty? ? v[:description] : v[:message]}"
|
|
210
245
|
end
|
|
211
246
|
|
|
212
|
-
# Block if any violation has action :block
|
|
213
247
|
blocked = violations.any? { |v| v[:action] == :block }
|
|
214
248
|
exit(blocked ? 2 : 0)
|
|
215
249
|
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
|
|
@@ -72,6 +72,24 @@ module RosettAi
|
|
|
72
72
|
# @return [Array] Parameter types passed as keyword arguments.
|
|
73
73
|
KEYWORD_PARAM_TYPES = [:keyreq, :key].freeze
|
|
74
74
|
|
|
75
|
+
# Lightweight adapter so plain resource-entry hashes satisfy the mcp >= 0.23.0
|
|
76
|
+
# server.resources= API, which requires objects responding to #uri and #to_h.
|
|
77
|
+
class ResourceListEntry
|
|
78
|
+
def initialize(attrs)
|
|
79
|
+
@attrs = attrs
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def uri
|
|
83
|
+
@attrs[:uri]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_h
|
|
87
|
+
{ uri: @attrs[:uri], name: @attrs[:name], description: @attrs[:description],
|
|
88
|
+
mimeType: @attrs[:mimeType] }.compact
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private_constant :ResourceListEntry
|
|
92
|
+
|
|
75
93
|
module_function
|
|
76
94
|
|
|
77
95
|
# @param server [MCP::Server]
|
|
@@ -89,8 +107,9 @@ module RosettAi
|
|
|
89
107
|
# @param server [MCP::Server]
|
|
90
108
|
def register_resources(server)
|
|
91
109
|
resources = RESOURCE_CLASSES.map(&:new)
|
|
92
|
-
server.
|
|
93
|
-
server.resources_read_handler do |
|
|
110
|
+
server.resources = resources.flat_map(&:list).map { |e| ResourceListEntry.new(e) }
|
|
111
|
+
server.resources_read_handler do |params|
|
|
112
|
+
uri = params.is_a?(Hash) ? (params[:uri] || params['uri']) : params.to_s
|
|
94
113
|
resource_name = uri.split('/').last
|
|
95
114
|
resource_class = find_resource_for_uri(resources, uri)
|
|
96
115
|
result = resource_class&.read(resource_name)
|
|
@@ -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
|
|
147
|
-
|
|
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)
|
data/lib/rosett_ai/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hugo Antonio Sepulveda Manriquez
|
|
@@ -237,16 +237,16 @@ dependencies:
|
|
|
237
237
|
name: mcp
|
|
238
238
|
requirement: !ruby/object:Gem::Requirement
|
|
239
239
|
requirements:
|
|
240
|
-
- - "
|
|
240
|
+
- - ">="
|
|
241
241
|
- !ruby/object:Gem::Version
|
|
242
|
-
version: 0.
|
|
242
|
+
version: 0.23.0
|
|
243
243
|
type: :runtime
|
|
244
244
|
prerelease: false
|
|
245
245
|
version_requirements: !ruby/object:Gem::Requirement
|
|
246
246
|
requirements:
|
|
247
|
-
- - "
|
|
247
|
+
- - ">="
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
|
-
version: 0.
|
|
249
|
+
version: 0.23.0
|
|
250
250
|
description: Configuration management tool for AI-assisted development workflows.
|
|
251
251
|
Author rules once, compile to Claude Code, Cursor, AGENTS.md, and more.
|
|
252
252
|
email:
|