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 +4 -4
- data/CHANGELOG.md +21 -1
- data/docs/configuration.md +1 -0
- data/docs/guardrails.md +42 -2
- data/docs/releasing.md +1 -1
- data/lib/samagotchi/bundles/guardrails/guardrails/small-models.yml +27 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +4 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/engine.rb +13 -0
- data/lib/samagotchi/guardrail_wiring.rb +4 -1
- data/lib/samagotchi/guardrails/gate.rb +7 -4
- data/lib/samagotchi/guardrails/model_size.rb +87 -0
- data/lib/samagotchi/guardrails/rules.rb +35 -3
- data/lib/samagotchi/guardrails/targets.rb +22 -4
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/session_commands.rb +6 -1
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/tool_runner.rb +12 -3
- data/lib/samagotchi/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80a8fec85f3a253ce7f762f3c8881ee0c49d6e73117f205a53cb64482e7a9cc3
|
|
4
|
+
data.tar.gz: 6e21caec0f46b5a2831382a2c77842e5d3935e363b6b6ea96f5d136a24ffa1eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
142
|
-
|
|
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.
|
|
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
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -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]),
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -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: -> {},
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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] }
|
data/lib/samagotchi/version.rb
CHANGED
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.
|
|
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
|