samagotchi 0.86.0 → 0.87.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 +17 -1
- data/bin/chi +39 -2
- data/docs/architecture.md +5 -3
- data/docs/cli.md +15 -2
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +47 -2
- data/docs/hooks.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +16 -4
- data/docs/sub-agent.md +6 -3
- data/lib/samagotchi/answer_command.rb +6 -6
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -3
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +3 -1
- data/lib/samagotchi/guardrails/parent_policy.rb +74 -5
- data/lib/samagotchi/parent_answers_command.rb +80 -0
- data/lib/samagotchi/parent_report.rb +53 -34
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/send_command.rb +75 -10
- data/lib/samagotchi/session.rb +26 -2
- data/lib/samagotchi/session_commands.rb +35 -9
- data/lib/samagotchi/session_setup.rb +10 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +17 -7
- data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -5
- data/lib/samagotchi/terminal_ui.rb +11 -5
- data/lib/samagotchi/tools/delegate.rb +4 -1
- data/lib/samagotchi/tools/delegate_wait.rb +11 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/worker.rb +9 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bfd7070b1c55cbb0e0647a79ec729996a78318ca2b2f4ccbf19c24ce7240eb9
|
|
4
|
+
data.tar.gz: 5fa4d754f3c9bb79767978d3b9fa6ac1c7091c84897a9043f3ad3129bd98c24c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 787b7debd71f8a408ffaa801bd7d352de1f3769d018959b35e3933b16641efb194220630482d14f5ec32d4c50b5fce8f56c8a78b88a1fc1c2de58c2e05e6b361
|
|
7
|
+
data.tar.gz: 5f3759bd7c6e1e9fea4e5b7af8eab1c107d5296f9eba85b0b99fd42381c5a1c3dbb01b7efb86b52ec14b11958333110a97cfab80b5d8fd5a3a1489cab2f50bdd
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.87.0] - 2026-10-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A session's own `parent_answers`, tighten-only: `chi --parent-answers LEVEL` and `chi send --new --parent-answers
|
|
16
|
+
LEVEL` save it in the new session (kept by respawns, forks and continues; a `delegate` child starts with its
|
|
17
|
+
parent's), and what a parent agent may answer there is the stricter of it and config.yml's
|
|
18
|
+
`guardrails.parent_answers`, in `chi answer`, the worker and the guidance. `/parent-answers` shows and sets it;
|
|
19
|
+
anyone may tighten it, only you loosen it, at the REPL, an attached terminal or the web.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- The known-names hook treats `#` URL fragments as token boundaries, allowing exact protected names in URLs while
|
|
24
|
+
still rejecting misspellings before a fragment.
|
|
25
|
+
|
|
11
26
|
## [0.86.0] - 2026-10-18
|
|
12
27
|
|
|
13
28
|
### Added
|
|
@@ -3466,7 +3481,8 @@ and long-lived sessions.
|
|
|
3466
3481
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3467
3482
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3468
3483
|
|
|
3469
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3484
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.87.0...HEAD
|
|
3485
|
+
[0.87.0]: https://github.com/dm1try/samagotchi/compare/v0.86.0...v0.87.0
|
|
3470
3486
|
[0.86.0]: https://github.com/dm1try/samagotchi/compare/v0.85.0...v0.86.0
|
|
3471
3487
|
[0.85.0]: https://github.com/dm1try/samagotchi/compare/v0.84.0...v0.85.0
|
|
3472
3488
|
[0.84.0]: https://github.com/dm1try/samagotchi/compare/v0.83.0...v0.84.0
|
data/bin/chi
CHANGED
|
@@ -82,7 +82,7 @@ ARGV.each do |arg|
|
|
|
82
82
|
base = arg.split("=", 2).first
|
|
83
83
|
# If dashed version is a known flag (registry or hardcoded), reject underscore variant
|
|
84
84
|
dashed = base.tr("_", "-")
|
|
85
|
-
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
85
|
+
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --parent-answers --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
86
86
|
if known.include?(dashed) || known.include?(base)
|
|
87
87
|
warn "Unknown option: #{arg} (did you mean #{dashed}?)"
|
|
88
88
|
exit 1
|
|
@@ -130,6 +130,11 @@ parser = OptionParser.new do |opts|
|
|
|
130
130
|
"saved in the session: --set cost-cap.cap=3)") do |v|
|
|
131
131
|
(options[:sets] ||= []) << v
|
|
132
132
|
end
|
|
133
|
+
opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
|
|
134
|
+
"hard|standard|soft, only stricter than config.yml's guardrails.parent_answers (saved in the " \
|
|
135
|
+
"session; only you loosen it, with /parent-answers in the terminal or the web)") do |v|
|
|
136
|
+
options[:parent_answers] = v
|
|
137
|
+
end
|
|
133
138
|
opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
|
|
134
139
|
opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
|
|
135
140
|
"config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
|
|
@@ -234,6 +239,22 @@ if options[:sets] && web
|
|
|
234
239
|
warn "Error: --set is a new session's (chi --set, chi send --new --set), not chi web's"
|
|
235
240
|
exit 2
|
|
236
241
|
end
|
|
242
|
+
# --parent-answers: a level in any case (no abbreviations), else a usage
|
|
243
|
+
# error, as chi send's.
|
|
244
|
+
if options[:parent_answers]
|
|
245
|
+
require "samagotchi/guardrails/parent_policy"
|
|
246
|
+
levels = Samagotchi::Guardrails::ParentPolicy::ORDER
|
|
247
|
+
word = options[:parent_answers].strip.downcase
|
|
248
|
+
unless levels.include?(word)
|
|
249
|
+
warn "Error: --parent-answers takes #{levels.join(", ")}, not #{options[:parent_answers]}"
|
|
250
|
+
exit 2
|
|
251
|
+
end
|
|
252
|
+
options[:parent_answers] = word
|
|
253
|
+
end
|
|
254
|
+
if options[:parent_answers] && web
|
|
255
|
+
warn "Error: --parent-answers is a new session's (chi --parent-answers, chi send --new --parent-answers), not chi web's"
|
|
256
|
+
exit 2
|
|
257
|
+
end
|
|
237
258
|
|
|
238
259
|
# A scratch session is new, runs here and goes when it ends.
|
|
239
260
|
if scratch
|
|
@@ -395,6 +416,20 @@ if options[:sets]
|
|
|
395
416
|
options[:toolkit_flags] = (options[:toolkit_flags] || []) + ["--set"]
|
|
396
417
|
end
|
|
397
418
|
|
|
419
|
+
# --parent-answers LEVEL: a new session's own guardrails.parent_answers. An
|
|
420
|
+
# existing session's (--attach/--resume) is ignored with the flags' line,
|
|
421
|
+
# never sent as a /parent-answers command: that would loosen it from a
|
|
422
|
+
# command line, which is the user's to do in a UI.
|
|
423
|
+
if options[:parent_answers]
|
|
424
|
+
if options[:attach] || options[:resume]
|
|
425
|
+
options[:toolkit_flags] = (options[:toolkit_flags] || []) + ["--parent-answers"]
|
|
426
|
+
else
|
|
427
|
+
require "samagotchi/guardrails/parent_policy"
|
|
428
|
+
note = Samagotchi::Guardrails::ParentPolicy.capped_note(options[:parent_answers])
|
|
429
|
+
warn "Warning: --parent-answers #{note}" if note
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
398
433
|
chi_update_hint.call
|
|
399
434
|
require "samagotchi/launch_mode"
|
|
400
435
|
launch, launch_note = if scratch
|
|
@@ -432,7 +467,8 @@ if launch == :attached
|
|
|
432
467
|
appended_prompt_files: options[:appended_prompt_files] || [],
|
|
433
468
|
toolkit: options[:toolkit_value],
|
|
434
469
|
toolkit_flags: options[:toolkit_flags] || [],
|
|
435
|
-
bundle_settings: options[:bundle_settings] || {}
|
|
470
|
+
bundle_settings: options[:bundle_settings] || {},
|
|
471
|
+
parent_answers: options[:parent_answers])
|
|
436
472
|
rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
|
|
437
473
|
warn "Error: #{e.message}"
|
|
438
474
|
exit 1
|
|
@@ -455,6 +491,7 @@ begin
|
|
|
455
491
|
toolkit: options[:toolkit_value],
|
|
456
492
|
toolkit_flags: options[:toolkit_flags] || [],
|
|
457
493
|
bundle_settings: options[:bundle_settings] || {},
|
|
494
|
+
parent_answers: options[:parent_answers],
|
|
458
495
|
non_interactive: options[:non_interactive],
|
|
459
496
|
model_name: options[:model],
|
|
460
497
|
scratch: scratch
|
data/docs/architecture.md
CHANGED
|
@@ -296,8 +296,9 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
296
296
|
(`chi answer`, a parent agent) the answer is checked by `Guardrails::ParentPolicy` in
|
|
297
297
|
`QuestionDesk#answer`: the question carries each option's effect (`effects`, set by its asker:
|
|
298
298
|
`Approval.payload`, `ContinueOffer`, `Relay.card`; derived for an untagged one) and `user_only`,
|
|
299
|
-
and an option beyond the worker's `guardrails.parent_answers`
|
|
300
|
-
|
|
299
|
+
and an option beyond the worker's `guardrails.parent_answers` (tightened by the session's own
|
|
300
|
+
`parent_answers`: `ParentPolicy.for_session`) is `403 parent_answer_refused` (with its `reason`),
|
|
301
|
+
the question still open. `chi answer` makes the same check first, from the session file.
|
|
301
302
|
- `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
|
|
302
303
|
`200`, or `409` when it is no longer pending.
|
|
303
304
|
- `POST /session/:id/tasks/stop` — `{task_id}`: stop a background task as the user
|
|
@@ -321,7 +322,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
321
322
|
pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
|
|
322
323
|
makes the child ask its parent's Bridge for the answer (`RelayVerifier`) and take it only for
|
|
323
324
|
this child and the question pending now. `200`, `409` no longer pending, `403` a parent agent's
|
|
324
|
-
pick beyond the child's own `guardrails.parent_answers`
|
|
325
|
+
pick beyond the child's own `guardrails.parent_answers` and session `parent_answers` (`parent_answer_refused`; the
|
|
326
|
+
relay card in the parent was held to the parent's), `422 relay_unverified` (the question
|
|
325
327
|
stays open).
|
|
326
328
|
- `POST /session/:id/relay/status` — on a parent's worker, `{relay_id}`: what its `RelayDesk`
|
|
327
329
|
(memory only) holds for that relay (`child_id`, `child_question_id`, `state`, `answer`, `by`);
|
data/docs/cli.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
|
|
20
20
|
- `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
|
|
21
21
|
- `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
22
|
-
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers` (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
|
|
22
|
+
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
|
|
23
23
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
24
24
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
25
25
|
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
|
|
@@ -237,6 +237,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
|
|
|
237
237
|
| `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
|
|
238
238
|
| `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
|
|
239
239
|
| `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
|
|
240
|
+
| `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
|
|
240
241
|
| `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number, `true` is true, `false` and `off` are false, anything else stays the text as given (`010`, `0x10`, `3:30`, `No`, `null`). For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
|
|
241
242
|
| `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
|
|
242
243
|
| `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
|
|
@@ -598,6 +599,18 @@ running turn keeps its tools. `/toolkit` alone answers mid-turn too; a change
|
|
|
598
599
|
sent while a turn runs waits for the turn's end. The web shows a session that
|
|
599
600
|
isn't on `full` as the `tools` chip. See [Toolkits](configuration.md#toolkits).
|
|
600
601
|
|
|
602
|
+
`/parent-answers` shows what a parent agent may answer on this session's
|
|
603
|
+
questions: the level that applies, the session's own and config.yml's
|
|
604
|
+
(`parent answers: standard (this session: standard; config.yml
|
|
605
|
+
guardrails.parent_answers: soft)`). `/parent-answers hard|standard|soft` sets the
|
|
606
|
+
session's own, `/parent-answers default` drops it; it is saved at once and
|
|
607
|
+
holds from the next answer. It never goes looser than config.yml's (the reply
|
|
608
|
+
says when config.yml's applies instead). Anyone may tighten it; only you loosen
|
|
609
|
+
it, at the REPL, an attached terminal or the web: `chi send`, a piped or marked
|
|
610
|
+
attach and `chi -p --non-interactive` are refused. `/parent-answers` alone answers
|
|
611
|
+
mid-turn too; a change waits for the turn's end. No chip: its reply is where the
|
|
612
|
+
web shows it. See [Guardrails](guardrails.md#answers-from-a-parent-agent).
|
|
613
|
+
|
|
601
614
|
A `/thinking`, `/reasoning-history` or `/toolkit` change that restarts the prompt cache
|
|
602
615
|
asks first: the reply says what it costs, changes nothing, and names the line
|
|
603
616
|
that applies it (`/thinking low --yes`). `--yes` (or `-y`) applies it at once.
|
|
@@ -1187,7 +1200,7 @@ Behavior notes:
|
|
|
1187
1200
|
model that it wasn't continued (or your reason); `!rollback` still erases it.
|
|
1188
1201
|
- `--non-interactive` (`chi -p`) has nobody to ask: the turn ends there. A `chi send --wait` on a session nobody can
|
|
1189
1202
|
answer (an older worker) reports `limit` (exit 1) with how to continue it: `chi send ID -m '/continue yes'`
|
|
1190
|
-
(not under `guardrails.parent_answers: hard
|
|
1203
|
+
(not under `guardrails.parent_answers: hard`, config.yml's or the session's own: a parent agent's `/continue yes`, sent with `chi send` or typed into a
|
|
1191
1204
|
piped `chi --attach`, is held to it as `chi answer --option Continue` is, and refused there).
|
|
1192
1205
|
- The check-in bundle's card closes as the turn ends, before the step-limit question opens, so the two never stand
|
|
1193
1206
|
together; with check-in's default `after: 50` and the limit of 300 a long turn shows check-in cards (at 50, 100, …)
|
data/docs/configuration.md
CHANGED
|
@@ -1633,7 +1633,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
1633
1633
|
| `guardrails.enabled` | `true` | | `false`: no rules, and hooks' asks are dropped (a deny still applies). config.yml only: a worker unsets any `SAMAGOTCHI_GUARDRAILS_*` it inherits. See [Guardrails](guardrails.md). |
|
|
1634
1634
|
| `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). |
|
|
1635
1635
|
| `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). |
|
|
1636
|
-
| `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`. 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). |
|
|
1636
|
+
| `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). |
|
|
1637
1637
|
| `log.file` | state dir | yes | See "Debug Log File". |
|
|
1638
1638
|
| `log.disable` | `false` | yes | No file logging. |
|
|
1639
1639
|
| `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
|
data/docs/guardrails.md
CHANGED
|
@@ -150,6 +150,47 @@ approval's block lists the Allow once command next to the deny; under
|
|
|
150
150
|
`standard` and `hard` it says allowing is up to the user. An invalid value
|
|
151
151
|
warns and is `hard`.
|
|
152
152
|
|
|
153
|
+
#### A session's own level
|
|
154
|
+
|
|
155
|
+
A session may carry its own `parent_answers`, set when it is created (`chi
|
|
156
|
+
--parent-answers LEVEL`, `chi send --new --parent-answers LEVEL`) and kept by
|
|
157
|
+
its respawns, forks and continues; a `delegate` child starts with its parent's,
|
|
158
|
+
as it is then. It can only tighten: config.yml's value is the ceiling, and the
|
|
159
|
+
stricter of the two applies, in `chi answer`'s check (from the session file),
|
|
160
|
+
the worker's (from its session) and in what the question block offers. A refusal
|
|
161
|
+
then names it: `a parent agent may only stop this turn here (parent_answers:
|
|
162
|
+
hard (this session))`. A looser one is saved as given with a warning
|
|
163
|
+
(`soft is looser than config.yml's guardrails.parent_answers: standard;
|
|
164
|
+
standard applies`) and takes effect only if config.yml is loosened later. A
|
|
165
|
+
value that isn't a level reads as `hard`.
|
|
166
|
+
|
|
167
|
+
`/parent-answers` shows the level that applies, the session's own and
|
|
168
|
+
config.yml's; `/parent-answers LEVEL` sets the session's own, `/parent-answers
|
|
169
|
+
default` drops it (config.yml's applies). Anyone may tighten it, a parent too
|
|
170
|
+
(`chi send ID -m "/parent-answers hard"`). Loosening it (toward `soft`, or
|
|
171
|
+
`default`) is yours alone: the line must come from you at the REPL, an attached
|
|
172
|
+
terminal or the web composer. chi refuses it (`loosening parent_answers is the
|
|
173
|
+
user's: …`) from `chi send` (a parent's or yours: the worker can't tell them
|
|
174
|
+
apart), from a piped or marked `chi --attach` or REPL, from `chi -p
|
|
175
|
+
--non-interactive`, from a delegate's follow-up and from a plugin; `chi answer`
|
|
176
|
+
has no command route. `--parent-answers` on `--attach`/`--resume` is ignored,
|
|
177
|
+
never sent as the command.
|
|
178
|
+
|
|
179
|
+
config.yml is read each time an answer is checked; a session's own value stays
|
|
180
|
+
as saved and binds whenever it is the stricter of the two. A session saved as
|
|
181
|
+
`standard` under a `hard` config.yml follows `hard`, and `standard` again once
|
|
182
|
+
config.yml is loosened. A `delegate` child keeps the value it started with,
|
|
183
|
+
whatever its parent changes later.
|
|
184
|
+
|
|
185
|
+
A parent agent on a real terminal (a PTY: tmux, a kitty window) with no agent
|
|
186
|
+
marker set looks like you, and can loosen it; so can one that edits a session
|
|
187
|
+
file (`<id>.json`, read again by the next worker that wakes for it). That is
|
|
188
|
+
not a security boundary either: config.yml still caps the result.
|
|
189
|
+
|
|
190
|
+
A setting form waits for a running turn to end, as `/toolkit`'s does: a
|
|
191
|
+
tightening typed while a child's question is pending applies after that turn,
|
|
192
|
+
and a parent may answer the question meanwhile under the level before it.
|
|
193
|
+
|
|
153
194
|
`soft` suits a capable parent model: it can let a child run one more command
|
|
154
195
|
it asked about, still with every guardrail rule in force. For a weak or
|
|
155
196
|
unknown parent model, set `standard` or `hard` (and see
|
|
@@ -177,7 +218,7 @@ A plugin's or a hook's question (`ask_user`) tags its options the same way
|
|
|
177
218
|
(`effects:`, see [Plugins](plugins.md)); an untagged one is a plain `answer`.
|
|
178
219
|
|
|
179
220
|
The guardrails settings (`enabled`, `mode`, `small_models`, `parent_answers`) are
|
|
180
|
-
read from config.yml only: they have no environment variable, and a worker
|
|
221
|
+
read from config.yml only (a session's own `parent_answers` can only tighten it): they have no environment variable, and a worker
|
|
181
222
|
chi starts unsets every `SAMAGOTCHI_GUARDRAILS_*` it would inherit. What a
|
|
182
223
|
parent's environment does choose is the config dir: `XDG_CONFIG_HOME` picks
|
|
183
224
|
which config.yml chi reads, and a worker the parent starts or wakes (`chi
|
|
@@ -204,7 +245,11 @@ person at a terminal with no marker keeps every scope.
|
|
|
204
245
|
This guards an honest but eager parent, not a hostile one: it is not a
|
|
205
246
|
security boundary. The worker's Bridge and the web's answer route on localhost
|
|
206
247
|
take an answer from any local process, which can leave the marker out (or
|
|
207
|
-
post through the web) and get every scope.
|
|
248
|
+
post through the web) and get every scope. In the same way a parent can
|
|
249
|
+
loosen a session's own level by posting `/parent-answers` with a forged
|
|
250
|
+
`tui:<pid>` or `web:<tab>` client id to the Bridge or the web's command route,
|
|
251
|
+
by editing the session file, or by running `chi --attach` on a PTY with the
|
|
252
|
+
markers cleared. chi's instructions for parents ([chi as a
|
|
208
253
|
sub-agent](sub-agent.md)) tell them to use `chi answer` only.
|
|
209
254
|
|
|
210
255
|
## Protected paths
|
data/docs/hooks.md
CHANGED
|
@@ -117,7 +117,7 @@ end
|
|
|
117
117
|
|
|
118
118
|
**Tag the options that spend or permit.** A question can be answered by a
|
|
119
119
|
parent agent (`chi answer`, a piped `chi --attach`), and
|
|
120
|
-
`guardrails.parent_answers` decides which options it may pick by their
|
|
120
|
+
`guardrails.parent_answers` (or the session's own, stricter one) decides which options it may pick by their
|
|
121
121
|
`effects:` (label => effect): `:spend` (more time or money: a Continue),
|
|
122
122
|
`:permit` (one call), `:permit_wide` (more than one), `:deny` (a Stop or a
|
|
123
123
|
Deny); an option left out is a plain `:answer`, which a parent may always
|
data/docs/plugins.md
CHANGED
|
@@ -1611,7 +1611,7 @@ bundles:
|
|
|
1611
1611
|
pass the spend, saved for the session) and **Stop** (the turn stops, `canceled` with `stopped_by: cost-cap`, and a
|
|
1612
1612
|
card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops. A parent
|
|
1613
1613
|
agent answering it (`chi answer`) may Continue under `guardrails.parent_answers: soft` or `standard`, and only Stop
|
|
1614
|
-
under `hard` (Continue is tagged `:spend`; see [Guardrails](guardrails.md#answers-from-a-parent-agent)). `/cap N` is
|
|
1614
|
+
under `hard` (config.yml's, or the session's own when stricter) (Continue is tagged `:spend`; see [Guardrails](guardrails.md#answers-from-a-parent-agent)). `/cap N` is
|
|
1615
1615
|
a command, not an answer: a parent that sends it (`chi send ID -m '/cap 10'`) raises the cap under any strategy.
|
|
1616
1616
|
- **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
|
|
1617
1617
|
cost-cap.cap=3`, `chi send --new --set cost-cap.cap=3`; `--set cost-cap.cap=off`, `0` or a negative number for none), else `child:` for a
|
data/docs/sessions.md
CHANGED
|
@@ -212,7 +212,8 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
212
212
|
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `removed` (a `--queue --wait` message taken back before it ran), `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` (`worker_stopped`: the turn waited for died with its worker and the next worker settled it) and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
213
213
|
- `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs; with an idle session whose last turn already ended, that ended turn is the answer: its reply, or its end when it left no reply — a turn that finished in the gap between waits is reported, not waited for. With none of those (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
|
|
214
214
|
- `--set BUNDLE.KEY=VALUE` (with `--new`, repeatable) gives the new session a bundle setting of its own, as `chi --set` does (see *Bundle settings* below): `chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap. A bundle or key the installed bundles don't offer is a usage error (exit 2), as is `--set` without `--new`.
|
|
215
|
-
- `--
|
|
215
|
+
- `--parent-answers LEVEL` (with `--new`) gives the new session its own `guardrails.parent_answers` (`hard`, `standard`, `soft`), tighten-only: config.yml's applies when it is stricter, with `chi send: warning: --parent-answers soft is looser than config.yml's …`. See *Parent answers* below.
|
|
216
|
+
- `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit, bundle settings and parent answers, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool`, `--set` and `--parent-answers` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
|
|
216
217
|
- The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
|
|
217
218
|
- A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
|
|
218
219
|
|
|
@@ -246,6 +247,17 @@ declares every tool (full). The file stores only the name: a kit's tools and pro
|
|
|
246
247
|
`/toolkit` changes it, saved at once, from the next turn's start. See
|
|
247
248
|
[Toolkits](configuration.md#toolkits).
|
|
248
249
|
|
|
250
|
+
## Parent answers
|
|
251
|
+
|
|
252
|
+
A session file's `parent_answers` (`hard`, `standard`, `soft`, or `null` for none) is the session's own
|
|
253
|
+
`guardrails.parent_answers`: what a parent agent (`chi answer`, a relayed answer, a piped or marked attach) may
|
|
254
|
+
answer on its questions. It may only tighten config.yml's: the stricter of the two applies, in the worker's check and
|
|
255
|
+
in `chi answer`'s, and in what `chi send --wait`, `delegate_result` and the delegate reports tell a parent it may pick.
|
|
256
|
+
It is set at creation (`chi --parent-answers`, `chi send --new --parent-answers`); a respawned worker, a continue and a
|
|
257
|
+
fork keep it, and a `delegate` child starts with its parent's as it is then (a later change on the parent doesn't reach
|
|
258
|
+
it). A value that isn't a level reads as `hard`. `/parent-answers` changes it; loosening it is yours alone (see
|
|
259
|
+
[Guardrails](guardrails.md#answers-from-a-parent-agent)).
|
|
260
|
+
|
|
249
261
|
## Bundle settings
|
|
250
262
|
|
|
251
263
|
A session file's `bundle_settings` (`{"cost-cap": {"cap": 3}}`, `{}` for none and for a session from before them)
|
|
@@ -265,11 +277,11 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
265
277
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one. After `wait: false` it isn't needed: the report brings the reply.
|
|
266
278
|
- A wait that timed out or was canceled still gets its delegate report: neither moves the parent's cursor, so the child's reply comes as a report when its turn ends. The result says so (`chi brings its reply here by itself as a delegate report …`; with `queue`, that it joins the next step or turn) and that `delegate_result` waits again only if the turn needs the reply now. A parent that gets no reports (`session.delegate_reports: off`, a `--no-shared` REPL, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
|
|
267
279
|
- What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
|
|
268
|
-
- **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `guardrails.parent_answers
|
|
269
|
-
- **A child at its cost cap** (the [cost-cap bundle](plugins.md#the-cost-cap-bundle), the child's own cap: `child:` in its settings, or `/cap`) asks a `hook` question, "Cost cap": a parent waiting on it (`delegate`, `delegate_result`) gets it back as `status: question` with the `chi answer` commands, `--option Continue` or `--option Stop` (under `guardrails.parent_answers: hard
|
|
280
|
+
- **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `guardrails.parent_answers`, or the child's own when stricter (`hard`: Stop only; see [chi as a sub-agent](sub-agent.md#the-step-limit)).
|
|
281
|
+
- **A child at its cost cap** (the [cost-cap bundle](plugins.md#the-cost-cap-bundle), the child's own cap: `child:` in its settings, or `/cap`) asks a `hook` question, "Cost cap": a parent waiting on it (`delegate`, `delegate_result`) gets it back as `status: question` with the `chi answer` commands, `--option Continue` or `--option Stop` (under `guardrails.parent_answers: hard`, config.yml's or the child's own, a parent's Continue is refused: Stop only). A **background** child (`wait: false`) paused at its cap does not wake its parent: delegate reports skip hook questions (they are the user's, on the child's card) and the approval relay carries approvals only, so the child waits on its card, with the web's bell. The parent sees it at its next `delegate_result`.
|
|
270
282
|
- **A child's approvals go to the parent's user** (the approval relay). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
|
|
271
283
|
- The child's own card stays answerable (the first answer wins: answered there, the parent's card closes as "answered in ab12"). While relayed it says `Waiting for approval in parent ab12: answering here works too`; `chi sessions list` shows the child as `waiting (in parent ab12)` (`--format json`: `relayed_to`), and the web's bell rings for the parent's card, not the child's.
|
|
272
|
-
- How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_answers` by the parent and again by the child.
|
|
284
|
+
- How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_answers` (each session's own, when stricter) by the parent and again by the child.
|
|
273
285
|
- A Stop of the parent's turn closes its card; the child's approval stays open there. A child's worker that goes closes the card (`… worker is gone`); a parent's worker that goes clears the child's "waiting in parent" line within 2 s. A later `delegate_result` relays it again.
|
|
274
286
|
- No relay where the parent can't host one (a `--no-shared` REPL, `--non-interactive`) or isn't waiting (`wait: false` until the next `delegate_result`): the approval waits on the child, with the web's bell, and the result's question block says to deny it or leave it for the user. The model's own questions and hooks' questions come back to the model as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
|
|
275
287
|
- **Delegate reports: a child's reply comes back by itself.** When a child ends a turn that was its parent's (its task, a follow-up, a continue the parent answered, a reminder: not a turn its user typed into it directly), it rings the parent: a small file in the parent's `children/` dir, written after the child's turn is saved. So does a question the parent's model decides (the child's own `ask_user_question`, the step-limit continue; not an approval or a hook's question, which the user answers on the child's card), the turn after such a question whoever starts it, and the child's worker crashing. The parent reads the child as `delegate_result` would and hands its model a **report**: the same text (`session:`, `status:`, the reply), under its own header (`[Delegate report, delivered by chi …; not your user's message. Tell your user what it found …]`), as an input message with `source: "delegate_report"`.
|
data/docs/sub-agent.md
CHANGED
|
@@ -99,7 +99,8 @@ question and starts a new turn. It can't be dismissed.
|
|
|
99
99
|
A parent may Continue by default: Continue grants no permission (every tool
|
|
100
100
|
call still meets the guardrails), it only spends time and tokens. With
|
|
101
101
|
`guardrails.parent_answers: hard` in chi's config.yml (config.yml only, no
|
|
102
|
-
environment variable
|
|
102
|
+
environment variable; a session's own `parent_answers` may tighten it, see
|
|
103
|
+
[Guardrails](guardrails.md#a-sessions-own-level)) a parent may only Stop: `chi answer` and the worker
|
|
103
104
|
refuse a Continue (exit 1, `a parent agent may only stop this turn here`), the
|
|
104
105
|
question block's command is the Stop, and the user continues it from the web or
|
|
105
106
|
the terminal. See [Guardrails](guardrails.md#answers-from-a-parent-agent).
|
|
@@ -126,7 +127,8 @@ the relay (a `--no-shared` REPL, `--non-interactive`).
|
|
|
126
127
|
An approval is a guardrail rule with `verdict: ask`: its user said "ask me". A
|
|
127
128
|
parent may always deny it (`--option Deny --text "why"`, `--text` alone, or
|
|
128
129
|
`--dismiss`). By default (`guardrails.parent_answers: soft` in chi's
|
|
129
|
-
config.yml, which has no environment variable
|
|
130
|
+
config.yml, which has no environment variable, unless the session's own
|
|
131
|
+
`parent_answers` is stricter) it may also give "Allow once";
|
|
130
132
|
the wider scopes never go, and neither does any allow on chi's own config,
|
|
131
133
|
hooks or guardrail rules: `chi answer` refuses those with exit 1 and a line
|
|
132
134
|
like `only Allow once (guardrails.parent_answers: soft) can be given here: deny
|
|
@@ -212,7 +214,8 @@ Start a task: `chi send --new --wait --format json --timeout 500 -m "<task>"`
|
|
|
212
214
|
JSON. If you won't answer, add `--dismiss` instead: chi does nothing it asked
|
|
213
215
|
about, finishes with what it found, and the reply comes back (exit 0).
|
|
214
216
|
- `question.kind: approval`: chi wants to run something its user's guardrails
|
|
215
|
-
flagged. What you may give follows chi's `guardrails.parent_answers
|
|
217
|
+
flagged. What you may give follows chi's `guardrails.parent_answers` (or the
|
|
218
|
+
session's own, stricter `parent_answers`), and the
|
|
216
219
|
question says it: when its `allowed_options` (and the text block's `allow:`
|
|
217
220
|
line) list "Allow once" (`soft`, the default), you may allow that one call if
|
|
218
221
|
your task clearly needs it; never a wider scope (this session, a repo, a
|
|
@@ -122,7 +122,7 @@ module Samagotchi
|
|
|
122
122
|
return failure(not_dismissable(pending)) if options[:dismiss] && ParentReport.continue?(pending)
|
|
123
123
|
|
|
124
124
|
selected = selection(pending, options) or return @selection_status
|
|
125
|
-
refusal = parent_refusal(pending, selected)
|
|
125
|
+
refusal = parent_refusal(pending, selected, session)
|
|
126
126
|
return failure(refusal) if refusal
|
|
127
127
|
|
|
128
128
|
posted = post(id, qid, selected, options)
|
|
@@ -160,13 +160,13 @@ module Samagotchi
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Why a parent may not give this answer, or nil (Guardrails::ParentPolicy,
|
|
163
|
-
# with this process's config.yml
|
|
164
|
-
#
|
|
165
|
-
# with its own
|
|
166
|
-
def parent_refusal(pending, selected)
|
|
163
|
+
# with this process's config.yml tightened by the session file's own
|
|
164
|
+
# parent_answers, and the question's own tags). A deny, text alone or a
|
|
165
|
+
# dismissal always passes. The worker checks it again with its own.
|
|
166
|
+
def parent_refusal(pending, selected, session)
|
|
167
167
|
offered = Array(pending.options).map(&:to_s)
|
|
168
168
|
indices = selected.map { |label| offered.index(label) }
|
|
169
|
-
strategy = Guardrails::ParentPolicy.
|
|
169
|
+
strategy = Guardrails::ParentPolicy.for_session(session)
|
|
170
170
|
reason = Guardrails::ParentPolicy.refusal(pending, indices, strategy: strategy)
|
|
171
171
|
reason && Guardrails::ParentPolicy.message(reason, pending: pending, strategy: strategy)
|
|
172
172
|
end
|
|
@@ -22,9 +22,9 @@ class KnownNames
|
|
|
22
22
|
DERIVATIONS = %w[home user git repo].freeze
|
|
23
23
|
DEFAULT_MIN_LENGTH = 6
|
|
24
24
|
# Splits a command or path into tokens; each token's [-_.] parts are
|
|
25
|
-
# candidates too. "~"
|
|
26
|
-
# after it is what is checked.
|
|
27
|
-
TOKEN_SPLIT = %r{[/\s"'=:,;|&()
|
|
25
|
+
# candidates too. "#" and "~" split: in "~name" (that user's home) the
|
|
26
|
+
# name after it is what is checked.
|
|
27
|
+
TOKEN_SPLIT = %r{[/\s"'=:,;|&()<>@~#]+}
|
|
28
28
|
PART_SPLIT = /[-_.]+/
|
|
29
29
|
# A candidate with a shell glob character is not checked: `workato*`
|
|
30
30
|
# is a pattern over the name, not a misspelling of it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: known-names
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.6
|
|
4
4
|
scope: system
|
|
5
5
|
description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
|
|
6
6
|
trust_level: reviewed
|
|
@@ -8,7 +8,7 @@ files:
|
|
|
8
8
|
known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
|
|
9
9
|
hooks:
|
|
10
10
|
known_names.rb:
|
|
11
|
-
sha256: sha256:
|
|
11
|
+
sha256: sha256:7743adea25655d019d308793e7616043fc29e3d664e1572b50d0b27bf3a9238f
|
|
12
12
|
event: before_tool_call
|
|
13
13
|
on_error: log
|
|
14
14
|
priority: 50
|
|
@@ -76,8 +76,10 @@ module Samagotchi
|
|
|
76
76
|
result.turn_id = report_turn_id if report_turn_id && !report_turn_id.to_s.empty?
|
|
77
77
|
result.text = result.text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
78
78
|
@reported = true
|
|
79
|
+
# What a parent may pick there: config.yml tightened by the session's own.
|
|
80
|
+
strategy = ParentReport.strategy_for(id, state_dir: @state_dir)
|
|
79
81
|
ParentReport.report(result, session_id: id, stdout: @stdout, stderr: @stderr, command: command_name,
|
|
80
|
-
json: @json, timeout: timeout, extra: report_extra)
|
|
82
|
+
json: @json, timeout: timeout, extra: report_extra, strategy: strategy)
|
|
81
83
|
rescue Interrupt
|
|
82
84
|
if @json
|
|
83
85
|
@stdout.puts(ParentReport.json_line(ReplyWait::Result.new(status: :canceled, turn_id: report_turn_id), session_id: id,
|
|
@@ -17,7 +17,9 @@ module Samagotchi
|
|
|
17
17
|
# but eager parent, not a security boundary.
|
|
18
18
|
#
|
|
19
19
|
# Strategies (guardrails.parent_answers, config.yml only): hard = answer
|
|
20
|
-
# and deny; standard = + spend; soft (the default) = + permit.
|
|
20
|
+
# and deny; standard = + spend; soft (the default) = + permit. A session
|
|
21
|
+
# may carry its own, stricter one (Session#parent_answers): config.yml
|
|
22
|
+
# is the ceiling (#effective).
|
|
21
23
|
#
|
|
22
24
|
# Effects: answer (an untagged option), deny, spend (more time and
|
|
23
25
|
# tokens: Continue), permit (one call: Allow once), permit_wide (more
|
|
@@ -45,16 +47,23 @@ module Samagotchi
|
|
|
45
47
|
CONTINUE_QUESTION = PendingQuestion.new(kind: CONTINUE_KIND, options: [CONTINUE, STOP], effects: [SPEND, DENY])
|
|
46
48
|
|
|
47
49
|
KEY = "guardrails.parent_answers"
|
|
50
|
+
# A session's own value (Session#parent_answers): it may only tighten KEY.
|
|
51
|
+
SESSION_KEY = "parent_answers"
|
|
48
52
|
HARD = "hard"
|
|
49
53
|
STANDARD = "standard"
|
|
50
54
|
SOFT = "soft"
|
|
51
55
|
|
|
52
|
-
# A strategy: its name
|
|
53
|
-
|
|
56
|
+
# A strategy: its name, the effects a parent may pick under it, and
|
|
57
|
+
# where it comes from: :config (config.yml's) or :session (a
|
|
58
|
+
# session's own, stricter one: #effective).
|
|
59
|
+
Strategy = Data.define(:name, :allowed, :origin) do
|
|
60
|
+
def initialize(name:, allowed:, origin: :config) = super
|
|
61
|
+
|
|
54
62
|
def allow?(effect) = allowed.include?(effect.to_s)
|
|
55
63
|
|
|
56
|
-
# How a refusal names the setting: "guardrails.parent_answers: hard"
|
|
57
|
-
|
|
64
|
+
# How a refusal names the setting: "guardrails.parent_answers: hard",
|
|
65
|
+
# or "parent_answers: hard (this session)" when the session's binds.
|
|
66
|
+
def label = origin == :session ? "#{SESSION_KEY}: #{name} (this session)" : "#{KEY}: #{name}"
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
STRATEGIES = {
|
|
@@ -74,6 +83,66 @@ module Samagotchi
|
|
|
74
83
|
# @return [Strategy]
|
|
75
84
|
def strategy(name) = STRATEGIES.fetch(name.to_s, STRATEGIES[HARD])
|
|
76
85
|
|
|
86
|
+
# Strictest first: a session's own level may only move left of
|
|
87
|
+
# config.yml's.
|
|
88
|
+
ORDER = [HARD, STANDARD, SOFT].freeze
|
|
89
|
+
# A session value that means "none of its own" (config's).
|
|
90
|
+
UNSET = "default"
|
|
91
|
+
|
|
92
|
+
# The stricter of two strategies, by name; a tie keeps +a+.
|
|
93
|
+
# @return [Strategy]
|
|
94
|
+
def stricter(a, b) = ORDER.index(b.name) < ORDER.index(a.name) ? b : a
|
|
95
|
+
|
|
96
|
+
# A session's own value as a level: nil when it has none (nil, "",
|
|
97
|
+
# "default"), the level, or hard for anything else (a hand-edited
|
|
98
|
+
# file: the strictest, as config's invalid value; not logged, as this
|
|
99
|
+
# is read on every parent answer).
|
|
100
|
+
# @return [String, nil]
|
|
101
|
+
def session_level(value)
|
|
102
|
+
word = value.to_s.strip.downcase
|
|
103
|
+
return nil if word.empty? || word == UNSET
|
|
104
|
+
|
|
105
|
+
STRATEGIES.key?(word) ? word : HARD
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# What a parent may answer on a session whose own value is
|
|
109
|
+
# +session_value+: config.yml's (+config+) unless the session's is
|
|
110
|
+
# stricter. Config's own object when it binds, so its label stays.
|
|
111
|
+
# @return [Strategy]
|
|
112
|
+
def effective(session_value, config: current)
|
|
113
|
+
level = session_level(session_value)
|
|
114
|
+
return config if level.nil?
|
|
115
|
+
|
|
116
|
+
own = STRATEGIES[level]
|
|
117
|
+
stricter(config, own).equal?(config) ? config : own.with(origin: :session)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Why a session's own +value+ doesn't apply: "soft is looser than
|
|
121
|
+
# config.yml's guardrails.parent_answers: standard; standard applies";
|
|
122
|
+
# nil when it applies (or is none).
|
|
123
|
+
# @return [String, nil]
|
|
124
|
+
def capped_note(value, config: current)
|
|
125
|
+
level = session_level(value)
|
|
126
|
+
return nil if level.nil? || effective(level, config: config).name == level
|
|
127
|
+
|
|
128
|
+
"#{level} is looser than config.yml's #{config.label}; #{config.name} applies"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# #effective with +session+'s own value (a Session; nil: config's).
|
|
132
|
+
# @return [Strategy]
|
|
133
|
+
def for_session(session) = effective(session&.parent_answers)
|
|
134
|
+
|
|
135
|
+
# Whether setting a session's value +from+ → +to+ loosens it: unset
|
|
136
|
+
# (config's) is the loosest, an invalid value hard. Only the user
|
|
137
|
+
# loosens (/parent-answers in a UI).
|
|
138
|
+
def looser?(from, to) = session_rank(to) > session_rank(from)
|
|
139
|
+
|
|
140
|
+
# hard 0, standard 1, soft 2, unset 3.
|
|
141
|
+
def session_rank(value)
|
|
142
|
+
level = session_level(value)
|
|
143
|
+
level.nil? ? ORDER.size : ORDER.index(level)
|
|
144
|
+
end
|
|
145
|
+
|
|
77
146
|
# @return [Array<String>] the effects +strategy+ lets a parent pick
|
|
78
147
|
def allowed_effects(strategy) = strategy.allowed
|
|
79
148
|
|