samagotchi 0.102.0 → 0.103.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 +40 -1
- data/bin/chi +23 -191
- data/docs/architecture.md +54 -0
- data/docs/cli.md +114 -7
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +2 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/remote-sessions.md +303 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +26 -2
- data/docs/sessions.md +12 -5
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +24 -4
- data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +22 -38
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +43 -0
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/flags.rb +5 -0
- data/lib/samagotchi/cli/parent_wait.rb +22 -13
- data/lib/samagotchi/context_command.rb +80 -73
- data/lib/samagotchi/engine.rb +12 -8
- data/lib/samagotchi/idle_recap.rb +7 -1
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/launch_mode.rb +22 -0
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +233 -0
- data/lib/samagotchi/send_command/busy.rb +58 -0
- data/lib/samagotchi/send_command/parent_gate.rb +23 -10
- data/lib/samagotchi/send_command/wait_target.rb +16 -3
- data/lib/samagotchi/send_command.rb +173 -108
- data/lib/samagotchi/session.rb +30 -5
- data/lib/samagotchi/session_access/contract.rb +129 -0
- data/lib/samagotchi/session_access/dispatch.rb +133 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +349 -0
- data/lib/samagotchi/session_access/remote.rb +638 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
- data/lib/samagotchi/session_access.rb +141 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_manager.rb +7 -2
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
- data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
- data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +360 -69
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -4
- data/lib/samagotchi/web/public/stage_frame.js +7 -2
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/stream_handlers.js +3 -1
- data/lib/samagotchi/web/public/working_set.js +159 -28
- data/lib/samagotchi/web/public/working_set_rail.js +39 -12
- data/lib/samagotchi/web/public/working_set_view.js +91 -33
- data/lib/samagotchi/web/session_access_routes.rb +343 -0
- data/lib/samagotchi/worker_sidecar.rb +12 -5
- data/lib/samagotchi/working_set.rb +88 -14
- data/lib/samagotchi/working_set_command.rb +133 -30
- data/lib/samagotchi/working_set_pin.rb +34 -0
- data/lib/samagotchi/working_set_steer.rb +191 -27
- metadata +19 -1
|
@@ -5,8 +5,10 @@ require "socket"
|
|
|
5
5
|
|
|
6
6
|
require_relative "worker_sidecar"
|
|
7
7
|
require_relative "delivery"
|
|
8
|
+
require_relative "chi_invocation"
|
|
8
9
|
require_relative "bridge_client/sse_parser"
|
|
9
10
|
require_relative "bridge_client/event_stream"
|
|
11
|
+
require_relative "bridge_client/stream_request"
|
|
10
12
|
|
|
11
13
|
module Samagotchi
|
|
12
14
|
# Client side of a session worker's Bridge: the 127.0.0.1 HTTP + SSE server
|
|
@@ -19,7 +21,7 @@ module Samagotchi
|
|
|
19
21
|
class BridgeClient
|
|
20
22
|
HOST = "127.0.0.1"
|
|
21
23
|
# A reply's status line; its code is group 1.
|
|
22
|
-
STATUS_LINE =
|
|
24
|
+
STATUS_LINE = StreamRequest::STATUS_LINE
|
|
23
25
|
STREAM_CONNECT_ATTEMPTS = 3
|
|
24
26
|
# Seconds #stream waits for the Bridge's response headers, per attempt:
|
|
25
27
|
# a worker that took the connection and froze must not hold the web's
|
|
@@ -54,9 +56,16 @@ module Samagotchi
|
|
|
54
56
|
# What a UI says when the Bridge answers a route with 404: the worker runs
|
|
55
57
|
# code from before the route. Restarting it picks up the installed chi.
|
|
56
58
|
# @param cant [String] what the old worker can't do, e.g. "run commands"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
# @param via [ChiInvocation] how the commands are spelled (chi --remote
|
|
60
|
+
# NAME: no --resume there, a message starts the fresh worker)
|
|
61
|
+
def self.stale_worker_message(session_id, cant:, via: ChiInvocation::HERE)
|
|
62
|
+
stop = via.command("sessions stop #{session_id}")
|
|
63
|
+
restart = if via.remote?
|
|
64
|
+
"#{stop}, then a message (#{via.command("send #{session_id} -m …")}) starts a fresh one"
|
|
65
|
+
else
|
|
66
|
+
"#{stop} && chi --resume #{session_id}"
|
|
67
|
+
end
|
|
68
|
+
"this session's worker runs an older chi and can't #{cant}; restart it: #{restart} (its turns still work)"
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
# Port of the live Bridge advertised in +session_dir+'s sidecar, or nil.
|
|
@@ -103,6 +112,10 @@ module Samagotchi
|
|
|
103
112
|
|
|
104
113
|
attr_reader :session_id, :port, :host
|
|
105
114
|
|
|
115
|
+
# Which worker this client talks to, for EventStream: a rediscovered
|
|
116
|
+
# client with another key is a new worker. A Bridge's port.
|
|
117
|
+
def worker_key = @port
|
|
118
|
+
|
|
106
119
|
# @param read_timeout [Numeric] see READ_TIMEOUT
|
|
107
120
|
def initialize(session_id:, port:, host: HOST, read_timeout: READ_TIMEOUT)
|
|
108
121
|
@session_id = session_id
|
|
@@ -332,45 +345,16 @@ module Samagotchi
|
|
|
332
345
|
|
|
333
346
|
# Open GET /session/:id/stream and read past the response headers.
|
|
334
347
|
# @param timeout [Float, nil] give up when the headers aren't all in by then
|
|
335
|
-
# @return [Array(TCPSocket, Integer), nil] the socket positioned
|
|
336
|
-
# body
|
|
348
|
+
# @return [Array(TCPSocket, Integer, Hash), nil] the socket positioned
|
|
349
|
+
# at the body, the HTTP status and the headers (StreamRequest.open), or
|
|
350
|
+
# nil when the Bridge can't be reached
|
|
337
351
|
def connect_stream(query: "", last_event_id: nil, timeout: nil)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
last_event_line = lei.empty? ? "" : "Last-Event-ID: #{lei}\r\n"
|
|
341
|
-
sock.write("GET /session/#{@session_id}/stream#{query} HTTP/1.1\r\nHost: #{@host}:#{@port}\r\nAccept: text/event-stream\r\n#{last_event_line}Connection: keep-alive\r\n\r\n")
|
|
342
|
-
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
343
|
-
status = header_line(sock, deadline).to_s[STATUS_LINE, 1].to_i
|
|
344
|
-
while (line = header_line(sock, deadline))
|
|
345
|
-
break if line.strip.empty?
|
|
346
|
-
end
|
|
347
|
-
[sock, status]
|
|
348
|
-
rescue SystemCallError, SocketError, IOError
|
|
349
|
-
sock&.close rescue nil
|
|
350
|
-
nil
|
|
352
|
+
StreamRequest.open(host: @host, port: @port, path: "/session/#{@session_id}/stream#{query}",
|
|
353
|
+
last_event_id: last_event_id, timeout: timeout)
|
|
351
354
|
end
|
|
352
355
|
|
|
353
356
|
private
|
|
354
357
|
|
|
355
|
-
# One line of the response headers, or nil at EOF; raises
|
|
356
|
-
# Errno::ETIMEDOUT once +deadline+ (monotonic, nil: none) has passed. A
|
|
357
|
-
# line read in pieces, so a Bridge that stops mid-line can't hold it.
|
|
358
|
-
def header_line(sock, deadline)
|
|
359
|
-
return sock.gets unless deadline
|
|
360
|
-
|
|
361
|
-
line = +""
|
|
362
|
-
loop do
|
|
363
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
364
|
-
raise Errno::ETIMEDOUT unless remaining.positive? && sock.wait_readable(remaining)
|
|
365
|
-
|
|
366
|
-
char = sock.getc
|
|
367
|
-
return line.empty? ? nil : line if char.nil?
|
|
368
|
-
|
|
369
|
-
line << char
|
|
370
|
-
return line if char == "\n"
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
|
|
374
358
|
# When this client stops waiting for a reply, as epoch seconds (see
|
|
375
359
|
# DEADLINE_SHARE). Wall clock: the worker runs on this machine and reads
|
|
376
360
|
# the same one.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-peer
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-outbox context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -12,7 +12,7 @@ plugin:
|
|
|
12
12
|
file: plugin.rb
|
|
13
13
|
sha256: sha256:50f427d3862ae9d0d15693b4d887c528162b96c0320cc663df89d59d830d4660
|
|
14
14
|
scripts:
|
|
15
|
-
pair: sha256:
|
|
15
|
+
pair: sha256:2e66103c43fce5eab05f897682ba943f394743c1ad47bf439be3ce529f1b2058
|
|
16
16
|
chi-peer-SKILL.md: sha256:e991a91139a127a60f9a216043eb5864945fb211952a1e3c8db2068c6cc254f1
|
|
17
17
|
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
18
18
|
# An agreeing answer's closing wake (the skills say an OK wakes chi once)
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
# is this session's inbox socket (chi's claude_send tool writes there), and
|
|
9
9
|
# claude-outbox, where Claude's answers go. Again after a Claude restart, it
|
|
10
10
|
# only refreshes the socket: the push carries the new hint. CHI names the chi
|
|
11
|
-
# command (default: chi on the PATH).
|
|
11
|
+
# command (default: chi on the PATH). Through a remote (CHI_REMOTE, or
|
|
12
|
+
# --remote NAME in CHI) chi there can't reach this machine's socket: it
|
|
13
|
+
# attaches claude-outbox only, and says so.
|
|
12
14
|
set -eu
|
|
13
15
|
|
|
14
16
|
usage() {
|
|
@@ -33,6 +35,23 @@ if [ -z "$sock" ]; then
|
|
|
33
35
|
fi
|
|
34
36
|
chi=${CHI:-chi}
|
|
35
37
|
|
|
38
|
+
# The remote, as SessionAccess::Dispatch names it: a --remote NAME (or
|
|
39
|
+
# --remote=NAME) word in CHI, the last one, beats CHI_REMOTE (trimmed; blank:
|
|
40
|
+
# local). An empty or dash-led name is still a remote: chi refuses it.
|
|
41
|
+
remote=$(printf '%s\n' "${CHI_REMOTE:-}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
42
|
+
via= take=
|
|
43
|
+
[ -z "$remote" ] || via=env
|
|
44
|
+
for word in $chi; do
|
|
45
|
+
if [ -n "$take" ]; then
|
|
46
|
+
remote=$word take=
|
|
47
|
+
continue
|
|
48
|
+
fi
|
|
49
|
+
case "$word" in
|
|
50
|
+
--remote) remote= via=chi take=1 ;;
|
|
51
|
+
--remote=*) remote=${word#--remote=} via=chi ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
|
|
36
55
|
# A JSON string: backslashes, quotes, tabs and newlines escaped.
|
|
37
56
|
json() {
|
|
38
57
|
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' |
|
|
@@ -51,6 +70,16 @@ attach() {
|
|
|
51
70
|
fi
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
if [ -n "$via" ]; then
|
|
74
|
+
attach claude-outbox --why "Claude's answers to what you sent it (claude-peer); Claude runs on another machine: claude_send can't reach it" \
|
|
75
|
+
--hint "latest Claude reply"
|
|
76
|
+
ask=$chi
|
|
77
|
+
[ "$via" = chi ] || ask="CHI_REMOTE=$remote $chi"
|
|
78
|
+
printf '%s\n' "pair: chi is on $remote, so chi can't send to this Claude session (claude_send needs its socket on chi's machine): paired with chi $id, claude-outbox only. Answer chi with $ask context push claude-outbox $id; chi's replies reach you only when the user relays them (or asks you to run $ask send --wait $id)"
|
|
79
|
+
$chi context ls "$id"
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
54
83
|
attach claude-inbox --why "the Claude Code session paired with you: send it parts with claude_send" --hint "$sock"
|
|
55
84
|
text="Claude Code peer inbox
|
|
56
85
|
socket: $sock
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codex-peer
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with an OpenAI Codex CLI session you work with: the codex_send tool and /codex send it a part of the work (a plan step, a diff, a finding) to validate or as context, with codex queue, and Codex's answer comes back as the codex-outbox context; /codex install-skill installs Codex's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -12,7 +12,7 @@ plugin:
|
|
|
12
12
|
file: plugin.rb
|
|
13
13
|
sha256: sha256:b03465566272891237ac1906980ff6956579a4703d3a42c414ce50a81de551f6
|
|
14
14
|
scripts:
|
|
15
|
-
pair: sha256:
|
|
15
|
+
pair: sha256:0830dcb856debea0e4a2a0eb0da8a73b9e9ac2444da957d2928ec734280186a1
|
|
16
16
|
chi-peer-SKILL.md: sha256:7c392ac04c115ea1e762ad562b5c4c44c3eca71558ba5d9dfeaeabdd533b6a82
|
|
17
17
|
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
18
18
|
# An agreeing answer's closing wake (the skills say an OK wakes chi once)
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
# `codex queue --thread <id>` with it), and codex-outbox, where Codex's
|
|
10
10
|
# answers go. Again later, it only refreshes the thread id: the push carries
|
|
11
11
|
# the new hint. CHI names the chi command (default: chi on the PATH).
|
|
12
|
+
# Through a remote (CHI_REMOTE, or --remote NAME in CHI) chi there can't
|
|
13
|
+
# queue to this machine's Codex: it attaches codex-outbox only, and says so.
|
|
12
14
|
#
|
|
13
15
|
# chi writes the session's context under its state folder, which Codex's
|
|
14
16
|
# default sandbox doesn't let it write: ~/.codex/config.toml needs
|
|
15
17
|
# [sandbox_workspace_write]
|
|
16
18
|
# writable_roots = ["<state>/samagotchi/context"]
|
|
17
|
-
# and a write refused says so.
|
|
19
|
+
# and a write refused says so. Through a remote chi talks HTTP instead,
|
|
20
|
+
# which the sandbox may refuse (no network): a connection that fails says so.
|
|
18
21
|
set -eu
|
|
19
22
|
|
|
20
23
|
usage() {
|
|
@@ -39,14 +42,46 @@ if [ -z "$thread" ]; then
|
|
|
39
42
|
fi
|
|
40
43
|
chi=${CHI:-chi}
|
|
41
44
|
|
|
42
|
-
#
|
|
45
|
+
# The remote, as SessionAccess::Dispatch names it: a --remote NAME (or
|
|
46
|
+
# --remote=NAME) word in CHI, the last one, beats CHI_REMOTE (trimmed; blank:
|
|
47
|
+
# local). An empty or dash-led name is still a remote: chi refuses it.
|
|
48
|
+
remote=$(printf '%s\n' "${CHI_REMOTE:-}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
49
|
+
via= take=
|
|
50
|
+
[ -z "$remote" ] || via=env
|
|
51
|
+
for word in $chi; do
|
|
52
|
+
if [ -n "$take" ]; then
|
|
53
|
+
remote=$word take=
|
|
54
|
+
continue
|
|
55
|
+
fi
|
|
56
|
+
case "$word" in
|
|
57
|
+
--remote) remote= via=chi take=1 ;;
|
|
58
|
+
--remote=*) remote=${word#--remote=} via=chi ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
config='~/.codex/config.toml'
|
|
63
|
+
[ -z "${CODEX_HOME:-}" ] || config=$CODEX_HOME/config.toml
|
|
64
|
+
|
|
65
|
+
# Prints chi's error, and the config line when the sandbox may have refused:
|
|
66
|
+
# writable_roots here, network_access through a remote.
|
|
43
67
|
fail() {
|
|
44
68
|
printf '%s\n' "$1" >&2
|
|
69
|
+
if [ -n "$via" ]; then
|
|
70
|
+
case "$1" in
|
|
71
|
+
*"connection broke"* | *"can't find host"* | *"host unreachable"* | *"no answer within"*)
|
|
72
|
+
cat >&2 <<MSG
|
|
73
|
+
pair: Codex's sandbox may be keeping chi from reaching $remote (workspace-write has no network by default): if so, add to $config
|
|
74
|
+
[sandbox_workspace_write]
|
|
75
|
+
network_access = true
|
|
76
|
+
then restart Codex and pair again.
|
|
77
|
+
MSG
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
45
82
|
case "$1" in
|
|
46
83
|
*"Operation not permitted"* | *"Read-only file system"* | *"Permission denied"*)
|
|
47
84
|
state=${XDG_STATE_HOME:-$HOME/.local/state}
|
|
48
|
-
config='~/.codex/config.toml'
|
|
49
|
-
[ -z "${CODEX_HOME:-}" ] || config=$CODEX_HOME/config.toml
|
|
50
85
|
cat >&2 <<MSG
|
|
51
86
|
pair: Codex's sandbox can't write chi's state: add to $config
|
|
52
87
|
[sandbox_workspace_write]
|
|
@@ -75,6 +110,16 @@ attach() {
|
|
|
75
110
|
fi
|
|
76
111
|
}
|
|
77
112
|
|
|
113
|
+
if [ -n "$via" ]; then
|
|
114
|
+
attach codex-outbox --why "Codex's answers to what you sent it (codex-peer); Codex runs on another machine: codex_send can't reach it" \
|
|
115
|
+
--hint "latest Codex reply"
|
|
116
|
+
ask=$chi
|
|
117
|
+
[ "$via" = chi ] || ask="CHI_REMOTE=$remote $chi"
|
|
118
|
+
printf '%s\n' "pair: chi is on $remote, so chi can't send to this Codex session (codex_send runs codex queue on chi's machine): paired with chi $id, codex-outbox only. Answer chi with $ask context push codex-outbox $id; chi's replies reach you only when the user relays them (or asks you to run $ask send --wait $id)"
|
|
119
|
+
$chi context ls "$id"
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
78
123
|
attach codex-inbox --why "the Codex session paired with you: send it parts with codex_send" --hint "$thread"
|
|
79
124
|
text="Codex peer inbox
|
|
80
125
|
thread: $thread
|
|
@@ -154,10 +154,12 @@ rules:
|
|
|
154
154
|
# code). add NAME --push and chi context push don't run anything. Quoted
|
|
155
155
|
# words ("context", '--cmd=…') and backslash-newline continuations still
|
|
156
156
|
# match; xargs chi context add asks whatever it is fed (its arguments
|
|
157
|
-
# aren't in the command).
|
|
157
|
+
# aren't in the command). chi's global flag --remote NAME (the only one
|
|
158
|
+
# that goes before a subcommand) may stand between chi and context;
|
|
159
|
+
# CHI_REMOTE=NAME leaves the words as they are.
|
|
158
160
|
- id: chi-context-cmd
|
|
159
161
|
tool: shell
|
|
160
|
-
command: '\bchi["'']?[\s\\]+["'']?context["'']?[\s\\]+["'']?add\b(?:[^\n]|\\\n)*[\s"''\\]--cmd(?:[\s="''\\]|$)|\bxargs\b[^;&|\n]*\bchi["'']?[\s\\]+["'']?context["'']?[\s\\]+["'']?add\b'
|
|
162
|
+
command: '\bchi["'']?(?:[\s\\]+["'']?--remote(?:=|["'']?[\s\\]+["'']?)[^\s"''\\;&|]+["'']?)*[\s\\]+["'']?context["'']?[\s\\]+["'']?add\b(?:[^\n]|\\\n)*[\s"''\\]--cmd(?:[\s="''\\]|$)|\bxargs\b[^;&|\n]*\bchi["'']?(?:[\s\\]+["'']?--remote(?:=|["'']?[\s\\]+["'']?)[^\s"''\\;&|]+["'']?)*[\s\\]+["'']?context["'']?[\s\\]+["'']?add\b'
|
|
161
163
|
verdict: ask
|
|
162
164
|
scopes: [once]
|
|
163
165
|
reason: attaches a command chi will run every few minutes, outside the guardrails
|
|
@@ -165,10 +167,11 @@ rules:
|
|
|
165
167
|
# ── chi broadcast: for the user, not an agent ─────────────────────────
|
|
166
168
|
# chi broadcast refuses to run inside a session (SAMAGOTCHI_PARENT_SESSION
|
|
167
169
|
# is set there), but an agent can unset that; this asks every time, and
|
|
168
|
-
# only the user may allow it. Quoted words
|
|
170
|
+
# only the user may allow it. Quoted words, continuations and --remote
|
|
171
|
+
# NAME before the word still match.
|
|
169
172
|
- id: chi-broadcast
|
|
170
173
|
tool: shell
|
|
171
|
-
command: '\bchi["'']?[\s\\]+["'']?broadcast\b'
|
|
174
|
+
command: '\bchi["'']?(?:[\s\\]+["'']?--remote(?:=|["'']?[\s\\]+["'']?)[^\s"''\\;&|]+["'']?)*[\s\\]+["'']?broadcast\b'
|
|
172
175
|
verdict: ask
|
|
173
176
|
scopes: [once]
|
|
174
177
|
reason: shares a note with every session it may concern; chi broadcast is for the user, not an agent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: guardrails
|
|
3
|
-
version: 0.9.
|
|
3
|
+
version: 0.9.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, git hooks, chi's own config and attached context from the shell, chi context add --cmd, chi broadcast, answering chi around chi answer, removing a memory, writing a memory that reaches the system prompt (identity, model notes) or a model overlay; in strict mode also git rebase and writes and git in another checkout outside the session's repo), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
6
6
|
trust_level: reviewed
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# How the follow-up commands chi prints (ParentReport's chi answer and
|
|
5
|
+
# chi send --wait, a stop's chi --resume) are spelled so that they work
|
|
6
|
+
# when pasted: on this machine plain chi, against a remote (chi --remote
|
|
7
|
+
# NAME, CHI_REMOTE=NAME alike) chi --remote NAME. What runs on this
|
|
8
|
+
# machine only (--attach, --resume: remote attach is its own phase) has
|
|
9
|
+
# no remote form: #attach and #resume are nil there, and #open_it and
|
|
10
|
+
# #shows name the remote's chi web instead.
|
|
11
|
+
# @!attribute remote [String, nil] the remote's name; nil: this machine
|
|
12
|
+
ChiInvocation = Data.define(:remote) do
|
|
13
|
+
def remote? = !remote.nil?
|
|
14
|
+
|
|
15
|
+
# The command word: "chi", or "chi --remote NAME".
|
|
16
|
+
def chi = remote? ? "chi --remote #{remote}" : "chi"
|
|
17
|
+
|
|
18
|
+
# +words+ after #chi: command("answer ID …").
|
|
19
|
+
def command(words) = "#{chi} #{words}"
|
|
20
|
+
|
|
21
|
+
# chi --attach ID; nil remotely.
|
|
22
|
+
def attach(id) = ("chi --attach #{id}" unless remote?)
|
|
23
|
+
|
|
24
|
+
# chi --resume ID; nil remotely.
|
|
25
|
+
def resume(id) = ("chi --resume #{id}" unless remote?)
|
|
26
|
+
|
|
27
|
+
# What shows the session ("… shows it"): chi --attach ID, or the
|
|
28
|
+
# remote's chi web.
|
|
29
|
+
def shows(id) = attach(id) || web
|
|
30
|
+
|
|
31
|
+
# Where the user opens it ("open it: …"): chi --attach ID or the web,
|
|
32
|
+
# or the remote's chi web.
|
|
33
|
+
def open_it(id) = remote? ? web : "#{attach(id)} or the web"
|
|
34
|
+
|
|
35
|
+
# The remote's chi web, by name.
|
|
36
|
+
def web = "chi web on #{remote}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ChiInvocation
|
|
40
|
+
# This machine's: plain chi.
|
|
41
|
+
HERE = new(remote: nil)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -26,9 +26,10 @@ module Samagotchi
|
|
|
26
26
|
@stderr.puts(text)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# @param usage [String, nil] what follows the line (nil: "(see …
|
|
30
|
+
# --help)" on it); a one-line error whose fix the line says passes nil
|
|
29
31
|
# @return [Integer] the usage exit status
|
|
30
|
-
def usage_error(message)
|
|
31
|
-
usage = usage_on_error
|
|
32
|
+
def usage_error(message, usage: usage_on_error)
|
|
32
33
|
error_line("#{command_name}: #{message}#{" (see #{command_name} --help)" unless usage}")
|
|
33
34
|
@stderr.puts(usage) if usage
|
|
34
35
|
Exit::USAGE
|
data/lib/samagotchi/cli/flags.rb
CHANGED
|
@@ -68,6 +68,11 @@ module Samagotchi
|
|
|
68
68
|
add(names, Flag.new(refusal: message))
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Each declared flag's names, in the order declared ([["-m",
|
|
72
|
+
# "--message"], ["--new"]]); refused flags too.
|
|
73
|
+
# @return [Array<Array<String>>]
|
|
74
|
+
def names = @flags.keys.group_by { |name| @flags[name].object_id }.values
|
|
75
|
+
|
|
71
76
|
# @param defaults [Hash] the options before any flag (not changed)
|
|
72
77
|
# @return [Result]
|
|
73
78
|
def parse(argv, defaults = {})
|
|
@@ -4,6 +4,7 @@ require_relative "../session"
|
|
|
4
4
|
require_relative "../reply_wait"
|
|
5
5
|
require_relative "../parent_report"
|
|
6
6
|
require_relative "exit"
|
|
7
|
+
require_relative "../session_access"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
module CLI
|
|
@@ -12,8 +13,8 @@ module Samagotchi
|
|
|
12
13
|
# for it, and the wait for what comes next, reported the ParentReport
|
|
13
14
|
# way (Ctrl-C included). The including class (a CLI::Command) defines
|
|
14
15
|
# #parse (options, or an exit status) and #run_parsed(options), sets
|
|
15
|
-
# @stdout, @stderr and @
|
|
16
|
-
# (specs stub them on the class).
|
|
16
|
+
# @stdout, @stderr and @access (a SessionAccess backend), and may
|
|
17
|
+
# override the constants (specs stub them on the class).
|
|
17
18
|
module ParentWait
|
|
18
19
|
POLL_INTERVAL = ReplyWait::POLL_INTERVAL
|
|
19
20
|
# No live worker this long while waiting: it died before it could mark
|
|
@@ -30,7 +31,14 @@ module Samagotchi
|
|
|
30
31
|
# With --format json stdout is one JSON object, whatever the end: a
|
|
31
32
|
# failure before the wait is reported from stderr's last line.
|
|
32
33
|
@stderr = ParentReport::LastLine.new(@stderr) if @json
|
|
33
|
-
status =
|
|
34
|
+
status = begin
|
|
35
|
+
run_parsed(options)
|
|
36
|
+
rescue SessionAccess::RemoteFailed => e
|
|
37
|
+
# A remote that went away mid-run: the command's line, and with
|
|
38
|
+
# --format json its object.
|
|
39
|
+
error_line("#{command_name}: #{e.message}")
|
|
40
|
+
Exit::FAILED
|
|
41
|
+
end
|
|
34
42
|
if @json && !@reported && status == 1
|
|
35
43
|
detail = @stderr.last.to_s.delete_prefix("#{command_name}: ")
|
|
36
44
|
@stdout.puts(ParentReport.error_line(detail, session_id: @session_id))
|
|
@@ -45,9 +53,7 @@ module Samagotchi
|
|
|
45
53
|
# error line; nil after the error line.
|
|
46
54
|
# @return [String, nil]
|
|
47
55
|
def resolve(given)
|
|
48
|
-
|
|
49
|
-
Session.load(id, state_dir: @state_dir)
|
|
50
|
-
@session_id = id
|
|
56
|
+
@session_id = @access.resolve(given)
|
|
51
57
|
rescue ArgumentError => e
|
|
52
58
|
error_line("#{command_name}: #{e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"}")
|
|
53
59
|
nil
|
|
@@ -70,16 +76,19 @@ module Samagotchi
|
|
|
70
76
|
# @return [Integer] the exit status
|
|
71
77
|
def wait_for_reply(id, cursor:, baseline:, timeout:, owner_grace: self.class::WORKER_GONE_AFTER, turn: nil,
|
|
72
78
|
queued: false, ended_before: false, report_turn_id: turn)
|
|
73
|
-
result =
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
result = @access.wait_reply(id, cursor: cursor, timeout: timeout, baseline: baseline, owner_grace: owner_grace,
|
|
80
|
+
poll_interval: self.class::POLL_INTERVAL, turn: turn, queued: queued,
|
|
81
|
+
ended_before: ended_before)
|
|
76
82
|
result.turn_id = report_turn_id if report_turn_id && !report_turn_id.to_s.empty?
|
|
77
83
|
result.text = result.text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
78
84
|
@reported = true
|
|
79
|
-
# What a parent may pick there: config.yml tightened by the session's
|
|
80
|
-
|
|
85
|
+
# What a parent may pick there: config.yml tightened by the session's
|
|
86
|
+
# own. Machine-local (this machine's config.yml): remotely the
|
|
87
|
+
# server's (P2's wait route).
|
|
88
|
+
strategy = @access.parent_strategy(id)
|
|
81
89
|
ParentReport.report(result, session_id: id, stdout: @stdout, stderr: @stderr, command: command_name,
|
|
82
|
-
json: @json, timeout: timeout, extra: report_extra, strategy: strategy
|
|
90
|
+
json: @json, timeout: timeout, extra: report_extra, strategy: strategy,
|
|
91
|
+
via: @access.invocation)
|
|
83
92
|
rescue Interrupt
|
|
84
93
|
if @json
|
|
85
94
|
@stdout.puts(ParentReport.json_line(ReplyWait::Result.new(status: :canceled, turn_id: report_turn_id), session_id: id,
|
|
@@ -87,7 +96,7 @@ module Samagotchi
|
|
|
87
96
|
@stdout.flush
|
|
88
97
|
@reported = true
|
|
89
98
|
end
|
|
90
|
-
error_line("#{command_name}: still running:
|
|
99
|
+
error_line("#{command_name}: still running: #{@access.invocation.shows(id)}")
|
|
91
100
|
Exit::INTERRUPTED
|
|
92
101
|
rescue ArgumentError
|
|
93
102
|
error_line("#{command_name}: the session is gone (deleted while waiting)")
|