samagotchi 0.8.0 → 0.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: 4db526cc3c79a4e5aa8061f124ff09e65e289415074fcb51c71c25ccee1ab0c9
4
- data.tar.gz: c30c2094cb357df9fe4e5618956e0c1a559ea0a2765709605488d03c5febbb87
3
+ metadata.gz: 80a8fec85f3a253ce7f762f3c8881ee0c49d6e73117f205a53cb64482e7a9cc3
4
+ data.tar.gz: 6e21caec0f46b5a2831382a2c77842e5d3935e363b6b6ea96f5d136a24ffa1eb
5
5
  SHA512:
6
- metadata.gz: 2ad7fd71ecfee430c9dec6d9b51971b1e7fdc38c099c5f4a868ad07add50a21ac293f6c686e5f8aca9b0d3bd919ee49e1d79336ac1fe1ac4926d6ace2de24adb
7
- data.tar.gz: 5856a9fe2fcb23b4802626ee918e7015cc718d74b4d17828265377a606cb76fdf80badb3f5d0e314c76bcd3ceb4629fc3f79ab735275fd102c1a2895b54b4f3d
6
+ metadata.gz: ad7fd8882239bb39478cddc39d48e85c2dedfb782d73da193b7dfd401b40599aaf2385462c911d633ebdf78d2f2841d3c400367c1e6feefc300778e617afa425
7
+ data.tar.gz: 2104f11e65bfdf5f9b82a92a2e1281cda2c6a1c6c28a58bd7eedc89e34492f47f34f8aec229aba66d6c875b465ae56f181a0d0a539b039c5ed79a4830c1e4cd0
data/CHANGELOG.md CHANGED
@@ -8,6 +8,25 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.1] - 2026-10-01
12
+
13
+ ### Added
14
+
15
+ - Guardrail rules can name the models they apply to (`models: small`, or globs on the model name). The guardrails
16
+ bundle (0.2.0, `chi update`; needs chi 0.8.1) adds two asks only small models get: `git checkout -- <file>` /
17
+ `git restore` (discards uncommitted changes) and `git stash drop`/`clear`. Small is `guardrails.small_models`:
18
+ `auto` (the default: 32B or less by the size in the model's name, an MoE's active size counting), a list of
19
+ globs, or `[]` for none. A chi older than 0.8.1 that shares the bundles dir (a checkout next to the gem, or a
20
+ worker still running after a gem update) doesn't know `models:` and denies every tool call until it is
21
+ restarted on the new chi.
22
+
23
+ ### Fixed
24
+
25
+ - A tool row's time in the REPL and the attached TUI no longer counts the wait for a guardrail approval
26
+ (`blocked (2m 54s)` for a call denied at once).
27
+ - docs/guardrails.md: rules are read again on the next tool call after config.yml or a bundle's rules change, with
28
+ no restart.
29
+
11
30
  ## [0.8.0] - 2026-10-01
12
31
 
13
32
  ### Added
@@ -462,7 +481,8 @@ and long-lived sessions.
462
481
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
463
482
  hotkey panel that send selected text or the clipboard to your sessions.
464
483
 
465
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.8.0...HEAD
484
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.8.1...HEAD
485
+ [0.8.1]: https://github.com/dm1try/samagotchi/compare/v0.8.0...v0.8.1
466
486
  [0.8.0]: https://github.com/dm1try/samagotchi/compare/v0.7.0...v0.8.0
467
487
  [0.7.0]: https://github.com/dm1try/samagotchi/compare/v0.6.0...v0.7.0
468
488
  [0.6.0]: https://github.com/dm1try/samagotchi/compare/v0.5.1...v0.6.0
@@ -753,6 +753,7 @@ described in their own sections.
753
753
  | `image.max_bytes` | `3750000` | | See "Images". |
754
754
  | `image.max_per_request` | `20` | | See "Images". |
755
755
  | `guardrails.enabled` | `true` | | See [Guardrails](guardrails.md). |
756
+ | `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. See [Guardrails](guardrails.md#rules-for-some-models). |
756
757
  | `log.file` | state dir | yes | See "Debug Log File". |
757
758
  | `log.disable` | `false` | yes | No file logging. |
758
759
  | `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
data/docs/guardrails.md CHANGED
@@ -138,8 +138,40 @@ guardrails:
138
138
  `/guardrails` marks them `disabled (guardrails.disable)` and names entries that
139
139
  match no rule. `disable:` only removes rules; hooks and the core checks still vote.
140
140
 
141
- Rules load when chi starts (a long-running worker picks up changes after its
142
- next start). A rule that doesn't parse (an unknown key, a bad regex, no
141
+ ### Rules for some models
142
+
143
+ `models:` makes a rule vote only for some models: `small`, or a glob on the
144
+ model's name (`Qwen3.6-*`, matched on the bare name without the host prefix and
145
+ on the model key `qwen3-6-27b`, case-insensitively), or a list of them. Without
146
+ `models:` a rule is for every model. With no model name set, a `models:` rule
147
+ doesn't vote.
148
+
149
+ ```yaml
150
+ guardrails:
151
+ small_models: auto # the default; or a list of globs, or [] for none
152
+ rules:
153
+ - id: no-force-push-small
154
+ tool: shell
155
+ command: '\bgit\s+push\b.*--force'
156
+ models: small # or "gemma-*", or [small, "deepseek-*"]
157
+ verdict: deny
158
+ ```
159
+
160
+ Which models are small is `guardrails.small_models`. Nothing reports a model's
161
+ size, so `auto` reads it from the name: `Qwen3.6-27B` is 27B, `gemma-4-E4B-it`
162
+ 4B, `Mixtral-8x7B` 56B, and for an MoE the active size counts
163
+ (`Ornith-1.5-35B-A3B` is 3B, `Qwen3-235B-A22B` 22B). 32B or less is small. A name
164
+ without a size (`deepseek-v4.1-flash`) is not small. A list of globs names the
165
+ small models yourself (`auto` may be one of them), and `[]` means no model is
166
+ small. It is read on every check, so `/model` and config edits apply at once.
167
+
168
+ `/guardrails` names the model and whether it is small (`model: Qwen3.6-27B —
169
+ small (auto, 27B)`), shows each rule's `models`, and marks a rule that doesn't
170
+ vote for the current model `off for this model`.
171
+
172
+ Rules are read again on the next tool call after `config.yml` or an installed
173
+ bundle's rules change, so a long-running worker follows edits without a
174
+ restart. A rule that doesn't parse (an unknown key, a bad regex, no
143
175
  verdict, a `disable:` that isn't a list of ids) makes chi **deny every tool call** and say why, rather than run
144
176
  without it.
145
177
 
@@ -157,6 +189,14 @@ repo; and shell commands that name chi's config, hooks or guardrails or
157
189
  `.git/hooks`. It denies writes into `.git/hooks`. The rules are in
158
190
  `lib/samagotchi/bundles/guardrails/guardrails/rules.yml`.
159
191
 
192
+ Small models (`models: small`, see above) get two more asks, in
193
+ `guardrails/small-models.yml`: `git checkout -- <path>`, `git checkout <rev> --
194
+ <path>`, `git checkout .` and `git restore <path>` (not `--staged` alone), which
195
+ discard uncommitted changes, and `git stash drop`/`clear`. They offer only
196
+ "once" and "this session", so an approval doesn't silence them for good. To get
197
+ them on every model, set `small_models` to `"*"`; to drop them, `[]` (or
198
+ `disable:` the ids).
199
+
160
200
  A bundle ships rules as `guardrails/*.yml` (the same `rules:` shape). Install
161
201
  records each file's sha256; a file changed afterwards, missing, or not parsing
162
202
  denies every call until the bundle is reinstalled.
data/docs/releasing.md CHANGED
@@ -49,7 +49,7 @@ as the GitHub release body.
49
49
  | Task | What it does |
50
50
  | --- | --- |
51
51
  | `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
52
- | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). |
52
+ | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: raise it if the change uses anything new. |
53
53
  | `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
54
54
  | `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG. No commit. |
55
55
  | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, `bundles:check`, rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
@@ -0,0 +1,27 @@
1
+ # Guardrail rules only small models get (models: small): asks that a strong
2
+ # model would find noise. Which models are small is guardrails.small_models:
3
+ # auto (the default) is 32B or less by the size in the model's name, an
4
+ # MoE's active size counting (35B-A3B is small); a list of globs; or [] for
5
+ # none. `/guardrails` says whether the current model is small.
6
+ #
7
+ # These asks offer only once and session: an approval for the repo or the
8
+ # whole rule would silence them for good, on any model.
9
+ # Same fields and git prefix as rules.yml.
10
+ rules:
11
+ # ── losing uncommitted work ────────────────────────────────────────────
12
+ # git checkout -- <path>, git checkout <rev> -- <path>, git checkout .,
13
+ # and git restore of the worktree (not git restore --staged alone).
14
+ - id: git-discard-worktree
15
+ tool: shell
16
+ command: '\bgit(?:\s+(?:-C\s+\S+|-c\s+\S+|--?[A-Za-z][\w-]*(?:=\S+)?))*\s+(?:checkout\b[^;&|\n]*\s(?:--\s+\S|\.(?:\s|$|/))|restore\b(?![^;&|\n]*\s--staged\b)(?=[^;&|\n]*\s[^-\s])|restore\b(?=[^;&|\n]*\s(?:--worktree|-W)\b))'
17
+ models: small
18
+ verdict: ask
19
+ reason: git checkout -- / git restore discards uncommitted changes
20
+ scopes: [once, session]
21
+ - id: git-stash-drop
22
+ tool: shell
23
+ command: '\bgit(?:\s+(?:-C\s+\S+|-c\s+\S+|--?[A-Za-z][\w-]*(?:=\S+)?))*\s+stash\s+(drop|clear)\b'
24
+ models: small
25
+ verdict: ask
26
+ reason: git stash drop/clear deletes stashed work
27
+ scopes: [once, session]
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: guardrails
3
- version: 0.1.1
3
+ version: 0.2.0
4
4
  scope: system
5
- description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, writes outside the repo, git hooks) and a note for the model
5
+ description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, writes outside the repo, git hooks), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
6
6
  trust_level: reviewed
7
+ # models: in small-models.yml (guardrails.small_models)
8
+ requires_chi: ">= 0.8.1"
7
9
  files:
8
10
  guardrails.md: sha256:04207fd1738aeeeef3b7a8053fc57597565bddd7862f01acaba784419cbd909b
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.8.0
3
+ version: 0.8.1
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
@@ -103,6 +103,9 @@ module Samagotchi
103
103
  Entry.new(key: "image.max_per_request", yaml_path: %w[image max_per_request], type: :integer, default: 20, expose: %i[env config]),
104
104
 
105
105
  Entry.new(key: "guardrails.enabled", yaml_path: %w[guardrails enabled], type: :bool, default: true, expose: %i[env config]),
106
+ # Which models get the rules with `models: small` (Guardrails::ModelSize): auto (32B or less by the name's size, an MoE's
107
+ # active size counting), globs on the bare model name or its key, "|"-separated (a YAML list works too), or "" ([]) for none.
108
+ Entry.new(key: "guardrails.small_models", yaml_path: %w[guardrails small_models], type: :string, default: "auto", expose: %i[env config]),
106
109
 
107
110
  Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli]),
108
111
  Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli]),
@@ -126,6 +126,7 @@ module Samagotchi
126
126
  tools: -> { @tools },
127
127
  session: -> { @session },
128
128
  model_key: -> { @model_key },
129
+ model_name: -> { guardrail_model_name },
129
130
  cancelled: -> { !!active_cancel_controller&.cancelled? },
130
131
  ask: ->(fields) { open_question(fields) }
131
132
  )
@@ -428,6 +429,11 @@ module Samagotchi
428
429
  attr_reader :metrics
429
430
  attr_reader :default_model_name, :effective_model_name
430
431
 
432
+ # The effective model's key (ModelOverlay.key_for its bare name): memory
433
+ # overlays, guardrail rules' models:.
434
+ # @return [String, nil]
435
+ attr_reader :model_key
436
+
431
437
  # The prompt profile for the effective model (see #profile_resolution).
432
438
  # @return [ModelProfile]
433
439
  def profile = profile_resolution.profile
@@ -452,6 +458,13 @@ module Samagotchi
452
458
  @host_registry.bare_name(full_ref)
453
459
  end
454
460
 
461
+ # The effective model's bare name for the guardrails' models: rules;
462
+ # nil without a model name.
463
+ def guardrail_model_name
464
+ name = @effective_model_name && bare_model_name(@effective_model_name)
465
+ name.to_s.strip.empty? ? nil : name
466
+ end
467
+
455
468
  # Point the kernel (and a chat backend) at the effective model's host
456
469
  # (after /model, --model, resume).
457
470
  def sync_kernel_client!
@@ -29,14 +29,16 @@ module Samagotchi
29
29
  # @param tools [#call] → Tools::Registry
30
30
  # @param session [#call] → Session, nil
31
31
  # @param model_key [#call] → String, the effective model's overlay key
32
+ # @param model_name [#call] → String, nil: the effective model's bare name
32
33
  # @param cancelled [#call] → Boolean, whether the running turn is cancelled
33
34
  # @param ask [#call] (fields) → the answer; the Engine's question flow
34
- def initialize(scratch:, hooks:, tools:, session:, model_key:, cancelled:, ask:)
35
+ def initialize(scratch:, hooks:, tools:, session:, model_key:, cancelled:, ask:, model_name: -> {})
35
36
  @scratch = scratch
36
37
  @hooks_lookup = hooks
37
38
  @tools_lookup = tools
38
39
  @session_lookup = session
39
40
  @model_key_lookup = model_key
41
+ @model_name_lookup = model_name
40
42
  @cancelled_lookup = cancelled
41
43
  @ask = ask
42
44
  @failures = Guardrails::LoadFailures.new
@@ -78,6 +80,7 @@ module Samagotchi
78
80
  -> { @hooks_lookup.call },
79
81
  context_lookup: -> { context },
80
82
  model_key_lookup: -> { @model_key_lookup.call },
83
+ model_name_lookup: -> { @model_name_lookup.call },
81
84
  approver: ->(verdict) { request_approval(verdict) },
82
85
  approvals_lookup: -> { @approvals },
83
86
  checks_lookup: -> { checks },
@@ -21,7 +21,8 @@ module Samagotchi
21
21
  # the kernel is built.
22
22
  # @param context_lookup [#call] returns the Context for this call
23
23
  # (the Engine's: session, interface, origin, per-turn git cache)
24
- # @param model_key_lookup [#call] the model key (memory overlays)
24
+ # @param model_key_lookup [#call] the model key (memory overlays, rules' models:)
25
+ # @param model_name_lookup [#call] the bare model name (rules' models:)
25
26
  # @param approver [#call, nil] settles an ask (Engine#request_approval);
26
27
  # without one an ask is denied
27
28
  # @param approvals_lookup [#call] returns the Approvals store (or nil)
@@ -31,12 +32,13 @@ module Samagotchi
31
32
  # hook's stop_turn): the rest of a batch is denied without a vote
32
33
  # @param tools_lookup [#call] returns the Tools::Registry (or nil): a
33
34
  # plugin tool's targets: say what its call acts on
34
- def initialize(hooks_lookup, context_lookup: -> { Context.new }, model_key_lookup: -> {}, approver: nil,
35
- approvals_lookup: -> {}, checks_lookup: -> { [] }, cancelled_lookup: -> { false },
35
+ def initialize(hooks_lookup, context_lookup: -> { Context.new }, model_key_lookup: -> {}, model_name_lookup: -> {},
36
+ approver: nil, approvals_lookup: -> {}, checks_lookup: -> { [] }, cancelled_lookup: -> { false },
36
37
  tools_lookup: -> {})
37
38
  @hooks_lookup = hooks_lookup
38
39
  @context_lookup = context_lookup
39
40
  @model_key_lookup = model_key_lookup
41
+ @model_name_lookup = model_name_lookup
40
42
  @approver = approver
41
43
  @approvals_lookup = approvals_lookup
42
44
  @checks_lookup = checks_lookup
@@ -113,7 +115,8 @@ module Samagotchi
113
115
  end
114
116
 
115
117
  def targets_for(call, context)
116
- Targets.for(call, context, model_key: @model_key_lookup.call, registry: @tools_lookup.call)
118
+ Targets.for(call, context, model_key: @model_key_lookup.call, model_name: @model_name_lookup.call,
119
+ registry: @tools_lookup.call)
117
120
  end
118
121
 
119
122
  # Registry#fire_each rescues a raising hook itself.
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../config"
4
+
5
+ module Samagotchi
6
+ module Guardrails
7
+ # Whether the effective model is a small one, for rules with
8
+ # `models: small` (guardrails.small_models). Nothing reports a model's
9
+ # size (llama.cpp's /props has no parameter count), so `auto` reads it
10
+ # from the bare model name: Qwen3.6-27B → 27, gemma-4-E4B-it → 4,
11
+ # Ornith-1.5-35B-A3B → 3 (an MoE's active size counts), Mixtral-8x7B
12
+ # → 56. A name without a size, and no model at all, is not small (fail
13
+ # open: the rules for small models are extra asks, not the defaults).
14
+ module ModelSize
15
+ SETTING = "guardrails.small_models"
16
+ # The largest auto-small size, in billions of parameters.
17
+ MAX_SMALL = 32
18
+ GLOB_FLAGS = File::FNM_CASEFOLD | File::FNM_EXTGLOB
19
+
20
+ # A size starts at the name's start or after a separator, and ends at
21
+ # a separator or the name's end (so 4bit and v4.1 aren't sizes).
22
+ NUM = '(\d+(?:\.\d+)?)'
23
+ BEFORE = '(?:\A|[-_/:\s])'
24
+ AFTER = '(?=[-_:\s]|\z)'
25
+ ACTIVE = /#{BEFORE}a#{NUM}b#{AFTER}/i
26
+ EXPERTS = /#{BEFORE}(\d+)x#{NUM}b#{AFTER}/i
27
+ PLAIN = /#{BEFORE}e?#{NUM}b#{AFTER}/i
28
+
29
+ module_function
30
+
31
+ # The size the bare model name gives, in billions; nil without one.
32
+ # @return [Numeric, nil]
33
+ def billions(name)
34
+ name = name.to_s
35
+ if (m = name.match(ACTIVE))
36
+ number(m[1])
37
+ elsif (m = name.match(EXPERTS))
38
+ m[1].to_i * number(m[2])
39
+ elsif (m = name.match(PLAIN))
40
+ number(m[1])
41
+ end
42
+ end
43
+
44
+ def number(text)
45
+ text.include?(".") ? text.to_f : text.to_i
46
+ end
47
+
48
+ # @param name [String, nil] the bare model name (no host prefix)
49
+ # @param key [String, nil] its model key (ModelOverlay.key_for)
50
+ # @param setting [String, nil] guardrails.small_models: "auto", globs
51
+ # on the name or the key, "|"-separated ("auto" may be one of them),
52
+ # or "" (never)
53
+ def small?(name, key, setting = self.setting)
54
+ return false if name.nil? || name.to_s.strip.empty?
55
+
56
+ entries = (setting.nil? ? "auto" : setting.to_s).split("|").map(&:strip).reject(&:empty?)
57
+ entries.any? do |entry|
58
+ if entry.casecmp?("auto")
59
+ (size = billions(name)) ? size <= MAX_SMALL : false
60
+ else
61
+ [name, key].compact.any? { |candidate| File.fnmatch(entry, candidate.to_s, GLOB_FLAGS) }
62
+ end
63
+ end
64
+ end
65
+
66
+ # How /guardrails says it: "small (auto, 27B)", "not small (auto, no
67
+ # size in the name)", "small (small_models: qwen*)".
68
+ def describe(name, key, setting = self.setting)
69
+ return "not small (no model name)" if name.nil? || name.to_s.strip.empty?
70
+
71
+ verdict = small?(name, key, setting) ? "small" : "not small"
72
+ return "#{verdict} (small_models: #{setting.empty? ? "[]" : setting})" unless setting.nil? || setting.strip.casecmp?("auto")
73
+
74
+ size = billions(name)
75
+ "#{verdict} (auto, #{size ? "#{size}B" : "no size in the name"})"
76
+ end
77
+
78
+ # The setting, live (config.yml is re-read on change); auto when it
79
+ # can't be read.
80
+ def setting
81
+ Config.get(SETTING)
82
+ rescue StandardError
83
+ "auto"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "verdict"
4
4
  require_relative "targets"
5
+ require_relative "model_size"
5
6
 
6
7
  module Samagotchi
7
8
  module Guardrails
@@ -13,6 +14,10 @@ module Samagotchi
13
14
  # path: "outside_repo", or a glob on the resolved paths (absolute
14
15
  # or "**/…" globs match the absolute path; others the path
15
16
  # relative to the repo root, the cwd outside one)
17
+ # models: "small" (Guardrails::ModelSize, guardrails.small_models),
18
+ # or a glob on the bare model name or the model key, or a
19
+ # list of them: the rule only votes for a model that matches
20
+ # (no model matches none; missing = every model)
16
21
  # verdict ask|deny, reason, scopes (for an ask; default all).
17
22
  # `guardrails.disable` switches single rules off (#parse_disable).
18
23
  # A rule that doesn't parse raises ParseError: the Engine then denies
@@ -21,13 +26,14 @@ module Samagotchi
21
26
  class Rules
22
27
  class ParseError < StandardError; end
23
28
 
24
- KEYS = %w[id tool command path verdict reason scopes].freeze
29
+ KEYS = %w[id tool command path models verdict reason scopes].freeze
25
30
  VERDICTS = %w[ask deny].freeze
26
31
  GLOB_FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
27
32
 
28
- Rule = Struct.new(:id, :tools, :command, :path, :verdict, :reason, :scopes, :source, keyword_init: true) do
33
+ Rule = Struct.new(:id, :tools, :command, :path, :models, :verdict, :reason, :scopes, :source, keyword_init: true) do
29
34
  def matches?(targets)
30
35
  return false unless targets
36
+ return false unless for_model?(targets.model_name, targets.model_key, -> { targets.small_model? })
31
37
  return false if tools && !tool_matches?(targets.tool)
32
38
  return false if command && !(targets.command && command.match?(targets.command))
33
39
  return false if path && !path_matches?(targets)
@@ -39,6 +45,21 @@ module Samagotchi
39
45
  tools.any? { |tool| Rules.glob?(tool) ? File.fnmatch(tool, name.to_s, File::FNM_EXTGLOB) : tool == name }
40
46
  end
41
47
 
48
+ # Whether the rule votes for this model (its models:; without one,
49
+ # for every model). +small+ says whether the model is a small one.
50
+ def for_model?(name, key, small = -> { ModelSize.small?(name, key) })
51
+ return true unless models
52
+ return false if name.nil? || name.empty?
53
+
54
+ models.any? do |entry|
55
+ if entry == "small"
56
+ small.call
57
+ else
58
+ [name, key].compact.any? { |candidate| File.fnmatch(entry, candidate, ModelSize::GLOB_FLAGS) }
59
+ end
60
+ end
61
+ end
62
+
42
63
  def path_matches?(targets)
43
64
  return targets.outside_repo? if path == "outside_repo"
44
65
 
@@ -83,7 +104,7 @@ module Samagotchi
83
104
  end
84
105
 
85
106
  Rule.new(id: id, tools: tools_of(raw["tool"], label), command: regex_of(raw["command"], label),
86
- path: path_of(raw["path"], label), verdict: verdict.to_sym,
107
+ path: path_of(raw["path"], label), models: models_of(raw["models"], label), verdict: verdict.to_sym,
87
108
  reason: (raw["reason"] || "rule #{id}").to_s, scopes: scopes_of(raw["scopes"], label), source: source)
88
109
  end
89
110
 
@@ -114,6 +135,17 @@ module Samagotchi
114
135
  value
115
136
  end
116
137
 
138
+ def self.models_of(value, label)
139
+ return nil if value.nil?
140
+
141
+ names = Array(value)
142
+ unless !names.empty? && names.all? { |n| n.is_a?(String) && !n.strip.empty? }
143
+ raise ParseError, "#{label}: models must be small, a glob or a list of them"
144
+ end
145
+
146
+ names.map(&:strip)
147
+ end
148
+
117
149
  def self.scopes_of(value, label)
118
150
  return nil if value.nil?
119
151
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../tools/tool_path"
4
4
  require_relative "../tools/memory"
5
5
  require_relative "../log"
6
+ require_relative "model_size"
6
7
 
7
8
  module Samagotchi
8
9
  module Guardrails
@@ -16,6 +17,10 @@ module Samagotchi
16
17
 
17
18
  attr_reader :tool, :command, :paths, :cwd, :repo_root
18
19
 
20
+ # @return [String, nil] the effective model: its bare name (no host
21
+ # prefix) and its key (ModelOverlay.key_for), for a rule's models:
22
+ attr_reader :model_name, :model_key
23
+
19
24
  # @return [Hash, nil] a plugin tool's arguments (call[:args]), for an
20
25
  # approval of a call whose targets name no command or path; nil for
21
26
  # chi's own tools
@@ -23,10 +28,12 @@ module Samagotchi
23
28
 
24
29
  # @param call [Hash] the parsed tool call
25
30
  # @param context [Context]
26
- # @param model_key [String, nil] for a memory_write model overlay
31
+ # @param model_key [String, nil] for a memory_write model overlay, and
32
+ # a rule's models:
33
+ # @param model_name [String, nil] the bare model name, for a rule's models:
27
34
  # @param registry [Tools::Registry, nil] the session's tools: a plugin
28
35
  # tool's entry says what it acts on (its targets:)
29
- def self.for(call, context, model_key: nil, registry: nil)
36
+ def self.for(call, context, model_key: nil, model_name: nil, registry: nil)
30
37
  tool = call[:name].to_s
31
38
  base = context.cwd
32
39
  command = nil
@@ -53,7 +60,8 @@ module Samagotchi
53
60
  entry = registry && registry[tool]
54
61
  args = call[:args] if entry && !entry.core? && call[:args].is_a?(Hash)
55
62
  end
56
- new(tool: tool, command: command, paths: paths.compact, cwd: cwd, repo_root: context.repo_root(cwd), args: args)
63
+ new(tool: tool, command: command, paths: paths.compact, cwd: cwd, repo_root: context.repo_root(cwd), args: args,
64
+ model_name: model_name, model_key: model_key)
57
65
  end
58
66
 
59
67
  # What a plugin tool's targets: callable says the call acts on:
@@ -93,8 +101,10 @@ module Samagotchi
93
101
  nil
94
102
  end
95
103
 
96
- def initialize(tool:, command:, paths:, cwd:, repo_root:, args: nil)
104
+ def initialize(tool:, command:, paths:, cwd:, repo_root:, args: nil, model_name: nil, model_key: nil)
97
105
  @args = args
106
+ @model_name = model_name
107
+ @model_key = model_key
98
108
  @tool = tool
99
109
  @command = command
100
110
  @paths = paths
@@ -104,6 +114,14 @@ module Samagotchi
104
114
 
105
115
  def shell? = SHELL_TOOLS.include?(@tool)
106
116
 
117
+ # Whether the effective model is a small one (guardrails.small_models,
118
+ # read when first asked: once per call's targets).
119
+ def small_model?
120
+ return @small_model if defined?(@small_model)
121
+
122
+ @small_model = ModelSize.small?(@model_name, @model_key)
123
+ end
124
+
107
125
  # Whether any path is outside the repo root (the cwd outside a repo).
108
126
  def outside_repo?
109
127
  root = @repo_root || @cwd
@@ -3,6 +3,7 @@
3
3
  require_relative "guardrails/verdict"
4
4
  require_relative "guardrails/context"
5
5
  require_relative "guardrails/targets"
6
+ require_relative "guardrails/model_size"
6
7
  require_relative "guardrails/approval"
7
8
  require_relative "guardrails/approvals"
8
9
  require_relative "guardrails/protected_paths"
@@ -234,6 +234,9 @@ module Samagotchi
234
234
  rules = @engine.guardrail_rules
235
235
  approvals = @engine.guardrail_approvals.entries
236
236
  lines = ["guardrails: #{rules.enabled? ? "on" : "off (guardrails.enabled: false; denies still apply)"}"]
237
+ name = @engine.guardrail_model_name
238
+ key = @engine.model_key
239
+ lines << "model: #{name || "none"} — #{Guardrails::ModelSize.describe(name, key)}"
237
240
  failures = @engine.guardrail_failures.list
238
241
  unless failures.empty?
239
242
  lines << "failed to load:"
@@ -248,9 +251,11 @@ module Samagotchi
248
251
  match = [
249
252
  ("tool #{rule.tools.join(",")}" if rule.tools),
250
253
  ("command /#{shorten_pattern(rule.command.source)}/" if rule.command),
251
- ("path #{rule.path}" if rule.path)
254
+ ("path #{rule.path}" if rule.path),
255
+ ("models #{rule.models.join(",")}" if rule.models)
252
256
  ].compact.join(", ")
253
257
  off = "disabled (guardrails.disable) — " if rules.disabled?(rule)
258
+ off ||= "off for this model — " unless rule.for_model?(name, key)
254
259
  lines << " #{idx + 1}. #{rule.id}: #{off}#{rule.verdict} (#{match}) — #{rule.reason} [#{rule.source}]"
255
260
  end
256
261
  rules.unmatched_disables.each { |entry| lines << " guardrails.disable: #{entry} matches no rule" }
@@ -209,9 +209,11 @@ private
209
209
  [event[:iteration].to_i, event[:call_index].to_i, event[:tool].to_s]
210
210
  end
211
211
 
212
+ # From tool_call_started, less an approval wait (waited_ms): an asked
213
+ # row counts from the answer.
212
214
  def tool_duration_ms(event)
213
215
  started_at = @tool_started_at.delete(tool_call_key(event))
214
- started_at && ((@clock.call - started_at) * 1000.0)
216
+ started_at && [((@clock.call - started_at) * 1000.0) - event[:waited_ms].to_f, 0.0].max
215
217
  end
216
218
  end
217
219
  end
@@ -25,8 +25,10 @@ module Samagotchi
25
25
 
26
26
  # @param kernel [KernelLoop] read lazily: Engine sets its hooks after
27
27
  # the kernel is built.
28
- def initialize(kernel)
28
+ # @param clock [#call] monotonic seconds, for the approval wait
29
+ def initialize(kernel, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
29
30
  @kernel = kernel
31
+ @clock = clock
30
32
  end
31
33
 
32
34
  # @param call_index [Integer] 1-based position of the call in its batch
@@ -54,8 +56,14 @@ module Samagotchi
54
56
  emit(on_stream_event, started)
55
57
 
56
58
  # The ask comes after tool_call_started: the UI shows the tool line,
57
- # then the approval under it.
58
- settle_ask(verdict) if verdict.ask?
59
+ # then the approval under it. waited_ms on tool_call_completed lets a
60
+ # UI that times the row from tool_call_started leave the wait out.
61
+ waited_ms = nil
62
+ if verdict.ask?
63
+ asked_at = @clock.call
64
+ settle_ask(verdict)
65
+ waited_ms = ((@clock.call - asked_at) * 1000).round
66
+ end
59
67
  before = verdict.deny? ? NOT_AN_EDIT : file_before(call)
60
68
  result = verdict.deny? ? denied(call, verdict) : dispatch(call)
61
69
  diff = file_change(call, before)
@@ -75,6 +83,7 @@ module Samagotchi
75
83
  tool: call[:name], output: capped, output_truncated: truncated, activity: result[:activity] }
76
84
  completed[:images] = images if images&.any?
77
85
  completed[:diff] = diff if diff
86
+ completed[:waited_ms] = waited_ms if waited_ms
78
87
  emit(on_stream_event, completed)
79
88
 
80
89
  run = { output: output, capped_output: capped, truncated: truncated, activity: result[:activity] }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -166,6 +166,7 @@ files:
166
166
  - lib/samagotchi/bundles/dev/manifest.yml
167
167
  - lib/samagotchi/bundles/guardrails/guardrails.md
168
168
  - lib/samagotchi/bundles/guardrails/guardrails/rules.yml
169
+ - lib/samagotchi/bundles/guardrails/guardrails/small-models.yml
169
170
  - lib/samagotchi/bundles/guardrails/manifest.yml
170
171
  - lib/samagotchi/bundles/known-names/hooks/known_names.rb
171
172
  - lib/samagotchi/bundles/known-names/known_names.md
@@ -217,6 +218,7 @@ files:
217
218
  - lib/samagotchi/guardrails/context.rb
218
219
  - lib/samagotchi/guardrails/gate.rb
219
220
  - lib/samagotchi/guardrails/load_failures.rb
221
+ - lib/samagotchi/guardrails/model_size.rb
220
222
  - lib/samagotchi/guardrails/protected_paths.rb
221
223
  - lib/samagotchi/guardrails/rules.rb
222
224
  - lib/samagotchi/guardrails/scratch_writes.rb