samagotchi 0.115.0 → 0.116.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 +53 -1
- data/bin/chi +2 -0
- data/docs/cli.md +5 -1
- data/docs/configuration.md +7 -0
- data/docs/guardrails.md +188 -1
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -0
- data/lib/samagotchi/bridge.rb +26 -0
- data/lib/samagotchi/bridge_client.rb +7 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +15 -0
- data/lib/samagotchi/engine.rb +6 -1
- data/lib/samagotchi/guardrail_wiring.rb +166 -2
- data/lib/samagotchi/guardrails/approval.rb +20 -0
- data/lib/samagotchi/guardrails/targets.rb +23 -2
- data/lib/samagotchi/guardrails/verdict.rb +23 -3
- data/lib/samagotchi/guardrails_command.rb +329 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +79 -2
- data/lib/samagotchi/session_metrics.rb +7 -1
- data/lib/samagotchi/system_one/answer.rb +78 -0
- data/lib/samagotchi/system_one/approval_question.rb +113 -0
- data/lib/samagotchi/system_one/client.rb +89 -0
- data/lib/samagotchi/system_one/errors.rb +14 -0
- data/lib/samagotchi/system_one/fake_backend.rb +45 -0
- data/lib/samagotchi/system_one/http_backend.rb +129 -0
- data/lib/samagotchi/system_one/log.rb +133 -0
- data/lib/samagotchi/system_one/question.rb +86 -0
- data/lib/samagotchi/system_one/record.rb +83 -0
- data/lib/samagotchi/system_one/self_protect.rb +90 -0
- data/lib/samagotchi/system_one/similar.rb +69 -0
- data/lib/samagotchi/system_one.rb +25 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +15 -4
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +14 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -0
- data/lib/samagotchi/web/public/activity.js +10 -0
- data/lib/samagotchi/web/public/app.js +3 -0
- data/lib/samagotchi/web/public/apple-touch-icon.png +0 -0
- data/lib/samagotchi/web/public/composer_fold.js +8 -8
- data/lib/samagotchi/web/public/composer_grip.js +65 -22
- data/lib/samagotchi/web/public/composer_size.js +30 -2
- data/lib/samagotchi/web/public/data.js +10 -0
- data/lib/samagotchi/web/public/favicon.svg +1 -1
- data/lib/samagotchi/web/public/icons/chi-180.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-192.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-512.png +0 -0
- data/lib/samagotchi/web/public/index.html +5 -0
- data/lib/samagotchi/web/public/system_one_wrong.js +32 -0
- data/lib/samagotchi/web/public/timing.js +9 -5
- data/lib/samagotchi/web/public/turn_events.js +1 -0
- data/lib/samagotchi/web/public/turn_html.js +13 -2
- data/lib/samagotchi/web/session_control.rb +22 -0
- data/lib/samagotchi/web/session_show.rb +35 -5
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e86ef8d1890939908a44f84be426b6132992e9483af840e9438ad2d59829da9
|
|
4
|
+
data.tar.gz: adce5380c4f88f152c7aab62c26176d38b6ee6f1e268abe24b7d4b8850c1bb4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c794e9f67ee56a67ec85fa0ec801919d3d0917b62b477491b12ca6475d0678402833b1191d973f791edfb7345c0cbee16509ecb8dcf3be1a5345ead097dfeb0a
|
|
7
|
+
data.tar.gz: 7d3da8e7edf45917785b14bf521eaee4559a829c631570a0336a4fcce00953b62a46a7e8f28ae4ffcb8130bae6b42b944e0783b8dbde35f918489a4504937ee8
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,57 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.116.0] - 2026-10-25
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- System 1's judgment now sees more of the ask it is given: its state carries up to five of the user's **earlier
|
|
16
|
+
answered asks** that look like this one (`SystemOne::Similar` — same rule and repository first, then the same rule,
|
|
17
|
+
then the ask whose logged call shares the most words with this one, ties newest first), and a `memory_write` ask
|
|
18
|
+
names the entry and its scope instead of only a path, with that entry's index description for a removal — the
|
|
19
|
+
`memory-remove` ask was scoring flat because the judge could not tell which memory was going. The judgments log is
|
|
20
|
+
read once per ask, shared by the examples and the record gate. See
|
|
21
|
+
[Guardrails](docs/guardrails.md#system-1-experimental).
|
|
22
|
+
- `/guardrails wrong [N]` appends a `should_have_asked` label to this session's Nth newest unlabelled
|
|
23
|
+
System 1 auto-allow (default 1), so its rule asks again until its record is rebuilt.
|
|
24
|
+
- `/guardrails` shows this session's recent System 1 auto-allows and each rule's record summary when System 1
|
|
25
|
+
is enabled or has judgments.
|
|
26
|
+
- chi web's owner can flag an auto-allowed tool row as **should have asked**; the worker appends the label only
|
|
27
|
+
for a judgment belonging to that session, and participants cannot reach the route; a flagged row stays marked
|
|
28
|
+
after a reload.
|
|
29
|
+
- System 1's auto-allowed calls are marked on their tool row in the terminal (` · auto-allowed (System 1, p=0.97)`) and
|
|
30
|
+
the web (an `auto-allowed · System 1` label, its rule and p on hover).
|
|
31
|
+
- **System 1 (experimental)**: `Samagotchi::SystemOne` talks the TypeSafe judgment API (`POST <host>/v1/systemone`) for
|
|
32
|
+
the questions a guardrail ask would ask, with the new `guardrails.system_one.*` settings (`mode`, `host`, `model`,
|
|
33
|
+
`rules`, `threshold`, `min_record`, `timeout_ms`) and `script/system-one-probe HOST [MODEL]`, which prints each
|
|
34
|
+
question's probability and latency. In `audit` mode every approval a user is about to see is now judged
|
|
35
|
+
first and written, with the answer they gave, to `guardrails/judgments.jsonl` beside the approvals store. See
|
|
36
|
+
[Guardrails](docs/guardrails.md#system-1-experimental).
|
|
37
|
+
- `chi guardrails replay [--dry-run] [--threshold T] [--model M]` runs System 1 over the approvals this machine's
|
|
38
|
+
sessions already answered — read from their `cards.json` sidecars — and writes each judgment and the user's answer
|
|
39
|
+
to `guardrails/judgments.jsonl`, so a rule's record starts with the asks the user really decided on. It prints a
|
|
40
|
+
per-rule table (asks, allowed, denied, auto at the threshold, false allows, no judgment) and skips unanswered
|
|
41
|
+
asks, relay cards and question ids a previous replay logged; a parent agent's answer is not the user's, so only
|
|
42
|
+
its deny is replayed, and only where the parent could have allowed. Exits 2 when `guardrails.system_one.mode`
|
|
43
|
+
is off or no host is set. See [Guardrails](docs/guardrails.md#seeding-the-record-chi-guardrails-replay).
|
|
44
|
+
- **System 1 `on` mode answers for the user**: an ask is allowed once, without a question, when its rule is in
|
|
45
|
+
`guardrails.system_one.rules`, the judgment's probability is at `guardrails.system_one.threshold` (0.8 now, was
|
|
46
|
+
0.95), and that rule's record is clean — `guardrails.system_one.min_record` (5) asks at the threshold the user
|
|
47
|
+
allowed, and no false allow among its last 20 labelled asks. Calls that write chi's own config.yml, hooks,
|
|
48
|
+
installed bundles or the guardrails store are never auto-allowed, nor `chi-answer-piped`, `chi-answer-http` or
|
|
49
|
+
`chi-broadcast`; memory files are fair game. Nothing is stored in `approvals.json`, no card is opened and no
|
|
50
|
+
notification is sent, so the auto-allowed tool row is its only trace. See
|
|
51
|
+
[Guardrails](docs/guardrails.md#answering-for-the-user-on-mode).
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- chi web's composer grip hides the message box with a click as well as a drag: a click on the expanded grip folds it
|
|
56
|
+
250 ms after the press, so a second click inside that window is a double-click and resets the height instead (a
|
|
57
|
+
drag, Enter or Space still act at once). A fold no longer forgets the height the grip set (`chi_composer_floor`):
|
|
58
|
+
the box opens again at the height it had, and only a double-click forgets it. See [Sessions](docs/sessions.md).
|
|
59
|
+
- chi's icon (the browser tab and the Home Screen app) is a deeper blue to purple, so the white χ stands out on any
|
|
60
|
+
wallpaper. On a phone, remove the Home Screen app and add it again to see it: iOS keeps the old icon.
|
|
61
|
+
|
|
11
62
|
## [0.115.0] - 2026-10-25
|
|
12
63
|
|
|
13
64
|
### Changed
|
|
@@ -4354,7 +4405,8 @@ and long-lived sessions.
|
|
|
4354
4405
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4355
4406
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4356
4407
|
|
|
4357
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4408
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.116.0...HEAD
|
|
4409
|
+
[0.116.0]: https://github.com/dm1try/samagotchi/compare/v0.115.0...v0.116.0
|
|
4358
4410
|
[0.115.0]: https://github.com/dm1try/samagotchi/compare/v0.114.0...v0.115.0
|
|
4359
4411
|
[0.114.0]: https://github.com/dm1try/samagotchi/compare/v0.113.0...v0.114.0
|
|
4360
4412
|
[0.113.0]: https://github.com/dm1try/samagotchi/compare/v0.112.0...v0.113.0
|
data/bin/chi
CHANGED
|
@@ -68,6 +68,8 @@ SUBCOMMANDS = {
|
|
|
68
68
|
"memory" => ["samagotchi/memory_command", "MemoryCommand"],
|
|
69
69
|
# install, upgrade, uninstall, status, diff, list, build (--scope, --force, …)
|
|
70
70
|
"bundle" => ["samagotchi/bundle_command", "BundleCommand"],
|
|
71
|
+
# the System 1 record: replay the asks this machine's sessions already answered
|
|
72
|
+
"guardrails" => ["samagotchi/guardrails_command", "GuardrailsCommand"],
|
|
71
73
|
# other machines' chi web, for chi --remote NAME (add, ls, rm)
|
|
72
74
|
"remote" => ["samagotchi/remote_command", "RemoteCommand"]
|
|
73
75
|
}.freeze
|
data/docs/cli.md
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
- `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
|
|
37
37
|
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
|
|
38
38
|
|
|
39
|
+
- `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
|
|
40
|
+
|
|
39
41
|
- `chi bundle try PATH -p TEXT [options]` — install a bundle in a temporary, filtered sandbox, run one real model turn,
|
|
40
42
|
and print its trace (see [Trying a bundle](#trying-a-bundle)).
|
|
41
43
|
|
|
@@ -1428,7 +1430,9 @@ In interactive assist mode, you can switch the request model without restarting:
|
|
|
1428
1430
|
- `/model <name> --alias <alias>`: create alias for current effective model (alias value may be bare or `host:model`).
|
|
1429
1431
|
- `/model`: show the effective model (and default when diverged: `runtime model: <effective> (default: <default>, profile=<name>, <source>)`, e.g. `profile=qwen36, server (chat_template)`). When the session's prompt carries model notes the line ends with `; notes: model_notes_deepseek (system, 612 chars)`; a switch's `runtime model set to …` names the new model's ([Memory](memory.md#model-notes)).
|
|
1430
1432
|
- `/model clear` (or `default`/`none`/`off`): clear the session override, reverting to the configured default.
|
|
1431
|
-
- `/guardrails`: the guardrail rules (by source), what failed to load,
|
|
1433
|
+
- `/guardrails`: the guardrail rules (by source), what failed to load, your stored approvals, and System 1's
|
|
1434
|
+
recent auto-allows; `/guardrails revoke N` removes approval N and `/guardrails wrong [N]` flags an auto-allow
|
|
1435
|
+
this session should have been asked about (see [Guardrails](guardrails.md)).
|
|
1432
1436
|
- `/models`: list model ids aggregated across all `hosts:` (an alias shows next to its id: a bare alias on every host listing the id, a `host:model` one only under its host; grouped `host (host:port):` with per-host `unreachable` warnings, e.g. an unset `api_key_env`; lists cached 60s, 10 minutes for a remote host; lazy — no startup prefill). Ids the host declares under `hosts.<name>.models` come first, as `rr/x (config)`, and don't count toward the cap; an unreachable host keeps them, under its warning, as `rr/x (config, host down)`. At most 20 ids per host, then `… and N more`; `/models <text>` lists every id containing `<text>` (any case), e.g. `/models qwen` on OpenRouter.
|
|
1433
1437
|
|
|
1434
1438
|
Notes:
|
data/docs/configuration.md
CHANGED
|
@@ -1886,6 +1886,13 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
1886
1886
|
| `guardrails.mode` | `auto` | | Which rules vote: `auto` leaves out the rules tagged `modes: [strict]` (in the guardrails bundle: `git rebase`, writes and git outside the session's repo); `strict` runs them all. An unknown value warns and is `auto`. config.yml only. See [Guardrails](guardrails.md#modes). |
|
|
1887
1887
|
| `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 `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
|
|
1888
1888
|
| `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. A session's own `parent_answers` (`chi --parent-answers`, `/parent-answers`) may only tighten it. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
|
|
1889
|
+
| `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1). |
|
|
1890
|
+
| `guardrails.system_one.host` | none | | The `hosts:` entry serving `/v1/systemone` (a local Splash, or the hosted Jev API); unset: no judgment. config.yml only. |
|
|
1891
|
+
| `guardrails.system_one.model` | none | | The judgment model; unset uses the host's first model, and a host that names none has no judgment. config.yml only. |
|
|
1892
|
+
| `guardrails.system_one.rules` | none | | The rule ids System 1 may answer for in `on` mode: `"git-push|git-rebase"` or a YAML list, parsed like `guardrails.small_models`. Nothing is auto-allowed while it is unset; the `chi-answer-piped`, `chi-answer-http` and `chi-broadcast` rules never are, however they are listed. config.yml only. |
|
|
1893
|
+
| `guardrails.system_one.threshold` | `0.8` | | The probability an auto-allow needs. config.yml only. |
|
|
1894
|
+
| `guardrails.system_one.min_record` | `5` | | How many clean records a rule needs before System 1 is trusted for it: labelled asks of that rule at the threshold the user allowed. config.yml only. |
|
|
1895
|
+
| `guardrails.system_one.timeout_ms` | `2000` | | Milliseconds one judgment may take; a slower one is no judgment and the ask goes to the user. config.yml only. |
|
|
1889
1896
|
| `log.file` | state dir | yes | See "Debug Log File". |
|
|
1890
1897
|
| `log.disable` | `false` | yes | No file logging. |
|
|
1891
1898
|
| `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
|
data/docs/guardrails.md
CHANGED
|
@@ -97,7 +97,12 @@ matches its exact folder.
|
|
|
97
97
|
A stored approval only relaxes an ask; a deny rule is never approvable.
|
|
98
98
|
A file that doesn't parse is moved aside to `approvals.json.corrupt-<UTC time>`
|
|
99
99
|
with one warning, and chi starts with no stored approvals (more asks, nothing lost).
|
|
100
|
-
`/guardrails` lists the rules and approvals; `/guardrails revoke N` removes
|
|
100
|
+
`/guardrails` lists the rules and approvals; `/guardrails revoke N` removes
|
|
101
|
+
approval N. When System 1 is enabled or has judgments, it also shows this
|
|
102
|
+
session's recent auto-allows and each rule's record summary. In System 1 `on`
|
|
103
|
+
mode, `/guardrails wrong [N]` flags the Nth newest auto-allowed call in this
|
|
104
|
+
session as one System 1 should have asked about (default 1). The flag is
|
|
105
|
+
appended to the judgments log; the rule asks again until its record is rebuilt.
|
|
101
106
|
The file tools can't write the store.
|
|
102
107
|
|
|
103
108
|
### Answers from a parent agent
|
|
@@ -613,6 +618,188 @@ A rule in config.yml that uses a key this chi doesn't know yet (`modes:`,
|
|
|
613
618
|
that doesn't parse denies every tool call (see Failing closed). Bundles say
|
|
614
619
|
which chi they need (`requires_chi`), so their rules don't hit this.
|
|
615
620
|
|
|
621
|
+
## System 1 (experimental)
|
|
622
|
+
|
|
623
|
+
System 1 is a cheap classifier that judges an approval — "would the user
|
|
624
|
+
allow this?" — before it reaches them, so the asks they always allow stop
|
|
625
|
+
reaching them. With `mode: audit` every ask a person is about to see is
|
|
626
|
+
judged and logged and **nothing about the ask changes**: the user is asked
|
|
627
|
+
exactly as they would have been, and their answer is what settles the call.
|
|
628
|
+
With `mode: on` an ask that passes every gate is allowed **once** without
|
|
629
|
+
reaching them — see [Answering for the user](#answering-for-the-user-on-mode).
|
|
630
|
+
|
|
631
|
+
`Samagotchi::SystemOne` (lib/samagotchi/system_one/) speaks the TypeSafe
|
|
632
|
+
"System One" judgment API: `POST <host>/v1/systemone` with
|
|
633
|
+
`{model, state, questions}` and answers of type `noul` (a probability),
|
|
634
|
+
`choice` or `score`. The host is a `hosts:` entry with `api: openai` — a
|
|
635
|
+
local Splash server, or the hosted Jev API — and its key is that host's own
|
|
636
|
+
`api_key_env:`.
|
|
637
|
+
|
|
638
|
+
```yaml
|
|
639
|
+
guardrails:
|
|
640
|
+
system_one:
|
|
641
|
+
mode: off # off (default) | audit (judge, log, never decide) | on
|
|
642
|
+
host: splash # the hosts: entry serving /v1/systemone
|
|
643
|
+
model: Qwen3.8-Flash-Next-GSQ-RCO-GGUF:Q2_0 # else the host's first model
|
|
644
|
+
rules: [git-push, git-rebase] # the rule ids it may answer for
|
|
645
|
+
threshold: 0.8 # the probability an auto-allow needs
|
|
646
|
+
min_record: 5 # clean records a rule needs before it is trusted
|
|
647
|
+
timeout_ms: 2000 # a slower judgment is no judgment
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`SystemOne::Client.from_config(host_registry: ...)` reads these on every
|
|
651
|
+
call, so a config.yml change takes effect on the next ask. `mode: off` (the
|
|
652
|
+
default) or no `host` means there is no client at all. Anything else is no
|
|
653
|
+
judgment and one `Log.warn(:guardrails, "system_one_failed", …)`: an
|
|
654
|
+
unreachable host, a host that isn't `api: openai`, a status other than 200,
|
|
655
|
+
a body that isn't JSON, a question with no answer, a request longer than
|
|
656
|
+
`timeout_ms`. A nil means "ask the user", which is what the caller would
|
|
657
|
+
have done anyway.
|
|
658
|
+
|
|
659
|
+
### What the audit writes
|
|
660
|
+
|
|
661
|
+
With a client, `GuardrailWiring#request_approval` asks System 1 one `noul`
|
|
662
|
+
question, `allow`, before the user is asked. Its instruction is a constant
|
|
663
|
+
in code (`SystemOne::ApprovalQuestion::INSTRUCTIONS`), reviewed like a
|
|
664
|
+
prompt; the state is the ask's own facts — the rule and its source, the
|
|
665
|
+
reason, the tool, the command (or the paths) cut to 2000 characters, the
|
|
666
|
+
edit's diff, the cwd, the repo and branch, and the session's title when it
|
|
667
|
+
has one. A memory ask also names what it is about: the entry and its scope,
|
|
668
|
+
and for a removal the description that entry's index line carries
|
|
669
|
+
(`Targets#memory`, read into the card by `Guardrails::Approval`): a path
|
|
670
|
+
alone tells the judge too little. The state then carries up to five of the
|
|
671
|
+
user's **earlier asks they answered** (`SystemOne::Similar`) — this rule in
|
|
672
|
+
this repository first, then this rule anywhere, then any answered ask, ranked
|
|
673
|
+
by how many words the logged call shares with this one, ties going to the
|
|
674
|
+
newest. Reading the log is best-effort, so a log that cannot be read simply
|
|
675
|
+
offers no examples. A participant's review turn is never judged: there is no
|
|
676
|
+
user there to ask. The judgment runs before the card appears, so an ask is
|
|
677
|
+
slower by up to `timeout_ms` (2000 ms by default).
|
|
678
|
+
|
|
679
|
+
Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
|
|
680
|
+
`$XDG_STATE_HOME/samagotchi/guardrails/` (`SystemOne::Log`): 0600, appended
|
|
681
|
+
under an exclusive `flock` (several workers write it), and rewritten —
|
|
682
|
+
keeping the newest half of its lines — once it passes 5 MB. A line is never
|
|
683
|
+
rewritten to add its label:
|
|
684
|
+
|
|
685
|
+
```json
|
|
686
|
+
{"kind":"judgment","id":"…","at":"…","session_id":"…","rule":"git-push","source":"config","tool":"execute","call":"git push origin main","call_digest":"<sha256 of the whole call>","repo_name":"samagotchi","p":0.97,"threshold":0.8,"model":"Qwen3.8-Flash","latency_ms":180,"decision":"audit","origin":"live"}
|
|
687
|
+
{"kind":"label","id":"…","at":"…","answer":"allow_once"}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
`decision` is what the judgment led to: `audit` (audit mode), `asked` (on
|
|
691
|
+
mode, the user was asked), `auto` (on mode, System 1 answered for the user).
|
|
692
|
+
A judgment that never arrived is logged too, with `p` null, so the failures
|
|
693
|
+
count as well. The label line, keyed by the judgment's id, is what the user
|
|
694
|
+
did: `allow_once`, `allow_wide` (any scope wider than once), `deny`, or
|
|
695
|
+
`none` for an ask that was cancelled. **A parent agent's answer is not the
|
|
696
|
+
user's: its allow labels nothing. Its deny is a label only where the parent
|
|
697
|
+
could have allowed instead** — the ask is not `user_only` and the session's
|
|
698
|
+
parent level permits a permit (`Guardrails::ParentPolicy`); a parent that was
|
|
699
|
+
not allowed to allow says nothing about what the user would have done. An
|
|
700
|
+
auto-allow has no label at all, since nobody was asked.
|
|
701
|
+
`SystemOne::Log#entries` reads the file and returns the judgments joined with
|
|
702
|
+
their last label.
|
|
703
|
+
|
|
704
|
+
Everything about the log is best-effort: a write that fails is one
|
|
705
|
+
`Log.warn(:guardrails, "system_one_log_failed", …)` and the approval goes on
|
|
706
|
+
as if System 1 were not there.
|
|
707
|
+
|
|
708
|
+
`ruby script/system-one-probe HOST [MODEL]` asks three fixed approval-like
|
|
709
|
+
questions through the client and prints each probability and the latency of
|
|
710
|
+
the call — how a host is checked before any of this is turned on.
|
|
711
|
+
|
|
712
|
+
### Answering for the user: `on` mode
|
|
713
|
+
|
|
714
|
+
In `on` mode an ask is allowed **once**, without reaching the user, when every
|
|
715
|
+
gate below passes, in this order:
|
|
716
|
+
|
|
717
|
+
1. the mode is `on` (in `audit` the judgment decides nothing);
|
|
718
|
+
2. the ask's rule is in `guardrails.system_one.rules` — `"git-push|git-rebase"`
|
|
719
|
+
or a YAML list, read like `guardrails.small_models`. Nothing is auto-allowed
|
|
720
|
+
by default: the user names the rules they want;
|
|
721
|
+
3. the rule is not one of the three that let an agent answer chi's own
|
|
722
|
+
questions — `chi-answer-piped`, `chi-answer-http`, `chi-broadcast` — which
|
|
723
|
+
are never auto-allowed, even when listed
|
|
724
|
+
(`SystemOne::SelfProtect::NEVER_AUTO`);
|
|
725
|
+
4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`): no
|
|
726
|
+
edit or write whose paths include config.yml in chi's config dir, anything
|
|
727
|
+
under the hooks dir, the installed bundles dir or the guardrails store (where
|
|
728
|
+
`approvals.json` and `judgments.jsonl` live), and no `execute` or
|
|
729
|
+
`task_create` command naming any of them — the `~/.config/samagotchi`
|
|
730
|
+
spelling counts too. The memories dir is deliberately not one of these:
|
|
731
|
+
reading or appending a memory file can be auto-allowed. A protected rule
|
|
732
|
+
(`Verdict::PROTECTED_RULES`) may be listed by name and System 1 may answer
|
|
733
|
+
it, but a call that really writes config.yml or the hooks is refused here;
|
|
734
|
+
5. the judgment's `p` is at least `guardrails.system_one.threshold` (0.8);
|
|
735
|
+
6. the rule's record is clean (`SystemOne::Record`, read from the log once per
|
|
736
|
+
ask): at least `guardrails.system_one.min_record` (5) of its asks judged at
|
|
737
|
+
or above the threshold that the user allowed, and none of its **last 20
|
|
738
|
+
labelled** asks judged at the threshold that the user denied or later
|
|
739
|
+
flagged as one System 1 should have asked about. Asks with no label, no
|
|
740
|
+
probability, or a `none` label count for neither side.
|
|
741
|
+
|
|
742
|
+
Then the judgment's `decision` is `auto`, the verdict settles as
|
|
743
|
+
`settle!(:allow, decided_by: "system one")` with scope `once`, and nothing is
|
|
744
|
+
written to `approvals.json` — the next call is judged again. **No question is
|
|
745
|
+
opened, so no card appears, no `:question_asked` hook fires, and no
|
|
746
|
+
notification is sent.** The verdict's activity carries
|
|
747
|
+
`note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p`; the line
|
|
748
|
+
that shows it in the terminal and the web is P3b, and is not rendered yet, so
|
|
749
|
+
today an auto-allowed call simply looks like a call that never needed
|
|
750
|
+
approval. If the judgment line cannot be written to the log, the ask goes to
|
|
751
|
+
the user.
|
|
752
|
+
|
|
753
|
+
A delegate child answers from its own session's settings: its System 1 can
|
|
754
|
+
auto-allow a call even when the child's `parent_answers: hard` bars a parent
|
|
755
|
+
from allowing it — the record gate is about the rule, not about who answers.
|
|
756
|
+
|
|
757
|
+
To correct an auto-allow after the fact, run `/guardrails` to see this
|
|
758
|
+
session's recent unlabelled auto-allows, newest first, then
|
|
759
|
+
`/guardrails wrong [N]` to label one. In chi web, the owner can instead click
|
|
760
|
+
**should have asked** on that tool row; participants cannot use that route.
|
|
761
|
+
The label is appended, not written over the judgment. `should_have_asked`
|
|
762
|
+
makes the rule's record fail the false-allow check, so the rule asks again
|
|
763
|
+
until enough later clean asks rebuild its record.
|
|
764
|
+
|
|
765
|
+
### Seeding the record: `chi guardrails replay`
|
|
766
|
+
|
|
767
|
+
A rule's record starts empty, so `chi guardrails replay` (this machine only)
|
|
768
|
+
judges the asks the sessions here already answered and writes them to the same
|
|
769
|
+
log, with the user's answer as the label:
|
|
770
|
+
|
|
771
|
+
```sh
|
|
772
|
+
chi guardrails replay [--dry-run] [--threshold T] [--model M]
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
It reads every session's sidecar, `$XDG_STATE_HOME/samagotchi/sessions/<id>/cards.json`
|
|
776
|
+
(the file `Bridge::CardStore` saves), and takes the entries of type `question`
|
|
777
|
+
whose `pending_question.approval` is there and that have an `answer` or were
|
|
778
|
+
`cancelled`. A label is the scope the user took — `allow_once` for "Allow once",
|
|
779
|
+
`allow_wide` for a wider one, `deny` for anything else, `none` for a cancelled
|
|
780
|
+
ask. Skipped: a question no one answered, a relay card, which carries no
|
|
781
|
+
approval of its own, and a question id a previous replay already logged — so
|
|
782
|
+
running it twice adds nothing. A parent agent's answer is not the user's: its
|
|
783
|
+
allow is skipped, and its deny is replayed only where the parent could have
|
|
784
|
+
allowed (the card is not `user_only` and the session's parent level permits a
|
|
785
|
+
permit). A cards file that doesn't parse is skipped with one warning line,
|
|
786
|
+
never a failed run.
|
|
787
|
+
|
|
788
|
+
Each ask is judged exactly as a live one is (`SystemOne::ApprovalQuestion`),
|
|
789
|
+
and written as a `judgment` line with `decision` and `origin` both `replay`,
|
|
790
|
+
its `question_id` and the card's `created_at` as `asked_at`, followed by its
|
|
791
|
+
label. Then a table, one row per rule sorted by how many asks it had, and a
|
|
792
|
+
total: the asks, how many the user allowed, denied, how many at the threshold
|
|
793
|
+
System 1 would have auto-allowed, how many of those the user denied (the false
|
|
794
|
+
allows), and how many got no judgment. `--threshold` overrides
|
|
795
|
+
`guardrails.system_one.threshold` for the table; `--model` overrides the model
|
|
796
|
+
for this run; `--dry-run` judges and prints without writing a line. Without a
|
|
797
|
+
client (`mode: off`, or no `host`) it exits 2 saying what to set.
|
|
798
|
+
|
|
799
|
+
Two limits on what the replay can see: `CardStore` keeps only the last 20
|
|
800
|
+
questions per session, and a `--no-shared` REPL session never writes a
|
|
801
|
+
`cards.json` at all, so its asks are not replayable.
|
|
802
|
+
|
|
616
803
|
## The guardrails bundle
|
|
617
804
|
|
|
618
805
|
```sh
|
data/docs/sessions.md
CHANGED
|
@@ -94,7 +94,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
94
94
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
95
95
|
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker and its LLM context and thinking chips, a choice for the next new session only; a fresh chain link's picker and its `/model`), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
96
96
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
97
|
-
- The web session page's message box folds away for room to read. Drag the grip on its top edge (the pill that shows on hover; always on a phone) down, past the box by 32 px: the pill turns accent while releasing would hide it, and on release the box folds and the info bar stays as the page's bottom edge. The grip stays as a handle on the info bar's top edge, drawn always: pull it up (8 px opens it, and the box follows the pointer in the same gesture), click or tap it (opens it and focuses the box), or just type on the page (the key lands in the box) to bring it back; Enter or Space on the focused grip folds or opens too. A draft (text or images, which folding never clears) colours the handle with the accent, a session waiting on you with the attention colour; its tooltip says so. Stop turn moves into the info bar while it is folded. The fold is remembered per browser (`chi_composer_collapsed`), an archived session opens folded for the visit, and opening it there, like the page opening it itself (a restored line, a quote, continue →'s opener), is for that visit only
|
|
97
|
+
- The web session page's message box folds away for room to read. Drag the grip on its top edge (the pill that shows on hover; always on a phone) down, past the box by 32 px: the pill turns accent while releasing would hide it, and on release the box folds and the info bar stays as the page's bottom edge. The grip stays as a handle on the info bar's top edge, drawn always: pull it up (8 px opens it, and the box follows the pointer in the same gesture), click or tap it (opens it and focuses the box), or just type on the page (the key lands in the box) to bring it back; Enter or Space on the focused grip folds or opens too. A draft (text or images, which folding never clears) colours the handle with the accent, a session waiting on you with the attention colour; its tooltip says so. Stop turn moves into the info bar while it is folded. The fold is remembered per browser (`chi_composer_collapsed`), an archived session opens folded for the visit, and opening it there, like the page opening it itself (a restored line, a quote, continue →'s opener), is for that visit only. The fold keeps the height the grip set (`chi_composer_floor`): the box opens again at the height it had, and only a double-click on the grip forgets it. A click on the expanded grip hides the box as well: it folds 250 ms after the press, so a second click inside that window is read as a double-click and resets the height instead (a drag, Enter or Space act at once, and a drag after a click takes the wait back). The start page's grip only resizes.
|
|
98
98
|
|
|
99
99
|
**Test-session hygiene:**
|
|
100
100
|
|
|
@@ -192,6 +192,7 @@ module Samagotchi
|
|
|
192
192
|
activity = { tool: part[:tool], status: part[:status], params: part[:params], **title }
|
|
193
193
|
activity[:action] = part[:action] if part[:action]
|
|
194
194
|
activity[:no_match] = true if part[:no_match]
|
|
195
|
+
activity[:guardrail] = part[:guardrail] if part[:guardrail]
|
|
195
196
|
completed = { type: :tool_call_completed, **call, output: part[:output], output_truncated: !!part[:output_truncated],
|
|
196
197
|
activity: activity }
|
|
197
198
|
completed[:duration_ms] = part[:duration_ms] unless part[:duration_ms].nil?
|
|
@@ -333,6 +334,9 @@ module Samagotchi
|
|
|
333
334
|
capped = output.length > @max_output_chars
|
|
334
335
|
tool[:status] = (event.dig(:activity, :status) || "ok").to_s
|
|
335
336
|
tool[:no_match] = true if event.dig(:activity, :no_match)
|
|
337
|
+
# Only System 1's auto-allow is drawn on the row (the web's label).
|
|
338
|
+
guardrail = event.dig(:activity, :guardrail)
|
|
339
|
+
tool[:guardrail] = guardrail if guardrail.is_a?(Hash) && guardrail[:decided_by] == "system one"
|
|
336
340
|
tool[:output] = capped ? ToolOutputCut.preview(output, cap: @max_output_chars, tool: event[:tool]) : output.dup
|
|
337
341
|
tool[:output_truncated] = capped || !!event[:output_truncated]
|
|
338
342
|
tool[:images] = event[:images] if event[:images]
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "bridge/sse_writer"
|
|
|
19
19
|
require_relative "bridge/turn_accumulator"
|
|
20
20
|
require_relative "session"
|
|
21
21
|
require_relative "engine"
|
|
22
|
+
require_relative "system_one/log"
|
|
22
23
|
require_relative "session_commands"
|
|
23
24
|
require_relative "queued_command"
|
|
24
25
|
require_relative "steer"
|
|
@@ -584,6 +585,7 @@ module Samagotchi
|
|
|
584
585
|
%w[POST cancel] => :handle_cancel,
|
|
585
586
|
%w[POST answer] => :handle_answer,
|
|
586
587
|
%w[POST question/dismiss] => :handle_dismiss_question,
|
|
588
|
+
%w[POST guardrails/wrong] => :handle_system_one_wrong,
|
|
587
589
|
%w[POST turn] => :handle_post_turn,
|
|
588
590
|
%w[POST turn/dequeue] => :handle_dequeue_turn,
|
|
589
591
|
%w[POST command] => :handle_command,
|
|
@@ -730,6 +732,30 @@ module Samagotchi
|
|
|
730
732
|
end
|
|
731
733
|
end
|
|
732
734
|
|
|
735
|
+
# POST /session/:id/guardrails/wrong {id}: append a false-allow label
|
|
736
|
+
# only for this session's System 1 auto-allow. Unknown ids are not
|
|
737
|
+
# distinguishable from judgments belonging to another session.
|
|
738
|
+
def handle_system_one_wrong(session_id, body)
|
|
739
|
+
parsed = parse_json(body)
|
|
740
|
+
return [{ "Allow" => "POST" }, 400, { error: "invalid_json" }] unless parsed.is_a?(Hash)
|
|
741
|
+
|
|
742
|
+
judgment_id = fetched(parsed, "id").to_s.strip
|
|
743
|
+
return [{ "Allow" => "POST" }, 400, { error: "missing_fields", detail: "id required" }] if judgment_id.empty?
|
|
744
|
+
|
|
745
|
+
log = SystemOne::Log.new(dir: Guardrails::Approvals.dir_for(@state_dir))
|
|
746
|
+
entry = log.entries.find do |judgment|
|
|
747
|
+
judgment["id"] == judgment_id &&
|
|
748
|
+
judgment["session_id"] == session_id &&
|
|
749
|
+
judgment["decision"] == "auto"
|
|
750
|
+
end
|
|
751
|
+
return [{}, 404, { error: "unknown_judgment" }] unless entry
|
|
752
|
+
return [{}, 200, { status: "labelled", id: judgment_id }] if entry["answer"] == "should_have_asked"
|
|
753
|
+
return [{}, 409, { error: "already_labelled" }] unless entry["answer"].nil?
|
|
754
|
+
return [{}, 500, { error: "label_failed", detail: "could not write the System 1 label" }] unless log.label(judgment_id, "should_have_asked")
|
|
755
|
+
|
|
756
|
+
[{}, 200, { status: "labelled", id: judgment_id }]
|
|
757
|
+
end
|
|
758
|
+
|
|
733
759
|
# What a parent's relay may say about this session's approval
|
|
734
760
|
# (BridgeClient#relay): opened and closed only change how the question
|
|
735
761
|
# shows (QuestionDesk#annotate); answered makes this worker ask the
|
|
@@ -145,6 +145,13 @@ module Samagotchi
|
|
|
145
145
|
post("question/dismiss", { id: id, deadline: deadline }, read_body: true)
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
# POST /session/:id/guardrails/wrong: mark this session's System 1
|
|
149
|
+
# auto-allow as one it should have asked about; 404 when unknown.
|
|
150
|
+
# @return [Response]
|
|
151
|
+
def mark_system_one_wrong(judgment_id:)
|
|
152
|
+
post("guardrails/wrong", { id: judgment_id }, read_body: true)
|
|
153
|
+
end
|
|
154
|
+
|
|
148
155
|
# POST /session/:id/turn/dequeue: take back a queued message (one that
|
|
149
156
|
# waits for the running turn's end) before it runs. 200 {status:
|
|
150
157
|
# "removed", enqueued_id, prompt}; 409 already_started (a turn took
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -173,6 +173,21 @@ module Samagotchi
|
|
|
173
173
|
# (Guardrails::ParentPolicy): hard = deny and plain answers; standard = + spend (Continue); soft = + permit (Allow
|
|
174
174
|
# once). An invalid value is hard, the strictest.
|
|
175
175
|
Entry.new(key: "guardrails.parent_answers", yaml_path: %w[guardrails parent_answers], type: :enum, default: "soft", expose: %i[config], enum_values: %w[hard standard soft], invalid: "hard", lifecycle: :next_tool_or_approval),
|
|
176
|
+
# System 1 (the cheap judgment model that answers the asks a user always allows). Client and settings; in `on`
|
|
177
|
+
# mode GuardrailWiring also reads rules, threshold and min_record for the auto-allow (docs/guardrails.md).
|
|
178
|
+
# off (the default) | audit (judge every ask, log it, never decide) | on. An unknown value warns and is off.
|
|
179
|
+
Entry.new(key: "guardrails.system_one.mode", yaml_path: %w[guardrails system_one mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :next_tool_or_approval),
|
|
180
|
+
# The hosts: entry serving /v1/systemone (Splash, or the hosted Jev API); unset: no judgment.
|
|
181
|
+
Entry.new(key: "guardrails.system_one.host", yaml_path: %w[guardrails system_one host], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
182
|
+
# The judgment model; unset: the host's first model, and no judgment if it names none.
|
|
183
|
+
Entry.new(key: "guardrails.system_one.model", yaml_path: %w[guardrails system_one model], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
184
|
+
# The rule ids System 1 may answer for ("a|b", or a YAML list, parsed like guardrails.small_models).
|
|
185
|
+
Entry.new(key: "guardrails.system_one.rules", yaml_path: %w[guardrails system_one rules], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
186
|
+
# The probability an auto-allow needs, and how many clean records a rule needs before it is trusted.
|
|
187
|
+
Entry.new(key: "guardrails.system_one.threshold", yaml_path: %w[guardrails system_one threshold], type: :float, default: 0.8, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
188
|
+
Entry.new(key: "guardrails.system_one.min_record", yaml_path: %w[guardrails system_one min_record], type: :integer, default: 5, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
189
|
+
# How long one judgment may take, in milliseconds; a judgment that doesn't arrive asks the user.
|
|
190
|
+
Entry.new(key: "guardrails.system_one.timeout_ms", yaml_path: %w[guardrails system_one timeout_ms], type: :integer, default: 2000, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
176
191
|
|
|
177
192
|
Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli], lifecycle: :process_start),
|
|
178
193
|
Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli], lifecycle: :process_start),
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -401,7 +401,12 @@ module Samagotchi
|
|
|
401
401
|
cancelled: -> { !!active_cancel_controller&.cancelled? },
|
|
402
402
|
# An approval of the model's call: :question_asked first (QuestionAsked).
|
|
403
403
|
ask: ->(fields) { open_question(question_asked(QuestionAsked::KIND_APPROVAL, fields)) },
|
|
404
|
-
notify: ->(message) { guardrail_notify(message) }
|
|
404
|
+
notify: ->(message) { guardrail_notify(message) },
|
|
405
|
+
# System 1 (experimental, audit phase): the judgment client is read
|
|
406
|
+
# from config on every ask, and the running turn's cancel controller
|
|
407
|
+
# closes a judgment that is no longer wanted.
|
|
408
|
+
system_one: -> { SystemOne::Client.from_config(host_registry: @host_registry) },
|
|
409
|
+
cancel_controller: -> { active_cancel_controller }
|
|
405
410
|
)
|
|
406
411
|
end
|
|
407
412
|
private :build_guardrail_wiring
|