samagotchi 0.133.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 +116 -1
- data/bin/chi +2 -3
- data/docs/architecture.md +6 -3
- data/docs/cli.md +50 -34
- data/docs/configuration.md +9 -6
- data/docs/guardrails.md +146 -13
- data/docs/internals/background-tasks.md +1 -0
- data/docs/internals/engine.md +6 -3
- data/docs/internals/participants.md +7 -6
- data/docs/internals/tool-guardrails.md +2 -0
- data/docs/internals/web-frontend.md +262 -0
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +5 -2
- data/docs/testing.md +34 -4
- 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/chi_home.rb +12 -2
- data/lib/samagotchi/config.rb +0 -2
- data/lib/samagotchi/desktop/macos.rb +19 -4
- data/lib/samagotchi/engine.rb +82 -12
- data/lib/samagotchi/guardrail_wiring.rb +8 -6
- data/lib/samagotchi/guardrails/approval.rb +41 -5
- data/lib/samagotchi/guardrails/implicit_writers.rb +158 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +11 -5
- data/lib/samagotchi/guardrails/pair_command.rb +828 -86
- data/lib/samagotchi/guardrails/parent_approvals.rb +13 -16
- data/lib/samagotchi/guardrails/read_only_shell.rb +58 -4
- data/lib/samagotchi/guardrails/shared_git.rb +1 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +7 -2
- data/lib/samagotchi/guardrails/shell_lex.rb +45 -0
- data/lib/samagotchi/guardrails/shell_paths.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +8 -3
- data/lib/samagotchi/guardrails_command.rb +13 -10
- data/lib/samagotchi/home_command.rb +22 -6
- data/lib/samagotchi/home_doctor.rb +36 -3
- data/lib/samagotchi/home_notice.rb +8 -1
- data/lib/samagotchi/hooks/loader.rb +5 -0
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/log_line.rb +12 -0
- data/lib/samagotchi/message_parts.rb +1 -1
- data/lib/samagotchi/project_env.rb +3 -11
- data/lib/samagotchi/revise_anchor.rb +33 -0
- data/lib/samagotchi/self_report.rb +3 -1
- 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 +70 -16
- data/lib/samagotchi/testing/waiting.rb +13 -0
- data/lib/samagotchi/toolkit_binding.rb +16 -0
- 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/update_command.rb +7 -3
- data/lib/samagotchi/update_hint.rb +6 -1
- 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 +262 -0
- data/lib/samagotchi/web/public/answer_refs.js +2 -0
- data/lib/samagotchi/web/public/app.js +462 -338
- data/lib/samagotchi/web/public/bubbles.js +3 -3
- data/lib/samagotchi/web/public/cards_place.js +9 -4
- data/lib/samagotchi/web/public/chip_popover.js +73 -11
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/composer.css +11 -1
- 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 +27 -0
- data/lib/samagotchi/web/public/index.html +82 -37
- 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/llm_context_chip.js +5 -13
- 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/refs.js +6 -7
- data/lib/samagotchi/web/public/restore.js +2 -0
- data/lib/samagotchi/web/public/review_start.js +1 -1
- 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/start_page.js +4 -4
- data/lib/samagotchi/web/public/start_settings.js +29 -18
- data/lib/samagotchi/web/public/start_toolkit.js +5 -4
- 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 +7 -3
- data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +46 -101
- data/lib/samagotchi/web/public/turn_events.js +2 -2
- data/lib/samagotchi/web/public/turn_html.js +17 -9
- data/lib/samagotchi/web/public/turn_model.js +6 -6
- data/lib/samagotchi/web/public/turn_range.js +32 -0
- data/lib/samagotchi/web/public/verdict_card.js +5 -13
- data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
- data/lib/samagotchi/web/public/verify_target.js +7 -10
- 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 +16 -3
- data/lib/samagotchi/web/public/hold.js +0 -78
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shell_lex"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
module Guardrails
|
|
7
|
+
# Programs that write outside the folder they run in with no word of
|
|
8
|
+
# the command naming where: a global install (npm -g, gem install, brew,
|
|
9
|
+
# cargo/go install, pipx, uv tool), a user config file (npm config set
|
|
10
|
+
# writes ~/.npmrc, gh config set ~/.config/gh, bundle config set), the
|
|
11
|
+
# system's defaults, a launchd job, a tool manager's global version, git
|
|
12
|
+
# maintenance's schedule and git lfs's global filters. A pair turn's "a
|
|
13
|
+
# write outside the project always asks" counts them as outside writes
|
|
14
|
+
# (PairCommand#outside_writes?). The table holds examples, not a closed
|
|
15
|
+
# set: a program it doesn't know runs unasked.
|
|
16
|
+
#
|
|
17
|
+
# pip counts with or without --user: it writes into its interpreter's
|
|
18
|
+
# site-packages, which no word names and which a plain `pip` (the
|
|
19
|
+
# worker's PATH, any VIRTUAL_ENV it inherited) may place anywhere. A pip
|
|
20
|
+
# or python named by a path into the project (.venv/bin/pip) installs
|
|
21
|
+
# into the project, and -t/--target/--prefix/--root name where it
|
|
22
|
+
# writes (their value is checked as any word): those don't count
|
|
23
|
+
# (PairCommand checks the program's path: LOCAL_INSTALLERS).
|
|
24
|
+
#
|
|
25
|
+
# Not counted: a project install (npm install, bundle install, uv sync)
|
|
26
|
+
# and the caches every build writes (~/.npm, ~/.cache, go's module
|
|
27
|
+
# cache): they hold downloads, not what runs as the owner.
|
|
28
|
+
module ImplicitWriters
|
|
29
|
+
NPM_CHANGES = %w[install i in add isntall uninstall un unlink remove rm r update up upgrade link ln].freeze
|
|
30
|
+
# program => the word groups of a call that writes outside: its
|
|
31
|
+
# arguments hold a word of every group of one entry (:global: npm's
|
|
32
|
+
# global flag in any spelling, .global?; [] alone: any call).
|
|
33
|
+
BY_VERB = {
|
|
34
|
+
"npm" => [[NPM_CHANGES, :global], [%w[link ln]], [%w[config c], %w[set delete rm edit fix]],
|
|
35
|
+
[%w[set login adduser add-user logout]]],
|
|
36
|
+
"yarn" => [[%w[global]], [%w[link]], [%w[config], %w[set delete unset]]],
|
|
37
|
+
"pnpm" => [[NPM_CHANGES, :global], [%w[config], %w[set delete]], [%w[setup]]],
|
|
38
|
+
"bun" => [[NPM_CHANGES, :global], [%w[link]]],
|
|
39
|
+
"pip" => [[%w[install uninstall]], [%w[config], %w[set unset edit]]],
|
|
40
|
+
"ensurepip" => [[]],
|
|
41
|
+
"easy_install" => [[]],
|
|
42
|
+
"pipenv" => [[%w[install uninstall sync update]]],
|
|
43
|
+
"poetry" => [[%w[self]]],
|
|
44
|
+
"conda" => [[%w[install remove uninstall update upgrade create]]],
|
|
45
|
+
"gem" => [[%w[install i update uninstall cleanup pristine]],
|
|
46
|
+
[%w[sources], %w[-a --add -r --remove -c --clear-all]]],
|
|
47
|
+
"bundle" => [[%w[install], %w[--system]]],
|
|
48
|
+
"defaults" => [[%w[write delete rename import]]],
|
|
49
|
+
"cargo" => [[%w[install uninstall]]],
|
|
50
|
+
"go" => [[%w[install]], [%w[env], %w[-w -u]]],
|
|
51
|
+
"uv" => [[%w[tool], %w[install uninstall upgrade update-shell]], [%w[python], %w[install uninstall]],
|
|
52
|
+
[%w[self], %w[update]], [%w[pip], %w[install uninstall sync], %w[--system]]],
|
|
53
|
+
"deno" => [[%w[install uninstall], :global]],
|
|
54
|
+
"dotnet" => [[%w[tool], %w[install update uninstall], %w[-g --global --tool-path]]],
|
|
55
|
+
"corepack" => [[%w[enable disable]], [%w[install], :global]],
|
|
56
|
+
"pyenv" => [[%w[global install uninstall rehash]]],
|
|
57
|
+
"rbenv" => [[%w[global install uninstall rehash]]],
|
|
58
|
+
"nodenv" => [[%w[global install uninstall rehash]]],
|
|
59
|
+
"asdf" => [[%w[install uninstall global plugin reshim]]],
|
|
60
|
+
"volta" => [[%w[install uninstall setup fetch]]],
|
|
61
|
+
"nvm" => [[%w[install uninstall alias unalias]]],
|
|
62
|
+
"code" => [[%w[--install-extension --uninstall-extension]]],
|
|
63
|
+
"gh" => [[%w[auth], %w[login logout refresh setup-git switch]], [%w[config], %w[set]],
|
|
64
|
+
[%w[extension extensions ext], %w[install upgrade remove]], [%w[alias], %w[set delete import]]],
|
|
65
|
+
# the global config and a launchd/cron schedule; git lfs's global filters
|
|
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]]],
|
|
68
|
+
"git-maintenance" => [[%w[register start unregister stop]]],
|
|
69
|
+
"git-lfs" => [[%w[install uninstall update]]],
|
|
70
|
+
"scalar" => [[%w[register unregister clone reconfigure delete]]]
|
|
71
|
+
}.freeze
|
|
72
|
+
# program => the verbs (its first argument) that only read: every
|
|
73
|
+
# other one writes outside.
|
|
74
|
+
READ_VERBS = {
|
|
75
|
+
"brew" => %w[list ls info search desc deps uses outdated leaves config doctor --prefix --cellar --repository
|
|
76
|
+
--version -v --help -h help],
|
|
77
|
+
"pipx" => %w[list environment --version --help -h],
|
|
78
|
+
"launchctl" => %w[list print print-cache print-disabled blame procinfo hostinfo examine version help],
|
|
79
|
+
"rustup" => %w[show which --version -V help --help -h completions]
|
|
80
|
+
}.freeze
|
|
81
|
+
# Installers whose program named by a path into the project (a
|
|
82
|
+
# project venv's pip) install there, and the options that say where.
|
|
83
|
+
LOCAL_INSTALLERS = %w[pip ensurepip easy_install].freeze
|
|
84
|
+
PLACE_OPTIONS = /\A(?:-t|--target|--prefix|--root)(?:=|\z)/
|
|
85
|
+
# pip3, pip3.12 → pip.
|
|
86
|
+
ALIASES = { /\Apip\d*(?:\.\d+)?\z/ => "pip" }.freeze
|
|
87
|
+
# npm's global flag: -g, --global, --global=true, --location=global
|
|
88
|
+
# (and --location global, two words: .global?).
|
|
89
|
+
GLOBAL = /\A(?:-g|--global|--global=(?!false\z).*|--location=global)\z/
|
|
90
|
+
# npm reads its config from the environment too.
|
|
91
|
+
NPM_ENV_GLOBAL = /\Anpm_config_(?:global|location)\z/i
|
|
92
|
+
|
|
93
|
+
module_function
|
|
94
|
+
|
|
95
|
+
# The table's name for +program+ (its word's ShellLex.program_name).
|
|
96
|
+
def name(program)
|
|
97
|
+
base = ShellLex.program_name(program)
|
|
98
|
+
ALIASES.each { |pattern, to| return to if base.match?(pattern) }
|
|
99
|
+
base
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Whether +program+ (its word) run with +args+ writes outside the
|
|
103
|
+
# folder it runs in with no word naming where. +local+ says the
|
|
104
|
+
# program's word is a path into the project (LOCAL_INSTALLERS);
|
|
105
|
+
# +assigned+ the variables the command sets before it.
|
|
106
|
+
def outside?(program, args, local: false, assigned: {})
|
|
107
|
+
program = name(program)
|
|
108
|
+
if (verbs = READ_VERBS[program])
|
|
109
|
+
return !(args.empty? || verbs.include?(args.first))
|
|
110
|
+
end
|
|
111
|
+
return bundle_config_outside?(args) if program == "bundle" && args.first == "config"
|
|
112
|
+
|
|
113
|
+
entries = BY_VERB[program] or return false
|
|
114
|
+
return git_outside?(entries, args, assigned) if program == "git"
|
|
115
|
+
return false unless entries.any? { |groups| groups.all? { |group| group?(group, args, assigned) } }
|
|
116
|
+
return true unless LOCAL_INSTALLERS.include?(program)
|
|
117
|
+
return true if args.include?("--user")
|
|
118
|
+
|
|
119
|
+
!(local || args.any? { |arg| arg.match?(PLACE_OPTIONS) })
|
|
120
|
+
end
|
|
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
|
+
|
|
133
|
+
def group?(group, args, assigned)
|
|
134
|
+
group == :global ? global?(args, assigned) : args.intersect?(group)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# npm's (pnpm's, bun's, deno's, corepack's) global flag, in any of
|
|
138
|
+
# its spellings or the environment's (npm_config_global=true).
|
|
139
|
+
def global?(args, assigned = {})
|
|
140
|
+
args.any? { |arg| arg.match?(GLOBAL) } || args.each_cons(2).include?(%w[--location global]) ||
|
|
141
|
+
assigned.any? { |key, value| key.match?(NPM_ENV_GLOBAL) && !%w[false local project].include?(value.to_s) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# bundle config set/unset writes ~/.bundle/config unless --local, and
|
|
145
|
+
# so does the old form `bundle config NAME VALUE`; a list or a get
|
|
146
|
+
# (or a NAME alone) only reads.
|
|
147
|
+
def bundle_config_outside?(args)
|
|
148
|
+
rest = args.drop(1)
|
|
149
|
+
return false if rest.include?("--local")
|
|
150
|
+
|
|
151
|
+
words = rest.reject { |arg| arg.start_with?("-") }
|
|
152
|
+
return false if words.empty? || %w[list get].include?(words.first)
|
|
153
|
+
|
|
154
|
+
%w[set unset].include?(words.first) || words.size > 1
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -28,7 +28,7 @@ module Samagotchi
|
|
|
28
28
|
# loopback name, URL openers (open, xdg-open, a browser) and
|
|
29
29
|
# listeners (nc -l, socat …LISTEN, a static HTTP server), background
|
|
30
30
|
# and scheduled jobs, the environment and
|
|
31
|
-
# credential printers, git
|
|
31
|
+
# credential printers, git config writes (reads only), home sweeps
|
|
32
32
|
# and links to them, and a program named by a $var or a glob); a
|
|
33
33
|
# write or edit of git's hooks (GIT_HOOKS);
|
|
34
34
|
# - owner-voiced tools (OWNER_VOICED, and any delegate_* tool): what
|
|
@@ -77,13 +77,15 @@ module Samagotchi
|
|
|
77
77
|
HOME_SWEEP = "a participant's turn can't search, copy or link a folder that holds secrets or chi's own files"
|
|
78
78
|
LINK = "a participant's turn makes a link: a path through it can lead out of the project, so the owner " \
|
|
79
79
|
"approves it"
|
|
80
|
-
GIT_CONFIG = "a participant's turn
|
|
81
|
-
"
|
|
80
|
+
GIT_CONFIG = "a participant's turn writes no git config (git config but its reads, git -c, --config-env, a " \
|
|
81
|
+
"clone's or init's -c/--config, GIT_CONFIG_* and GIT_TEMPLATE_DIR): settings can make git run a " \
|
|
82
|
+
"command in the owner's name"
|
|
82
83
|
ENV_LOADER = "a participant's turn runs mise or direnv: they load the project's env files (a mise.local.toml, " \
|
|
83
84
|
"an .envrc), which may hold the owner's secrets, so the owner approves it"
|
|
84
85
|
MISE_CONFIG = "a participant's turn changes mise's config: the owner's next worker start runs it (its [env] " \
|
|
85
86
|
"can run commands), so the owner approves it"
|
|
86
|
-
GIT_HOOKS = "a participant's turn can't write git's hooks (.git/hooks
|
|
87
|
+
GIT_HOOKS = "a participant's turn can't write or install git's hooks (.git/hooks, .git/modules/*/hooks, a " \
|
|
88
|
+
"template, pre-commit install, husky): git runs them in the owner's name"
|
|
87
89
|
# chi web on loopback asks the owner's token too (Web::App#token_gate,
|
|
88
90
|
# LAN mode); this text check stays as defense in depth: a loopback-only
|
|
89
91
|
# chi web still answers loopback as the owner.
|
|
@@ -95,6 +97,8 @@ module Samagotchi
|
|
|
95
97
|
"owner's cookies"
|
|
96
98
|
LISTENER = "a participant's turn can't listen on a port: what it serves on this machine could catch the " \
|
|
97
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"
|
|
98
102
|
VERB = "a participant's turn runs only a program named plainly: no $, $(…) or glob in a command's name"
|
|
99
103
|
UNREADABLE = "a participant's turn runs only commands chi can read: split this command, or write the file " \
|
|
100
104
|
"with the write tool"
|
|
@@ -193,13 +197,15 @@ module Samagotchi
|
|
|
193
197
|
|
|
194
198
|
# Why the owner is asked (fresh, once): a command that makes a link
|
|
195
199
|
# (LINK), one that runs mise or direnv (ENV_LOADER), a write to mise's
|
|
196
|
-
# 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
|
|
197
202
|
# NO_PROJECT without one); nil when nothing asks.
|
|
198
203
|
def ask_reason(verdict)
|
|
199
204
|
targets = verdict.targets
|
|
200
205
|
return LINK if targets&.command && command(targets, nil).links?
|
|
201
206
|
return ENV_LOADER if targets&.command && command(targets, nil).loads_env?
|
|
202
207
|
return MISE_CONFIG if mise_config_write?(verdict)
|
|
208
|
+
return NET if targets&.command && command(targets, nil).net_ask?
|
|
203
209
|
return nil unless outside_write?(verdict)
|
|
204
210
|
|
|
205
211
|
project_root ? OUTSIDE_WRITE : NO_PROJECT
|