ace-hitl 0.17.0 → 0.23.2
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 +162 -0
- data/README.md +60 -117
- data/docs/usage.md +247 -421
- data/handbook/workflow-instructions/hitl.wf.md +55 -6
- data/lib/ace/hitl/adapters/registry.rb +65 -0
- data/lib/ace/hitl/adapters/terminal.rb +75 -0
- data/lib/ace/hitl/adapters.rb +80 -0
- data/lib/ace/hitl/atoms/hitl_address.rb +89 -0
- data/lib/ace/hitl/atoms/hitl_event_text.rb +54 -0
- data/lib/ace/hitl/cli/commands/create.rb +81 -12
- data/lib/ace/hitl/cli/commands/lifecycle_command.rb +1 -7
- data/lib/ace/hitl/cli/commands/list.rb +22 -1
- data/lib/ace/hitl/cli/commands/send.rb +85 -0
- data/lib/ace/hitl/cli/commands/serve.rb +25 -44
- data/lib/ace/hitl/cli/commands/show.rb +23 -3
- data/lib/ace/hitl/cli/commands/update.rb +4 -0
- data/lib/ace/hitl/cli/commands/wait.rb +5 -0
- data/lib/ace/hitl/cli.rb +8 -18
- data/lib/ace/hitl/lifecycle/frame_serving.rb +0 -15
- data/lib/ace/hitl/lifecycle/kinds.rb +1 -1
- data/lib/ace/hitl/lifecycle/lifecycle_requests.rb +0 -36
- data/lib/ace/hitl/lifecycle/policy.rb +0 -44
- data/lib/ace/hitl/lifecycle/protocol.rb +1 -2
- data/lib/ace/hitl/lifecycle/service.rb +2 -3
- data/lib/ace/hitl/lifecycle/store.rb +11 -75
- data/lib/ace/hitl/lifecycle/tls_access_policy.rb +0 -14
- data/lib/ace/hitl/lifecycle/tls_project_scope.rb +0 -15
- data/lib/ace/hitl/lifecycle/tls_service.rb +2 -3
- data/lib/ace/hitl/lifecycle.rb +2 -4
- data/lib/ace/hitl/live_client.rb +11 -87
- data/lib/ace/hitl/models/hitl_event.rb +35 -0
- data/lib/ace/hitl/molecules/hitl_address_resolver.rb +80 -0
- data/lib/ace/hitl/molecules/hitl_config_loader.rb +22 -1
- data/lib/ace/hitl/molecules/hitl_creator.rb +126 -27
- data/lib/ace/hitl/molecules/hitl_delivery_state.rb +58 -0
- data/lib/ace/hitl/molecules/hitl_display_formatter.rb +3 -2
- data/lib/ace/hitl/molecules/hitl_loader.rb +19 -2
- data/lib/ace/hitl/molecules/hitl_route_loader.rb +108 -0
- data/lib/ace/hitl/organisms/hitl_manager.rb +73 -6
- data/lib/ace/hitl/organisms/hitl_sender.rb +148 -0
- data/lib/ace/hitl/providers/lab.rb +15 -129
- data/lib/ace/hitl/version.rb +1 -1
- data/lib/ace/hitl.rb +8 -1
- metadata +12 -43
- data/lib/ace/hitl/cli/commands/ask.rb +0 -136
- data/lib/ace/hitl/cli/commands/overseer_ack.rb +0 -30
- data/lib/ace/hitl/cli/commands/overseer_pending.rb +0 -28
- data/lib/ace/hitl/cli/commands/overseer_send.rb +0 -35
- data/lib/ace/hitl/cli/commands/proposal.rb +0 -75
- data/lib/ace/hitl/lifecycle/overseer.rb +0 -111
- data/lib/ace/hitl/lifecycle/proposals.rb +0 -297
- data/lib/ace/hitl/lifecycle/service_publication_binding.rb +0 -97
- data/lib/ace/hitl/proposals/evaluator.rb +0 -29
- data/lib/ace/hitl/proposals/policy.rb +0 -127
- data/lib/ace/hitl/providers/lab/assignment_binding.rb +0 -126
- data/lib/ace/hitl/providers/lab/protected_assignment_binding.rb +0 -75
- data/lib/ace/hitl/providers/providers.rb +0 -33
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Send < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "Send a HITL event through the configured adapter routes"
|
|
13
|
+
|
|
14
|
+
example [
|
|
15
|
+
"8xhkep # Deliver one event; per-route results",
|
|
16
|
+
"8xhkep --scope all # Resolve across worktrees"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
argument :ref, required: true, desc: "HITL reference (full ID or shortcut)"
|
|
20
|
+
|
|
21
|
+
option :scope, type: :string, desc: "Scope (current, all)"
|
|
22
|
+
option :force, type: :boolean, desc: "Redeliver every configured route (same case ID; recipients dedup)"
|
|
23
|
+
option :"dry-run", type: :boolean, desc: "Show retry/skip decisions without delivering"
|
|
24
|
+
|
|
25
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
26
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
27
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
28
|
+
|
|
29
|
+
def call(ref:, **options)
|
|
30
|
+
scope = validate_scope(options[:scope])
|
|
31
|
+
|
|
32
|
+
sender = Organisms::HitlSender.new
|
|
33
|
+
report = sender.send(ref, scope: scope,
|
|
34
|
+
force: options[:force] == true,
|
|
35
|
+
dry_run: options[:"dry-run"] == true)
|
|
36
|
+
|
|
37
|
+
unless options[:quiet]
|
|
38
|
+
puts "HITL event #{report.event_id} sent through #{report.results.size} route(s):"
|
|
39
|
+
report.results.each do |result|
|
|
40
|
+
detail = result.detail ? " - #{result.detail}" : ""
|
|
41
|
+
puts " #{result.route} (#{result.adapter}): #{result.status}#{detail}"
|
|
42
|
+
end
|
|
43
|
+
report.skipped.each do |result|
|
|
44
|
+
detail = result.detail ? " - #{result.detail}" : ""
|
|
45
|
+
puts " #{result.route} (#{result.adapter}): skipped#{detail}"
|
|
46
|
+
end
|
|
47
|
+
if report.results.empty? && report.skipped.any?
|
|
48
|
+
puts "already delivered on all routes; nothing to retry (use --force to redeliver)"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return if options[:"dry-run"] == true
|
|
53
|
+
|
|
54
|
+
unless report.all_delivered?
|
|
55
|
+
raise Ace::Support::Cli::Error.new(
|
|
56
|
+
"HITL event '#{report.event_id}' not delivered on all routes"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
rescue Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError => e
|
|
60
|
+
candidates = e.matches.map(&:file_path).join(", ")
|
|
61
|
+
raise Ace::Support::Cli::Error.new(
|
|
62
|
+
"HITL event '#{ref}' is ambiguous across scope '#{scope || "all"}'. Candidates: #{candidates}"
|
|
63
|
+
)
|
|
64
|
+
rescue Ace::Hitl::Adapters::Error,
|
|
65
|
+
Ace::Hitl::Molecules::HitlRouteLoader::NoRoutesError,
|
|
66
|
+
Ace::Hitl::Molecules::HitlRouteLoader::InvalidRouteError => e
|
|
67
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def validate_scope(raw_scope)
|
|
73
|
+
return nil if raw_scope.nil?
|
|
74
|
+
|
|
75
|
+
scope = raw_scope.to_s.strip
|
|
76
|
+
allowed = Ace::Hitl::Molecules::WorktreeScopeResolver::VALID_SCOPES
|
|
77
|
+
return scope if allowed.include?(scope)
|
|
78
|
+
|
|
79
|
+
raise Ace::Support::Cli::Error.new("Invalid scope '#{raw_scope}'. Allowed: #{allowed.join(", ")}")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -2,40 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
require "ace/support/cli"
|
|
4
4
|
require_relative "../../lifecycle"
|
|
5
|
-
require_relative "../../providers/
|
|
5
|
+
require_relative "../../providers/lab"
|
|
6
6
|
|
|
7
7
|
module Ace
|
|
8
8
|
module Hitl
|
|
9
9
|
module CLI
|
|
10
10
|
module Commands
|
|
11
|
-
# The
|
|
12
|
-
#
|
|
13
|
-
# Authorization facts come only from the trusted grants
|
|
14
|
-
# document; the store root and socket paths come from the
|
|
15
|
-
#
|
|
11
|
+
# The operator-only local scoped store boundary: serves explicitly
|
|
12
|
+
# granted nonsecret operator questions over a peer-authenticated
|
|
13
|
+
# UNIX socket. Authorization facts come only from the trusted grants
|
|
14
|
+
# document; the store root and socket paths come from the deployment
|
|
15
|
+
# (options/environment), never from caller payloads.
|
|
16
|
+
#
|
|
17
|
+
# The installed managed-attempt authority and the host/Incus mTLS
|
|
18
|
+
# crossing were removed with the old authority model.
|
|
16
19
|
class Serve < Ace::Support::Cli::Command
|
|
17
20
|
include Ace::Support::Cli::Base
|
|
18
21
|
|
|
19
|
-
desc "Run the
|
|
22
|
+
desc "Run the operator-only HITL store boundary service (peer-credential access)"
|
|
20
23
|
|
|
21
24
|
option :socket, type: :string, desc: "Boundary socket path (default: ACE_HITL_SOCKET or /run/lab/hitl.sock)"
|
|
22
25
|
option :group, type: :string,
|
|
23
26
|
desc: "Dedicated lifecycle group allowed to connect (required; root and lab-control are refused)"
|
|
24
27
|
option :store_root, type: :string, desc: "Private store root (default: ACE_HITL_STORE_ROOT or /run/lab/hitl)"
|
|
25
|
-
option :grants_path, type: :string,
|
|
28
|
+
option :grants_path, type: :string,
|
|
29
|
+
desc: "Trusted grants document (default: ACE_HITL_GRANTS_PATH or /etc/lab/ace-lab/authorization.yml)"
|
|
26
30
|
option :operator_only, type: :boolean,
|
|
27
31
|
desc: "Serve explicitly granted nonsecret operator questions without a managed execution authority"
|
|
28
|
-
option :repo_root, type: :string, desc: "
|
|
29
|
-
option :tls_authority, type: :string, desc: "
|
|
32
|
+
option :repo_root, type: :string, desc: "(removed) managed attempt authority repository"
|
|
33
|
+
option :tls_authority, type: :string, desc: "(removed) host/Incus mTLS crossing authority"
|
|
30
34
|
|
|
31
35
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
32
36
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
33
37
|
|
|
34
38
|
def call(**options)
|
|
35
|
-
if options[:
|
|
36
|
-
raise_cli_error(
|
|
39
|
+
if options[:repo_root] || options[:tls_authority]
|
|
40
|
+
raise_cli_error(
|
|
41
|
+
"managed and TLS serve were removed with the installed attempt authority; " \
|
|
42
|
+
"serve supports --operator-only only"
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
unless options[:operator_only]
|
|
46
|
+
raise_cli_error(
|
|
47
|
+
"the installed managed attempt authority is removed; serve requires --operator-only"
|
|
48
|
+
)
|
|
37
49
|
end
|
|
38
|
-
return serve_tls_authority(options) if options[:tls_authority]
|
|
39
50
|
|
|
40
51
|
group = dedicated_group(options)
|
|
41
52
|
policy = Providers::Lab.grants_policy(grants_path: grants_path(options))
|
|
@@ -46,15 +57,9 @@ module Ace
|
|
|
46
57
|
"fail every client endpoint verification"
|
|
47
58
|
)
|
|
48
59
|
end
|
|
49
|
-
binding_policy = if options[:operator_only]
|
|
50
|
-
raise_cli_error("operator-only serve cannot select a managed repository") if options[:repo_root]
|
|
51
|
-
Lifecycle::OperatorOnlyBinding.new
|
|
52
|
-
else
|
|
53
|
-
Providers::Lab.assignment_binding(repo_root: options[:repo_root])
|
|
54
|
-
end
|
|
55
60
|
service = Lifecycle::Service.new(
|
|
56
61
|
root: store_root(options),
|
|
57
|
-
binding:
|
|
62
|
+
binding: Lifecycle::OperatorOnlyBinding.new,
|
|
58
63
|
policy: policy,
|
|
59
64
|
socket_path: socket_path(options),
|
|
60
65
|
group: group,
|
|
@@ -67,30 +72,6 @@ module Ace
|
|
|
67
72
|
|
|
68
73
|
private
|
|
69
74
|
|
|
70
|
-
# The host/Incus crossing (spec 8xa.t.nh0): authority config,
|
|
71
|
-
# credential layout and TLS material are verified fail-closed
|
|
72
|
-
# BEFORE the loopback listener opens; the verified project-
|
|
73
|
-
# scoped client certificate is the only admitted peer.
|
|
74
|
-
def serve_tls_authority(options)
|
|
75
|
-
authority = Lifecycle::TlsAuthority.load(options[:tls_authority])
|
|
76
|
-
binding_policy = Providers::Lab.assignment_binding(repo_root: options[:repo_root])
|
|
77
|
-
service = Lifecycle::TlsService.new(
|
|
78
|
-
root: store_root(options),
|
|
79
|
-
binding: binding_policy,
|
|
80
|
-
project: authority.project_id,
|
|
81
|
-
credential: authority.credential,
|
|
82
|
-
decision_policy: Providers::Lab.grants_policy(grants_path: grants_path(options)),
|
|
83
|
-
ssl_context: authority.ssl_context!,
|
|
84
|
-
listen: authority.listen,
|
|
85
|
-
port: authority.port,
|
|
86
|
-
group: authority.service_group,
|
|
87
|
-
logger: options[:verbose] ? ->(message) { warn message } : nil
|
|
88
|
-
)
|
|
89
|
-
service.run
|
|
90
|
-
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
91
|
-
raise_cli_error(e.message)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
75
|
def dedicated_group(options)
|
|
95
76
|
group = options[:group].to_s
|
|
96
77
|
return group unless group.empty?
|
|
@@ -45,10 +45,30 @@ module Ace
|
|
|
45
45
|
puts "ID: #{event.id}"
|
|
46
46
|
puts "Title: #{event.title}"
|
|
47
47
|
puts "Status: #{event.status}"
|
|
48
|
-
puts "
|
|
48
|
+
puts "Type: #{event.type}"
|
|
49
|
+
puts "Kind: #{event.kind}" if event.question?
|
|
50
|
+
puts "Result: #{event.result}" if event.report?
|
|
49
51
|
puts "Tags: #{event.tags.join(", ")}"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
if event.recipient
|
|
53
|
+
puts "Recipient: #{Ace::Hitl::Atoms::HitlAddress.to_s(event.recipient)}"
|
|
54
|
+
end
|
|
55
|
+
if event.reply_to
|
|
56
|
+
puts "Reply-To: #{Ace::Hitl::Atoms::HitlAddress.to_s(event.reply_to)}"
|
|
57
|
+
end
|
|
58
|
+
if event.report?
|
|
59
|
+
puts "Report: #{event.content}"
|
|
60
|
+
else
|
|
61
|
+
puts "Questions: #{event.questions.join(" | ")}"
|
|
62
|
+
puts "Answer: #{event.answer || "(none)"}"
|
|
63
|
+
end
|
|
64
|
+
delivery = event.metadata["delivery"]
|
|
65
|
+
if delivery.is_a?(Hash) && !delivery.empty?
|
|
66
|
+
delivery.sort.each do |route, record|
|
|
67
|
+
detail = record["detail"].to_s.empty? ? "" : " - #{record["detail"]}"
|
|
68
|
+
attempted = record["attempted_at"] ? " (#{record["attempted_at"]})" : ""
|
|
69
|
+
puts "Delivery: #{route} (#{record["adapter"]}): #{record["status"]}#{detail}#{attempted}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
52
72
|
puts resolved_location if resolved_location
|
|
53
73
|
puts "Path: #{event.file_path}"
|
|
54
74
|
end
|
|
@@ -59,6 +59,8 @@ module Ace
|
|
|
59
59
|
raise Ace::Support::Cli::Error.new(
|
|
60
60
|
"HITL event '#{ref}' is ambiguous across scope '#{scope || "all"}'. Candidates: #{candidates}"
|
|
61
61
|
)
|
|
62
|
+
rescue Ace::Hitl::Organisms::HitlManager::NotAnswerableError => e
|
|
63
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
raise Ace::Support::Cli::Error.new("HITL event '#{ref}' not found") unless event
|
|
@@ -81,6 +83,8 @@ module Ace
|
|
|
81
83
|
puts "HITL event archived after successful dispatch."
|
|
82
84
|
when :managed_request
|
|
83
85
|
raise Ace::Support::Cli::Error.new("Managed requests require scoped live delivery; unscoped resume is prohibited")
|
|
86
|
+
when :report
|
|
87
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{event.id}' is a report; reports do not await an answer or resume")
|
|
84
88
|
when :no_answer
|
|
85
89
|
raise Ace::Support::Cli::Error.new("Cannot resume '#{event.id}' without an answer")
|
|
86
90
|
when :failed
|
|
@@ -47,6 +47,9 @@ module Ace
|
|
|
47
47
|
|
|
48
48
|
manager = Ace::Hitl::Organisms::HitlManager.new
|
|
49
49
|
event = manager.show(ref, scope: scope)&.dig(:event)
|
|
50
|
+
if event&.report?
|
|
51
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{event.id}' is a report; reports do not await an answer")
|
|
52
|
+
end
|
|
50
53
|
if event && event.metadata["lab_request_id"]
|
|
51
54
|
emit(Ace::Hitl::LiveClient.new(boundary: lifecycle_client).wait(
|
|
52
55
|
request: event.metadata.fetch("lab_request_id"), timeout: options[:timeout] || 0,
|
|
@@ -70,6 +73,8 @@ module Ace
|
|
|
70
73
|
puts "Resume: #{resume}" if resume
|
|
71
74
|
when :timeout
|
|
72
75
|
raise Ace::Support::Cli::Error.new("Timed out waiting for HITL event '#{ref}'")
|
|
76
|
+
when :report
|
|
77
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{ref}' is a report; reports do not await an answer")
|
|
73
78
|
else
|
|
74
79
|
raise Ace::Support::Cli::Error.new("HITL event '#{ref}' not found")
|
|
75
80
|
end
|
data/lib/ace/hitl/cli.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "ace/support/cli"
|
|
4
4
|
require_relative "../hitl/version"
|
|
5
5
|
require_relative "cli/commands/create"
|
|
6
|
-
require_relative "cli/commands/
|
|
6
|
+
require_relative "cli/commands/send"
|
|
7
7
|
require_relative "cli/commands/operator_question"
|
|
8
8
|
require_relative "cli/commands/read"
|
|
9
9
|
require_relative "cli/commands/show"
|
|
@@ -17,10 +17,6 @@ require_relative "cli/commands/pending"
|
|
|
17
17
|
require_relative "cli/commands/states"
|
|
18
18
|
require_relative "cli/commands/duty"
|
|
19
19
|
require_relative "cli/commands/serve"
|
|
20
|
-
require_relative "cli/commands/overseer_send"
|
|
21
|
-
require_relative "cli/commands/overseer_pending"
|
|
22
|
-
require_relative "cli/commands/overseer_ack"
|
|
23
|
-
require_relative "cli/commands/proposal"
|
|
24
20
|
|
|
25
21
|
module Ace
|
|
26
22
|
module Hitl
|
|
@@ -30,43 +26,40 @@ module Ace
|
|
|
30
26
|
PROGRAM_NAME = "ace-hitl"
|
|
31
27
|
|
|
32
28
|
REGISTERED_COMMANDS = [
|
|
33
|
-
["
|
|
34
|
-
["create", "Create HITL event"],
|
|
29
|
+
["create", "Create HITL event (question or report)"],
|
|
35
30
|
["operator-question", "Create a nonsecret question under an installed operator project grant"],
|
|
36
31
|
["read", "Read authenticated lifecycle request facts"],
|
|
37
|
-
["ask", "Ask a human via HITL and forward the request to the Lab"],
|
|
38
32
|
["show", "Show HITL event details"],
|
|
39
33
|
["list", "List HITL events"],
|
|
40
34
|
["update", "Update HITL event metadata or answer"],
|
|
35
|
+
["send", "Send a HITL event through the configured adapter routes"],
|
|
41
36
|
["wait", "Wait for an answer on a specific HITL event"],
|
|
42
37
|
["deliver", "Deliver an answer (stdin) to a pending HITL relay request"],
|
|
43
38
|
["consume", "Wait for and consume the answer of one own HITL relay request"],
|
|
44
39
|
["cancel", "Cancel one HITL relay request with an audited reason"],
|
|
45
40
|
["pending", "List answerable HITL relay requests (transport)"],
|
|
46
41
|
["states", "List public HITL lifecycle projections (transport)"],
|
|
47
|
-
["duty", "Project pending and escalated HITL requests (transport)"]
|
|
48
|
-
["overseer-send", "Queue a bounded, type-tagged Overseer response (stdin)"],
|
|
49
|
-
["overseer-pending", "List queued Overseer responses (host-broker)"],
|
|
50
|
-
["overseer-ack", "Acknowledge one relayed Overseer response"]
|
|
42
|
+
["duty", "Project pending and escalated HITL requests (transport)"]
|
|
51
43
|
].freeze
|
|
52
44
|
|
|
53
45
|
HELP_EXAMPLES = [
|
|
54
46
|
"ace-hitl list --status pending",
|
|
55
47
|
"ace-hitl show abc123 --content",
|
|
56
48
|
"ace-hitl create \"Which auth strategy?\" --kind decision",
|
|
57
|
-
"ace-hitl
|
|
49
|
+
"ace-hitl create \"Blocked on auth\" --kind decision --to overseer --task 8xg.t.sbz.1.1",
|
|
50
|
+
"ace-hitl create \"Work done\" --type report --result success --task 8xg.t.sbz.1.0",
|
|
58
51
|
"ace-hitl update abc123 --answer \"Use JWT with refresh tokens\"",
|
|
52
|
+
"ace-hitl send 8xhkep",
|
|
59
53
|
"ace-hitl wait abc123 --poll-every 600 --timeout 14400"
|
|
60
54
|
].freeze
|
|
61
55
|
|
|
62
56
|
register "create", CLI::Commands::Create
|
|
63
|
-
register "proposal", CLI::Commands::Proposal
|
|
64
|
-
register "ask", CLI::Commands::Ask
|
|
65
57
|
register "operator-question", CLI::Commands::OperatorQuestion
|
|
66
58
|
register "read", CLI::Commands::Read
|
|
67
59
|
register "show", CLI::Commands::Show
|
|
68
60
|
register "list", CLI::Commands::List
|
|
69
61
|
register "update", CLI::Commands::Update
|
|
62
|
+
register "send", CLI::Commands::Send
|
|
70
63
|
register "wait", CLI::Commands::Wait
|
|
71
64
|
register "deliver", CLI::Commands::Deliver
|
|
72
65
|
register "consume", CLI::Commands::Consume
|
|
@@ -75,9 +68,6 @@ module Ace
|
|
|
75
68
|
register "states", CLI::Commands::States
|
|
76
69
|
register "duty", CLI::Commands::Duty
|
|
77
70
|
register "serve", CLI::Commands::Serve
|
|
78
|
-
register "overseer-send", CLI::Commands::OverseerSend
|
|
79
|
-
register "overseer-pending", CLI::Commands::OverseerPending
|
|
80
|
-
register "overseer-ack", CLI::Commands::OverseerAck
|
|
81
71
|
|
|
82
72
|
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
83
73
|
gem_name: "ace-hitl",
|
|
@@ -43,21 +43,6 @@ module Ace
|
|
|
43
43
|
def execute(store, op, params)
|
|
44
44
|
case op
|
|
45
45
|
when "ping" then store.capabilities
|
|
46
|
-
when "proposal-create" then store.proposal_create(**symbolize(params))
|
|
47
|
-
when "proposal-show" then store.proposal_show(required(params, "id"), project: required(params, "project"), history_after: params.fetch("history_after", 0))
|
|
48
|
-
when "proposal-revise" then store.proposal_revise(required(params, "id"), project: required(params, "project"), expected_revision: required(params, "expected_revision"),
|
|
49
|
-
operation_id: required(params, "operation_id"), document: required(params, "document"))
|
|
50
|
-
when "proposal-ack"
|
|
51
|
-
store.proposal_acknowledge(required(params, "id"), project: required(params, "project"), submitted_at: required(params, "submitted_at"))
|
|
52
|
-
when "proposal-reply"
|
|
53
|
-
store.proposal_reply(required(params, "id"), project: required(params, "project"), answer: required(params, "answer"),
|
|
54
|
-
received_at: required(params, "received_at"), sequence: required(params, "sequence"))
|
|
55
|
-
when "proposal-reconcile"
|
|
56
|
-
store.proposal_reconcile(required(params, "id"), project: required(params, "project"), checkpoint: required(params, "checkpoint"))
|
|
57
|
-
when "proposal-wake" then store.proposal_wake(project: required(params, "project"), after: params["after"])
|
|
58
|
-
when "proposal-due" then store.proposal_due(project: required(params, "project"), after: params["after"])
|
|
59
|
-
when "proposal-history" then store.proposal_history(project: required(params, "project"),
|
|
60
|
-
query: params.fetch("query", ""), after: params["after"])
|
|
61
46
|
when "operator-question" then store.operator_question(**symbolize(params))
|
|
62
47
|
when "create" then store.create(**symbolize(params))
|
|
63
48
|
when "read" then store.read(required(params, "id"))
|
|
@@ -13,42 +13,6 @@ module Ace
|
|
|
13
13
|
request("operator-question", params.transform_keys(&:to_s))
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def proposal_create(**params)
|
|
17
|
-
request("proposal-create", params)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def proposal_show(id, project:, history_after: 0)
|
|
21
|
-
request("proposal-show", {"id" => id, "project" => project, "history_after" => history_after})
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def proposal_revise(id, project:, expected_revision:, operation_id:, document:)
|
|
25
|
-
request("proposal-revise", {"id" => id, "project" => project, "expected_revision" => expected_revision, "operation_id" => operation_id, "document" => document})
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def proposal_acknowledge(id, project:, submitted_at:)
|
|
29
|
-
request("proposal-ack", {"id" => id, "project" => project, "submitted_at" => submitted_at})
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def proposal_reply(id, project:, **params)
|
|
33
|
-
request("proposal-reply", {"id" => id, "project" => project}.merge(params))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def proposal_reconcile(id, project:, checkpoint:)
|
|
37
|
-
request("proposal-reconcile", {"id" => id, "project" => project, "checkpoint" => checkpoint})
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def proposal_wake(project:, after: nil)
|
|
41
|
-
request("proposal-wake", {"project" => project, "after" => after}.compact)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def proposal_due(project:, after: nil)
|
|
45
|
-
request("proposal-due", {"project" => project, "after" => after}.compact)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def proposal_history(project:, query: "", after: nil)
|
|
49
|
-
request("proposal-history", {"project" => project, "query" => query, "after" => after}.compact)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
16
|
def create(deadline: nil, **params)
|
|
53
17
|
request("create", params.transform_keys(&:to_s), deadline: deadline)
|
|
54
18
|
end
|
|
@@ -106,17 +106,6 @@ module Ace
|
|
|
106
106
|
false
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
def proposal?(_peer, project:)
|
|
110
|
-
false
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def proposal_silence?(caller_uid:, requester:, project:, operation:)
|
|
114
|
-
false
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def silence_operation?(project:, operation:)
|
|
118
|
-
false
|
|
119
|
-
end
|
|
120
109
|
# @return [Boolean] true when the peer may deliver answers and
|
|
121
110
|
# observe pending/states for the given project
|
|
122
111
|
def transport?(_peer, project: nil)
|
|
@@ -170,41 +159,8 @@ module Ace
|
|
|
170
159
|
uid.is_a?(Integer) ? uid : nil
|
|
171
160
|
end
|
|
172
161
|
|
|
173
|
-
# Decision admission is explicitly installed, never inferred from
|
|
174
|
-
# an ordinary requester or transport grant.
|
|
175
|
-
def proposal?(peer, project:)
|
|
176
|
-
current = document
|
|
177
|
-
proposal_principal?(peer.uid, project, current)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# A proposer grant allows asking. Only this separate, exact project
|
|
181
|
-
# operation list delegates a decision after the acknowledged window.
|
|
182
|
-
def proposal_silence?(caller_uid:, requester:, project:, operation:)
|
|
183
|
-
current = document
|
|
184
|
-
proposal_principal?(caller_uid, project, current) &&
|
|
185
|
-
delegated_operation?(project, operation, current)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def silence_operation?(project:, operation:)
|
|
189
|
-
delegated_operation?(project, operation, document)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
162
|
private
|
|
193
163
|
|
|
194
|
-
def proposal_principal?(uid, project, current)
|
|
195
|
-
uids = hitl_facts(current)["proposal_uids"]
|
|
196
|
-
projects = authorized_projects(uid, current)
|
|
197
|
-
uid.is_a?(Integer) && uids.is_a?(Array) && uids.include?(uid) && projects &&
|
|
198
|
-
(projects.empty? || projects.include?(project.to_s))
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def delegated_operation?(project, operation, current)
|
|
202
|
-
delegations = hitl_facts(current)["silence_operations"]
|
|
203
|
-
operations = delegations.is_a?(Hash) && delegations[project]
|
|
204
|
-
operations.is_a?(Array) && operation.is_a?(String) &&
|
|
205
|
-
operation.match?(/\A[a-z][a-z0-9-]{0,63}\z/) && operations.include?(operation)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
164
|
def document
|
|
209
165
|
@document || TrustedFile.read_yaml(@grants_path) || {}
|
|
210
166
|
end
|
|
@@ -14,8 +14,7 @@ module Ace
|
|
|
14
14
|
VERSION = 1
|
|
15
15
|
MAX_FRAME_BYTES = 64 * 1024
|
|
16
16
|
DEFAULT_DEADLINE_SECONDS = 10.0
|
|
17
|
-
OPERATIONS = %w[operator-question create read deliver consume cancel pending states ping
|
|
18
|
-
proposal-create proposal-show proposal-revise proposal-ack proposal-reply proposal-reconcile proposal-due proposal-history proposal-wake].freeze
|
|
17
|
+
OPERATIONS = %w[operator-question create read deliver consume cancel pending states ping].freeze
|
|
19
18
|
|
|
20
19
|
# Wire failures map to the transport error; classified lifecycle
|
|
21
20
|
# failures pass through under their own names.
|
|
@@ -41,7 +41,7 @@ module Ace
|
|
|
41
41
|
|
|
42
42
|
def initialize(root:, binding:, policy:, socket_path:, group: Store::DEFAULT_GROUP,
|
|
43
43
|
deadline_seconds: Protocol::DEFAULT_DEADLINE_SECONDS, vault: nil, logger: nil,
|
|
44
|
-
idle_seconds: DEFAULT_IDLE_SECONDS
|
|
44
|
+
idle_seconds: DEFAULT_IDLE_SECONDS)
|
|
45
45
|
@root = root
|
|
46
46
|
@binding = binding
|
|
47
47
|
@policy = policy
|
|
@@ -51,7 +51,6 @@ module Ace
|
|
|
51
51
|
@vault = vault || OtpVault::MemoryVault.new
|
|
52
52
|
@logger = logger
|
|
53
53
|
@idle_seconds = idle_seconds
|
|
54
|
-
@proposal_clock = proposal_clock
|
|
55
54
|
@max_connections = MAX_CONNECTIONS
|
|
56
55
|
@connections = 0
|
|
57
56
|
@handler_threads = []
|
|
@@ -249,7 +248,7 @@ module Ace
|
|
|
249
248
|
policy: @policy,
|
|
250
249
|
identity: peer,
|
|
251
250
|
vault: @vault,
|
|
252
|
-
|
|
251
|
+
stopping: -> { @stopping }, group: @group
|
|
253
252
|
)
|
|
254
253
|
end
|
|
255
254
|
|