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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module SessionAccess
|
|
5
|
+
# Which session commands and flags run against another machine's
|
|
6
|
+
# sessions (chi --remote NAME …, CHI_REMOTE; SessionAccess::Dispatch)
|
|
7
|
+
# and which run on this machine only (refused under a remote, exit 2:
|
|
8
|
+
# "chi <cmd> runs on this machine only: run it on <remote>"). The v1 scope is the remote-sessions plan's
|
|
9
|
+
# D3: send, answer, note, a session's context (add, push, ls, show, rm)
|
|
10
|
+
# and sessions list/stop/restart/archive/unarchive/delete; the rest
|
|
11
|
+
# stays local. bin/chi's subcommands that act on the machine, not on
|
|
12
|
+
# sessions, are MACHINE. The run flags (--attach, -p, --resume) are not
|
|
13
|
+
# subcommands and not in this table (remote attach is its own phase).
|
|
14
|
+
#
|
|
15
|
+
# spec/session_access_contract_spec.rb fails when a subcommand, one of
|
|
16
|
+
# its subcommands or one of a supported one's flags is in neither.
|
|
17
|
+
module Contract
|
|
18
|
+
SUPPORTED = :supported
|
|
19
|
+
LOCAL_ONLY = :local_only
|
|
20
|
+
|
|
21
|
+
# bin/chi subcommands that act on this machine: never sessions. chi
|
|
22
|
+
# remote (this machine's list of remotes) runs here under a remote too
|
|
23
|
+
# (Dispatch); the others are refused there.
|
|
24
|
+
MACHINE = %w[bootstrap update models desktop memory bundle remote].freeze
|
|
25
|
+
|
|
26
|
+
# "command", "command sub" or either with a flag (its long name) →
|
|
27
|
+
# SUPPORTED or LOCAL_ONLY. A LOCAL_ONLY command or subcommand lists
|
|
28
|
+
# no flags: all of it is local.
|
|
29
|
+
TABLE = {
|
|
30
|
+
"send" => SUPPORTED,
|
|
31
|
+
"send --message" => SUPPORTED,
|
|
32
|
+
"send --new" => SUPPORTED,
|
|
33
|
+
# Required with --new under a remote (D4: no laptop path guessed).
|
|
34
|
+
"send --dir" => SUPPORTED,
|
|
35
|
+
"send --continues" => SUPPORTED,
|
|
36
|
+
"send --model" => SUPPORTED,
|
|
37
|
+
"send --llm-context" => SUPPORTED,
|
|
38
|
+
"send --llm-context-apply" => SUPPORTED,
|
|
39
|
+
"send --llm-context-budget" => SUPPORTED,
|
|
40
|
+
"send --thinking" => SUPPORTED,
|
|
41
|
+
"send --toolkit" => SUPPORTED,
|
|
42
|
+
"send --tool" => SUPPORTED,
|
|
43
|
+
"send --no-tool" => SUPPORTED,
|
|
44
|
+
"send --set" => SUPPORTED,
|
|
45
|
+
"send --parent-answers" => SUPPORTED,
|
|
46
|
+
"send --image" => SUPPORTED,
|
|
47
|
+
"send --wait" => SUPPORTED,
|
|
48
|
+
"send --cut" => SUPPORTED,
|
|
49
|
+
"send --queue" => SUPPORTED,
|
|
50
|
+
"send --timeout" => SUPPORTED,
|
|
51
|
+
"send --format" => SUPPORTED,
|
|
52
|
+
"send --turn" => SUPPORTED,
|
|
53
|
+
# Refused on both (there is no --all).
|
|
54
|
+
"send --all" => SUPPORTED,
|
|
55
|
+
|
|
56
|
+
"answer" => SUPPORTED,
|
|
57
|
+
"answer --question" => SUPPORTED,
|
|
58
|
+
"answer --option" => SUPPORTED,
|
|
59
|
+
"answer --text" => SUPPORTED,
|
|
60
|
+
"answer --dismiss" => SUPPORTED,
|
|
61
|
+
"answer --timeout" => SUPPORTED,
|
|
62
|
+
"answer --format" => SUPPORTED,
|
|
63
|
+
|
|
64
|
+
"note" => SUPPORTED,
|
|
65
|
+
"note --message" => SUPPORTED,
|
|
66
|
+
"note --source" => SUPPORTED,
|
|
67
|
+
"note --all" => SUPPORTED,
|
|
68
|
+
|
|
69
|
+
"context" => SUPPORTED,
|
|
70
|
+
"context add" => SUPPORTED,
|
|
71
|
+
"context add --cmd" => SUPPORTED,
|
|
72
|
+
"context add --push" => SUPPORTED,
|
|
73
|
+
"context add --every" => SUPPORTED,
|
|
74
|
+
"context add --why" => SUPPORTED,
|
|
75
|
+
"context add --hint" => SUPPORTED,
|
|
76
|
+
"context add --project" => LOCAL_ONLY,
|
|
77
|
+
"context push" => SUPPORTED,
|
|
78
|
+
"context push --message" => SUPPORTED,
|
|
79
|
+
"context push --project" => LOCAL_ONLY,
|
|
80
|
+
"context ls" => SUPPORTED,
|
|
81
|
+
"context ls --format" => SUPPORTED,
|
|
82
|
+
"context ls --project" => LOCAL_ONLY,
|
|
83
|
+
"context show" => SUPPORTED,
|
|
84
|
+
"context show --json" => SUPPORTED,
|
|
85
|
+
"context show --project" => LOCAL_ONLY,
|
|
86
|
+
"context rm" => SUPPORTED,
|
|
87
|
+
"context rm --project" => LOCAL_ONLY,
|
|
88
|
+
"context refresh" => LOCAL_ONLY,
|
|
89
|
+
"context mute" => LOCAL_ONLY,
|
|
90
|
+
"context unmute" => LOCAL_ONLY,
|
|
91
|
+
|
|
92
|
+
"sessions" => SUPPORTED,
|
|
93
|
+
"sessions list" => SUPPORTED,
|
|
94
|
+
"sessions list --sort" => SUPPORTED,
|
|
95
|
+
"sessions list --order" => SUPPORTED,
|
|
96
|
+
"sessions list --limit" => SUPPORTED,
|
|
97
|
+
"sessions list --cwd" => SUPPORTED,
|
|
98
|
+
"sessions list --format" => SUPPORTED,
|
|
99
|
+
# Remotely every project's by default (D4); --scope=project needs
|
|
100
|
+
# --cwd there.
|
|
101
|
+
"sessions list --scope" => SUPPORTED,
|
|
102
|
+
"sessions list --live" => SUPPORTED,
|
|
103
|
+
"sessions list --archived" => SUPPORTED,
|
|
104
|
+
"sessions stop" => SUPPORTED,
|
|
105
|
+
"sessions restart" => SUPPORTED,
|
|
106
|
+
"sessions archive" => SUPPORTED,
|
|
107
|
+
"sessions unarchive" => SUPPORTED,
|
|
108
|
+
"sessions delete" => SUPPORTED,
|
|
109
|
+
"sessions delete --force" => SUPPORTED,
|
|
110
|
+
"sessions prune" => LOCAL_ONLY,
|
|
111
|
+
"sessions clean" => LOCAL_ONLY,
|
|
112
|
+
"sessions stats" => LOCAL_ONLY,
|
|
113
|
+
|
|
114
|
+
"broadcast" => LOCAL_ONLY
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
module_function
|
|
118
|
+
|
|
119
|
+
# @param words [Array<String>] a command, maybe its subcommand, maybe
|
|
120
|
+
# a flag's long name: status("context", "add", "--cmd")
|
|
121
|
+
# @return [Symbol, nil] SUPPORTED, LOCAL_ONLY, or nil when not listed
|
|
122
|
+
def status(*words) = TABLE[words.join(" ")]
|
|
123
|
+
|
|
124
|
+
# Whether +words+ run against a remote: the command, its subcommand
|
|
125
|
+
# and the flag each listed SUPPORTED.
|
|
126
|
+
def supported?(*words) = (1..words.size).all? { |n| status(*words.first(n)) == SUPPORTED }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../session_access"
|
|
4
|
+
require_relative "contract"
|
|
5
|
+
require_relative "../remotes"
|
|
6
|
+
require_relative "../cli/exit"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
module SessionAccess
|
|
10
|
+
# bin/chi's front for chi --remote NAME … and CHI_REMOTE=NAME: takes the
|
|
11
|
+
# flag off the front of the arguments (before the subcommand lookup),
|
|
12
|
+
# names the remote (the flag beats the env; no default remote), refuses
|
|
13
|
+
# what runs on this machine only (Contract) and runs a session command
|
|
14
|
+
# with a Remote for its SessionAccess. chi remote itself, --version and
|
|
15
|
+
# --help always run here.
|
|
16
|
+
class Dispatch
|
|
17
|
+
FLAG = "--remote"
|
|
18
|
+
ENV_KEY = "CHI_REMOTE"
|
|
19
|
+
# Run here whatever the remote: chi remote manages the remotes.
|
|
20
|
+
ALWAYS_HERE = %w[remote].freeze
|
|
21
|
+
# bin/chi's own words besides its SUBCOMMANDS (a refusal names them).
|
|
22
|
+
OWN_WORDS = %w[web scratch self].freeze
|
|
23
|
+
HELP = %w[--version -h --help].freeze
|
|
24
|
+
# The run flags (not subcommands) a refusal names: remote attach is
|
|
25
|
+
# its own phase (P4).
|
|
26
|
+
RUN_FLAGS = { "--attach" => "--attach", "--resume" => "--resume", "-p" => "-p", "--prompt" => "-p",
|
|
27
|
+
"--shared" => "--shared", "--no-shared" => "--no-shared" }.freeze
|
|
28
|
+
|
|
29
|
+
# A usage error in the flag itself (--remote with no name, a bad one).
|
|
30
|
+
class Usage < StandardError; end
|
|
31
|
+
|
|
32
|
+
# The flag (any number of them at the front; the last wins) taken off
|
|
33
|
+
# +argv+, and the remote it and +env+ name.
|
|
34
|
+
# @return [Dispatch]
|
|
35
|
+
# @raise [Usage]
|
|
36
|
+
def self.from(argv, env: ENV)
|
|
37
|
+
rest = argv.dup
|
|
38
|
+
flag = nil
|
|
39
|
+
while (arg = rest.first) && (arg == FLAG || arg.start_with?("#{FLAG}="))
|
|
40
|
+
rest.shift
|
|
41
|
+
flag = arg == FLAG ? rest.shift : arg.delete_prefix("#{FLAG}=")
|
|
42
|
+
raise Usage, "#{FLAG} needs a remote's name (chi remote ls lists them)" if flag.nil? || flag.strip.empty? || flag.start_with?("-")
|
|
43
|
+
end
|
|
44
|
+
from_env = env.fetch(ENV_KEY, "").to_s.strip
|
|
45
|
+
new(rest, remote: flag || (from_env.empty? ? nil : from_env))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_reader :argv, :remote
|
|
49
|
+
|
|
50
|
+
def initialize(argv, remote:)
|
|
51
|
+
@argv = argv
|
|
52
|
+
@remote = remote
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def remote? = !@remote.nil?
|
|
56
|
+
|
|
57
|
+
# Whether the arguments run here as they would without a remote: chi
|
|
58
|
+
# remote, --version, --help.
|
|
59
|
+
def here?
|
|
60
|
+
first = @argv.first
|
|
61
|
+
ALWAYS_HERE.include?(first) || (@argv.size == 1 && HELP.include?(first))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The one refusal of a command or flag that runs on this machine only,
|
|
65
|
+
# or nil when the Contract lets it run against the remote.
|
|
66
|
+
# @return [String, nil]
|
|
67
|
+
def refusal
|
|
68
|
+
words = local_words or return nil
|
|
69
|
+
|
|
70
|
+
"chi #{words} runs on this machine only: run it on #{@remote}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The session command against the remote: refused (exit 2), or run
|
|
74
|
+
# with a Remote. A failure to reach it is the command's line, exit 1.
|
|
75
|
+
# @param subcommands [Hash] bin/chi's SUBCOMMANDS
|
|
76
|
+
# @return [Integer] the exit status
|
|
77
|
+
def run(subcommands, stderr: $stderr, transport: nil)
|
|
78
|
+
command = @argv.first
|
|
79
|
+
unless command.nil? || command.start_with?("-") || subcommands.key?(command) || OWN_WORDS.include?(command)
|
|
80
|
+
stderr.puts("Error: unknown command #{command} (see chi --help)")
|
|
81
|
+
return CLI::Exit::FAILED
|
|
82
|
+
end
|
|
83
|
+
if (message = refusal)
|
|
84
|
+
stderr.puts("Error: #{message}")
|
|
85
|
+
return CLI::Exit::USAGE
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
entry = Remotes.find(Remotes.check_name!(@remote))
|
|
89
|
+
path, klass = subcommands.fetch(@argv.first)
|
|
90
|
+
require path
|
|
91
|
+
access = Remote.new(entry, transport: transport)
|
|
92
|
+
Samagotchi.const_get(klass).new(@argv[1..] || [], access: access).run
|
|
93
|
+
rescue Remotes::Invalid => e
|
|
94
|
+
stderr.puts("Error: #{e.message}")
|
|
95
|
+
CLI::Exit::USAGE
|
|
96
|
+
rescue RemoteFailed => e
|
|
97
|
+
stderr.puts("chi #{@argv.first}: #{e.message}")
|
|
98
|
+
CLI::Exit::FAILED
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# The words that run here only: the command (and its subcommand, a
|
|
104
|
+
# flag) the Contract lists local_only, a machine command, or the run
|
|
105
|
+
# flags (a session in this terminal, attach). nil: supported.
|
|
106
|
+
def local_words
|
|
107
|
+
command = @argv.first
|
|
108
|
+
return run_words if command.nil? || command.start_with?("-")
|
|
109
|
+
return command if Contract::MACHINE.include?(command) || Contract.status(command).nil?
|
|
110
|
+
return command if Contract.status(command) == Contract::LOCAL_ONLY
|
|
111
|
+
|
|
112
|
+
sub = @argv[1] if Contract::TABLE.key?("#{command} #{@argv[1]}")
|
|
113
|
+
return "#{command} #{sub}" if sub && Contract.status(command, sub) == Contract::LOCAL_ONLY
|
|
114
|
+
|
|
115
|
+
prefix = [command, sub].compact
|
|
116
|
+
flags.each do |flag|
|
|
117
|
+
return [*prefix, flag].join(" ") if Contract.status(*prefix, flag) == Contract::LOCAL_ONLY
|
|
118
|
+
end
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The long flags given, up to a "--".
|
|
123
|
+
def flags
|
|
124
|
+
@argv.drop(1).take_while { |arg| arg != "--" }.select { |arg| arg.start_with?("--") }.map { |arg| arg.split("=", 2).first }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def run_words
|
|
128
|
+
flag = @argv.take_while { |arg| arg != "--" }.map { |arg| arg.split("=", 2).first }.find { |arg| RUN_FLAGS.key?(arg) }
|
|
129
|
+
flag ? RUN_FLAGS[flag] : "(a session in this terminal)"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require_relative "../session_access"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
module SessionAccess
|
|
11
|
+
# A remote's chi web over HTTP (Net::HTTP), its access token as
|
|
12
|
+
# Authorization: Bearer: one request per call, each on its own
|
|
13
|
+
# connection (a command makes a handful). A connection that fails
|
|
14
|
+
# raises RemoteFailed naming the remote and why.
|
|
15
|
+
class HTTPTransport
|
|
16
|
+
# One answer: the status, the body, and the body read as JSON (nil
|
|
17
|
+
# when it isn't).
|
|
18
|
+
Reply = Data.define(:status, :body, :json) do
|
|
19
|
+
def self.of(status, body)
|
|
20
|
+
json = begin
|
|
21
|
+
JSON.parse(body.to_s)
|
|
22
|
+
rescue JSON::ParserError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
new(status: status.to_i, body: body.to_s, json: json)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def error = json.is_a?(Hash) ? json["error"] : nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
OPEN_TIMEOUT = 5
|
|
32
|
+
# Above chi web's longest answer (a wait route's 30 s, a create's
|
|
33
|
+
# wait for its worker).
|
|
34
|
+
READ_TIMEOUT = 75
|
|
35
|
+
|
|
36
|
+
# @param entry [Remotes::Entry]
|
|
37
|
+
def initialize(entry, open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT)
|
|
38
|
+
@entry = entry
|
|
39
|
+
@uri = URI(entry.url)
|
|
40
|
+
@open_timeout = open_timeout
|
|
41
|
+
@read_timeout = read_timeout
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspect = "#<#{self.class.name} #{@entry.label}>"
|
|
45
|
+
|
|
46
|
+
# @param method [Symbol] :get, :post, :delete
|
|
47
|
+
# @param path [String] "/api/…"
|
|
48
|
+
# @param query [Hash, nil] the query's parameters (nil values left out)
|
|
49
|
+
# @param body [Hash, nil] sent as JSON
|
|
50
|
+
# @param raw [String, nil] sent as is (an image), with +content_type+
|
|
51
|
+
# @param read_timeout [Numeric, nil] this request's, instead of the
|
|
52
|
+
# transport's (a remote attach's turn, its join reads)
|
|
53
|
+
# @return [Reply]
|
|
54
|
+
# @raise [RemoteFailed] it could not be asked
|
|
55
|
+
def call(method, path, query: nil, body: nil, raw: nil, content_type: nil, read_timeout: nil)
|
|
56
|
+
read_timeout ||= @read_timeout
|
|
57
|
+
request = build(method, path, query)
|
|
58
|
+
request["Authorization"] = "Bearer #{@entry.token}" if @entry.token
|
|
59
|
+
request["Accept"] = "application/json"
|
|
60
|
+
if raw
|
|
61
|
+
request["Content-Type"] = content_type || "application/octet-stream"
|
|
62
|
+
request.body = raw
|
|
63
|
+
elsif body
|
|
64
|
+
request["Content-Type"] = "application/json"
|
|
65
|
+
request.body = JSON.generate(body)
|
|
66
|
+
elsif method != :get
|
|
67
|
+
request["Content-Length"] = "0"
|
|
68
|
+
end
|
|
69
|
+
response = http(read_timeout).request(request)
|
|
70
|
+
Reply.of(response.code, response.body)
|
|
71
|
+
rescue Errno::ECONNREFUSED
|
|
72
|
+
failed("connection refused")
|
|
73
|
+
rescue Net::OpenTimeout, Errno::ETIMEDOUT
|
|
74
|
+
failed("no answer within #{@open_timeout} s")
|
|
75
|
+
rescue Net::ReadTimeout
|
|
76
|
+
failed("it took the request but sent no answer within #{read_timeout} s")
|
|
77
|
+
rescue SocketError => e
|
|
78
|
+
failed(e.message.include?("getaddrinfo") ? "can't find host #{@uri.host}" : e.message)
|
|
79
|
+
rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
|
|
80
|
+
failed("host unreachable")
|
|
81
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
82
|
+
failed("TLS failed: #{e.message}")
|
|
83
|
+
rescue IOError, SystemCallError => e
|
|
84
|
+
failed("the connection broke (#{e.class.name.split("::").last})")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def http(read_timeout)
|
|
90
|
+
Net::HTTP.new(@uri.host, @uri.port).tap do |http|
|
|
91
|
+
http.use_ssl = @uri.scheme == "https"
|
|
92
|
+
http.open_timeout = @open_timeout
|
|
93
|
+
http.read_timeout = read_timeout
|
|
94
|
+
http.write_timeout = read_timeout
|
|
95
|
+
http.max_retries = 0
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build(method, path, query)
|
|
100
|
+
target = path
|
|
101
|
+
params = query&.compact
|
|
102
|
+
target = "#{path}?#{URI.encode_www_form(params)}" if params && !params.empty?
|
|
103
|
+
{ get: Net::HTTP::Get, post: Net::HTTP::Post, delete: Net::HTTP::Delete }.fetch(method).new(target)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def failed(reason) = raise(RemoteFailed, "#{@entry.label}: #{reason}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|