samagotchi 0.134.0 → 0.135.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 +49 -1
- data/bin/chi +2 -3
- data/docs/architecture.md +6 -3
- data/docs/cli.md +39 -34
- data/docs/configuration.md +1 -2
- data/docs/guardrails.md +53 -9
- data/docs/internals/engine.md +6 -3
- data/docs/internals/participants.md +1 -1
- data/docs/internals/web-frontend.md +262 -0
- data/docs/sessions.md +2 -0
- data/docs/testing.md +27 -2
- data/lib/samagotchi/bridge.rb +11 -6
- data/lib/samagotchi/bridge_client.rb +8 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +0 -2
- data/lib/samagotchi/engine.rb +69 -12
- data/lib/samagotchi/guardrail_wiring.rb +8 -6
- data/lib/samagotchi/guardrails/approval.rb +41 -5
- data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
- data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
- data/lib/samagotchi/guardrails/pair_command.rb +387 -78
- data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
- data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
- data/lib/samagotchi/guardrails/shared_git.rb +1 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
- data/lib/samagotchi/guardrails_command.rb +13 -10
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/message_parts.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +33 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager.rb +13 -11
- data/lib/samagotchi/system_one/approval_question.rb +21 -16
- data/lib/samagotchi/system_one/self_protect.rb +13 -15
- data/lib/samagotchi/tools/delegate_relay.rb +6 -5
- data/lib/samagotchi/tools/delegate_wait.rb +5 -5
- data/lib/samagotchi/tools/execute.rb +6 -6
- data/lib/samagotchi/tools/output_guardrails.rb +9 -2
- data/lib/samagotchi/tools/read.rb +5 -5
- data/lib/samagotchi/turn_anchors.rb +49 -0
- data/lib/samagotchi/verify_anchor.rb +5 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -14
- data/lib/samagotchi/web/message_display.rb +17 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_view.rb +8 -0
- data/lib/samagotchi/web/public/activity.js +1 -1
- data/lib/samagotchi/web/public/annotations.js +1 -1
- data/lib/samagotchi/web/public/answer_fold.js +3 -3
- data/lib/samagotchi/web/public/app.js +447 -340
- data/lib/samagotchi/web/public/bubbles.js +3 -3
- data/lib/samagotchi/web/public/cards_place.js +4 -2
- data/lib/samagotchi/web/public/composer.css +8 -0
- data/lib/samagotchi/web/public/context_chips.js +9 -6
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/dock_reserve.js +13 -15
- data/lib/samagotchi/web/public/feature_gates.js +33 -0
- data/lib/samagotchi/web/public/history_reveal.js +8 -4
- data/lib/samagotchi/web/public/index.html +38 -9
- data/lib/samagotchi/web/public/latest.js +46 -0
- data/lib/samagotchi/web/public/lists_view.js +15 -13
- data/lib/samagotchi/web/public/live_timing.js +2 -0
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/method_api.js +14 -0
- data/lib/samagotchi/web/public/notifications.js +4 -11
- data/lib/samagotchi/web/public/participant.js +11 -6
- data/lib/samagotchi/web/public/participant_names.js +1 -1
- data/lib/samagotchi/web/public/participants_panel.js +4 -2
- data/lib/samagotchi/web/public/project_picker.js +18 -22
- data/lib/samagotchi/web/public/question_cards.js +2 -2
- data/lib/samagotchi/web/public/restore.js +2 -0
- data/lib/samagotchi/web/public/revise_stack.js +372 -0
- data/lib/samagotchi/web/public/scope.js +12 -20
- data/lib/samagotchi/web/public/send_turn.js +29 -16
- data/lib/samagotchi/web/public/session_actions.js +15 -14
- data/lib/samagotchi/web/public/session_fields.js +152 -0
- data/lib/samagotchi/web/public/session_payload.js +35 -0
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/session_store.js +191 -0
- data/lib/samagotchi/web/public/stage_model.js +4 -4
- data/lib/samagotchi/web/public/stage_view.js +11 -16
- data/lib/samagotchi/web/public/stream_handlers.js +88 -90
- data/lib/samagotchi/web/public/task_stop.js +1 -1
- data/lib/samagotchi/web/public/timing.js +2 -2
- data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +27 -92
- data/lib/samagotchi/web/public/turn_events.js +2 -2
- data/lib/samagotchi/web/public/turn_html.js +5 -3
- data/lib/samagotchi/web/public/turn_model.js +6 -6
- data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +3 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/web/session_control.rb +2 -2
- data/lib/samagotchi/worker.rb +9 -9
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +12 -3
- data/lib/samagotchi/web/public/hold.js +0 -78
|
@@ -330,9 +330,10 @@ module Samagotchi
|
|
|
330
330
|
return verdict
|
|
331
331
|
end
|
|
332
332
|
|
|
333
|
-
|
|
333
|
+
# The card and the session file carry the facts as the hash they were.
|
|
334
|
+
answer = @ask.call(payload.merge(approval: payload[:approval].to_h))
|
|
334
335
|
system_one_label(judgment, answer, payload)
|
|
335
|
-
Guardrails::Approval.settle(verdict, answer, payload[:approval]
|
|
336
|
+
Guardrails::Approval.settle(verdict, answer, payload[:approval].scopes)
|
|
336
337
|
end
|
|
337
338
|
|
|
338
339
|
private
|
|
@@ -391,9 +392,9 @@ module Samagotchi
|
|
|
391
392
|
decision = system_one_decision(client, auto)
|
|
392
393
|
|
|
393
394
|
id = log.judgment(session_id: @session_lookup.call&.id,
|
|
394
|
-
rule: approval
|
|
395
|
+
rule: approval.rule, source: approval.source, tool: approval.tool,
|
|
395
396
|
**SystemOne::ApprovalQuestion.call_fields(approval),
|
|
396
|
-
repo_name: approval
|
|
397
|
+
repo_name: approval.repo_name,
|
|
397
398
|
p: p, threshold: Config.get("guardrails.system_one.threshold"), model: client.model,
|
|
398
399
|
latency_ms: latency_ms, decision: decision, origin: "live")
|
|
399
400
|
# A judgment the log cannot hold is no judgment: the user is asked.
|
|
@@ -418,12 +419,13 @@ module Samagotchi
|
|
|
418
419
|
# user opted in, the never-list and chi's own files, the probability, and
|
|
419
420
|
# the rule's record. Any one that fails leaves the ask to the user. The
|
|
420
421
|
# entries are the judgment log read once for this ask.
|
|
422
|
+
# @param approval [Guardrails::ApprovalFacts] the ask's facts
|
|
421
423
|
# @param entries [Array<Hash>] SystemOne::Log#entries
|
|
422
424
|
# @param targets [Guardrails::Targets, nil] the call's, for its env:
|
|
423
425
|
def system_one_auto_allow?(client, approval, p, entries, targets: nil)
|
|
424
426
|
return false unless client.mode == "on"
|
|
425
427
|
|
|
426
|
-
rule = approval
|
|
428
|
+
rule = approval.rule.to_s
|
|
427
429
|
return false if rule.empty?
|
|
428
430
|
return false unless system_one_rules.include?(rule)
|
|
429
431
|
return false if SystemOne::SelfProtect.refuse?(approval, targets: targets)
|
|
@@ -456,7 +458,7 @@ module Samagotchi
|
|
|
456
458
|
id = judgment && judgment[:id]
|
|
457
459
|
return nil if id.nil?
|
|
458
460
|
|
|
459
|
-
scopes = Array(payload[:approval]
|
|
461
|
+
scopes = Array(payload[:approval].scopes)
|
|
460
462
|
if !answer.is_a?(Samagotchi::QuestionAnswer)
|
|
461
463
|
label = "none"
|
|
462
464
|
elsif answer.parent_agent?
|
|
@@ -6,6 +6,41 @@ require_relative "../question_answer"
|
|
|
6
6
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
module Guardrails
|
|
9
|
+
# An ask's facts, as Approval.payload builds them (its approval:) and
|
|
10
|
+
# the question card shows them: the tool (tool, acts_as, label), what
|
|
11
|
+
# it acts on (command, paths, args, memory), where (cwd, repo_root,
|
|
12
|
+
# repo_name, branch), who asked and why (rule, source, reason), the
|
|
13
|
+
# offered scopes, an edit's preview (EditPreview.for) and, for a
|
|
14
|
+
# participant's turn, who asks (author, author_name, asked_by).
|
|
15
|
+
#
|
|
16
|
+
# In this process it is passed as this value; it crosses to the UIs and
|
|
17
|
+
# the session file (PendingQuestion's approval) as #to_h, the hash it
|
|
18
|
+
# always was: symbol keys, in member order, unset fields left out. A
|
|
19
|
+
# saved or sent one (string or symbol keys, missing fields, a key we
|
|
20
|
+
# don't know dropped) comes back through .from_h. memory and preview
|
|
21
|
+
# stay as given (string keys inside after a load).
|
|
22
|
+
ApprovalFacts = Data.define(:tool, :acts_as, :label, :command, :paths, :args, :memory, :cwd, :repo_root,
|
|
23
|
+
:repo_name, :branch, :rule, :source, :reason, :scopes, :preview, :author,
|
|
24
|
+
:author_name, :asked_by) do
|
|
25
|
+
def initialize(**fields)
|
|
26
|
+
super(**members.to_h { |m| [m, nil] }, **fields)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param raw [ApprovalFacts, Hash, nil] facts as built, saved or sent
|
|
30
|
+
# @return [ApprovalFacts, nil] nil for anything that is not facts
|
|
31
|
+
def self.from_h(raw)
|
|
32
|
+
return raw if raw.is_a?(ApprovalFacts)
|
|
33
|
+
return nil unless raw.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
new(**members.to_h { |m| [m, raw.key?(m) ? raw[m] : raw[m.to_s]] })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The card's and the session file's shape: symbol keys, in member
|
|
39
|
+
# order, unset fields left out.
|
|
40
|
+
# @return [Hash{Symbol => Object}]
|
|
41
|
+
def to_h = super.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
9
44
|
# An ask as a question for the user (Engine#open_question), and the
|
|
10
45
|
# answer back as a verdict. The question text is plain and complete, so
|
|
11
46
|
# a UI that doesn't know approvals still shows everything; `approval:`
|
|
@@ -42,9 +77,10 @@ module Samagotchi
|
|
|
42
77
|
# their turn asks: it offers once only (PARTICIPANT_SCOPES), and the
|
|
43
78
|
# card names them (approval author:, author_name:, the header)
|
|
44
79
|
# @param author_name [String, nil] their name (participants.json)
|
|
45
|
-
# @return [Hash] open_question fields
|
|
46
|
-
# (
|
|
47
|
-
#
|
|
80
|
+
# @return [Hash] open_question fields, with approval: an ApprovalFacts
|
|
81
|
+
# (its caller hands open_question approval.to_h); the caller adds
|
|
82
|
+
# user_only: (ParentApprovals.protected_facts? on approval:, a file
|
|
83
|
+
# that requires this one)
|
|
48
84
|
def payload(verdict, label: nil, preview: nil, only: nil, author: nil, author_name: nil)
|
|
49
85
|
asker = asker(author, author_name)
|
|
50
86
|
only = PARTICIPANT_SCOPES if asker
|
|
@@ -60,7 +96,7 @@ module Samagotchi
|
|
|
60
96
|
allow_freeform: true,
|
|
61
97
|
effects: effects(scopes),
|
|
62
98
|
kind: KIND,
|
|
63
|
-
approval:
|
|
99
|
+
approval: ApprovalFacts.new(
|
|
64
100
|
tool: targets&.tool || verdict.call[:name].to_s,
|
|
65
101
|
# The tool the call acts as (an MCP tool behind call_tool).
|
|
66
102
|
acts_as: targets&.acts_as,
|
|
@@ -86,7 +122,7 @@ module Samagotchi
|
|
|
86
122
|
author: (author.to_s if asker),
|
|
87
123
|
author_name: (author_name_for(author_name) if asker),
|
|
88
124
|
asked_by: (format(ASKED_BY, asker) if asker)
|
|
89
|
-
|
|
125
|
+
)
|
|
90
126
|
}
|
|
91
127
|
end
|
|
92
128
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "shell_lex"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
module Guardrails
|
|
5
7
|
# Programs that write outside the folder they run in with no word of
|
|
@@ -61,9 +63,10 @@ module Samagotchi
|
|
|
61
63
|
"gh" => [[%w[auth], %w[login logout refresh setup-git switch]], [%w[config], %w[set]],
|
|
62
64
|
[%w[extension extensions ext], %w[install upgrade remove]], [%w[alias], %w[set delete import]]],
|
|
63
65
|
# the global config and a launchd/cron schedule; git lfs's global filters
|
|
64
|
-
|
|
66
|
+
# (git's first group is its subcommand: .git_outside?)
|
|
67
|
+
"git" => [[%w[maintenance], %w[register start unregister stop]], [%w[lfs], %w[install uninstall update]]],
|
|
65
68
|
"git-maintenance" => [[%w[register start unregister stop]]],
|
|
66
|
-
"git-lfs" => [[%w[install uninstall]]],
|
|
69
|
+
"git-lfs" => [[%w[install uninstall update]]],
|
|
67
70
|
"scalar" => [[%w[register unregister clone reconfigure delete]]]
|
|
68
71
|
}.freeze
|
|
69
72
|
# program => the verbs (its first argument) that only read: every
|
|
@@ -89,9 +92,9 @@ module Samagotchi
|
|
|
89
92
|
|
|
90
93
|
module_function
|
|
91
94
|
|
|
92
|
-
# The table's name for +program+ (
|
|
95
|
+
# The table's name for +program+ (its word's ShellLex.program_name).
|
|
93
96
|
def name(program)
|
|
94
|
-
base =
|
|
97
|
+
base = ShellLex.program_name(program)
|
|
95
98
|
ALIASES.each { |pattern, to| return to if base.match?(pattern) }
|
|
96
99
|
base
|
|
97
100
|
end
|
|
@@ -108,6 +111,7 @@ module Samagotchi
|
|
|
108
111
|
return bundle_config_outside?(args) if program == "bundle" && args.first == "config"
|
|
109
112
|
|
|
110
113
|
entries = BY_VERB[program] or return false
|
|
114
|
+
return git_outside?(entries, args, assigned) if program == "git"
|
|
111
115
|
return false unless entries.any? { |groups| groups.all? { |group| group?(group, args, assigned) } }
|
|
112
116
|
return true unless LOCAL_INSTALLERS.include?(program)
|
|
113
117
|
return true if args.include?("--user")
|
|
@@ -115,6 +119,17 @@ module Samagotchi
|
|
|
115
119
|
!(local || args.any? { |arg| arg.match?(PLACE_OPTIONS) })
|
|
116
120
|
end
|
|
117
121
|
|
|
122
|
+
# git's entries match from its subcommand on (PairCommand.git_command_starts:
|
|
123
|
+
# git -C . maintenance start), the first group holding the subcommand,
|
|
124
|
+
# not its words anywhere (git add maintenance start, git commit -m lfs -m install).
|
|
125
|
+
def git_outside?(entries, args, assigned)
|
|
126
|
+
PairCommand.git_command_starts(args).grep(Integer).any? do |i|
|
|
127
|
+
entries.any? do |first, *rest|
|
|
128
|
+
first.include?(args[i]) && rest.all? { |group| group?(group, args[(i + 1)..], assigned) }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
118
133
|
def group?(group, args, assigned)
|
|
119
134
|
group == :global ? global?(args, assigned) : args.intersect?(group)
|
|
120
135
|
end
|
|
@@ -97,6 +97,8 @@ module Samagotchi
|
|
|
97
97
|
"owner's cookies"
|
|
98
98
|
LISTENER = "a participant's turn can't listen on a port: what it serves on this machine could catch the " \
|
|
99
99
|
"owner's browser cookies"
|
|
100
|
+
NET = "a participant's turn runs a command chi can't read for sure (git or chi behind a program it doesn't " \
|
|
101
|
+
"know, or a $(…) kept in a value the shell expands later), so the owner approves it"
|
|
100
102
|
VERB = "a participant's turn runs only a program named plainly: no $, $(…) or glob in a command's name"
|
|
101
103
|
UNREADABLE = "a participant's turn runs only commands chi can read: split this command, or write the file " \
|
|
102
104
|
"with the write tool"
|
|
@@ -195,13 +197,15 @@ module Samagotchi
|
|
|
195
197
|
|
|
196
198
|
# Why the owner is asked (fresh, once): a command that makes a link
|
|
197
199
|
# (LINK), one that runs mise or direnv (ENV_LOADER), a write to mise's
|
|
198
|
-
# config (MISE_CONFIG), a
|
|
200
|
+
# config (MISE_CONFIG), a command chi can't read for sure (NET:
|
|
201
|
+
# PairCommand#net_ask?), a write that may land outside the project (OUTSIDE_WRITE, or
|
|
199
202
|
# NO_PROJECT without one); nil when nothing asks.
|
|
200
203
|
def ask_reason(verdict)
|
|
201
204
|
targets = verdict.targets
|
|
202
205
|
return LINK if targets&.command && command(targets, nil).links?
|
|
203
206
|
return ENV_LOADER if targets&.command && command(targets, nil).loads_env?
|
|
204
207
|
return MISE_CONFIG if mise_config_write?(verdict)
|
|
208
|
+
return NET if targets&.command && command(targets, nil).net_ask?
|
|
205
209
|
return nil unless outside_write?(verdict)
|
|
206
210
|
|
|
207
211
|
project_root ? OUTSIDE_WRITE : NO_PROJECT
|