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
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Hitl
|
|
5
|
-
module Proposals
|
|
6
|
-
# Authenticated proposer wake. The existing Hermes actor performs
|
|
7
|
-
# reconciliation under its own transport identity after polling.
|
|
8
|
-
class Evaluator
|
|
9
|
-
def initialize(boundary:, project:)
|
|
10
|
-
@boundary, @project = boundary, project
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def call
|
|
14
|
-
results = []
|
|
15
|
-
after = nil
|
|
16
|
-
loop do
|
|
17
|
-
page = @boundary.proposal_wake(project: @project, after: after)
|
|
18
|
-
results.concat(page.fetch("items"))
|
|
19
|
-
cursor = page.fetch("next")
|
|
20
|
-
break unless cursor
|
|
21
|
-
raise Lifecycle::TransportError, "proposal cursor did not advance" unless cursor.is_a?(String) && (!after || cursor > after)
|
|
22
|
-
after = cursor
|
|
23
|
-
end
|
|
24
|
-
results
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "digest"
|
|
5
|
-
require "time"
|
|
6
|
-
require "ace/hitl/contract/secret_gate"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Hitl
|
|
10
|
-
module Proposals
|
|
11
|
-
# HITL owns decisions. This policy never claims or executes an effect.
|
|
12
|
-
module Policy
|
|
13
|
-
WINDOW = 16 * 60 * 60
|
|
14
|
-
APPROVED = %w[approved-explicitly approved-by-silence].freeze
|
|
15
|
-
REQUIRED = %w[operation target candidate_head input_digest context options recommendation prerequisites].freeze
|
|
16
|
-
|
|
17
|
-
module_function
|
|
18
|
-
|
|
19
|
-
def document(value)
|
|
20
|
-
data = JSON.parse(JSON.generate(value))
|
|
21
|
-
unless data.is_a?(Hash) && (REQUIRED - data.keys).empty? &&
|
|
22
|
-
(data.keys - REQUIRED - ["rationale"]).empty?
|
|
23
|
-
raise Lifecycle::StateError, "proposal fields differ from proposal/v1"
|
|
24
|
-
end
|
|
25
|
-
unless data["operation"].is_a?(String) && data["operation"].match?(/\A[a-z][a-z0-9-]{0,63}\z/) &&
|
|
26
|
-
data["candidate_head"].is_a?(String) && data["candidate_head"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
27
|
-
data["input_digest"].is_a?(String) && data["input_digest"].match?(/\A[0-9a-f]{64}\z/)
|
|
28
|
-
raise Lifecycle::StateError, "proposal must bind exact operation, candidate head and input digest"
|
|
29
|
-
end
|
|
30
|
-
target = data["target"]
|
|
31
|
-
unless target.is_a?(Hash) && (target.keys - %w[resource artifact_digest]).empty? &&
|
|
32
|
-
target["resource"].is_a?(String) && target["resource"].match?(/\A[a-zA-Z0-9_.:\/-]{1,256}\z/) &&
|
|
33
|
-
(!target["artifact_digest"] || (target["artifact_digest"].is_a?(String) &&
|
|
34
|
-
target["artifact_digest"].match?(/\A[0-9a-f]{64}\z/)))
|
|
35
|
-
raise Lifecycle::StateError, "proposal target must name exact resource and optional artifact digest"
|
|
36
|
-
end
|
|
37
|
-
target["artifact_digest"] ||= nil
|
|
38
|
-
%w[context recommendation].each { |key| text!(data[key], key) }
|
|
39
|
-
text!(data["rationale"], "rationale") if data.key?("rationale")
|
|
40
|
-
%w[options prerequisites].each do |key|
|
|
41
|
-
unless data[key].is_a?(Array) && data[key].length <= 8 && (key != "options" || !data[key].empty?)
|
|
42
|
-
raise Lifecycle::StateError, "proposal #{key} must be a bounded list"
|
|
43
|
-
end
|
|
44
|
-
data[key].each { |item| text!(item, key) }
|
|
45
|
-
end
|
|
46
|
-
if JSON.generate(data).bytesize > 3500 || Contract::SecretGate::PATTERN.match?(JSON.generate(data))
|
|
47
|
-
raise Lifecycle::StateError, "proposal must be bounded and non-secret"
|
|
48
|
-
end
|
|
49
|
-
data
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def text!(value, field)
|
|
53
|
-
unless value.is_a?(String) && value.valid_encoding? && !value.strip.empty? &&
|
|
54
|
-
value.bytesize <= 1024 && !value.include?("\0")
|
|
55
|
-
raise Lifecycle::StateError, "invalid proposal #{field}"
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def timestamp(value)
|
|
60
|
-
time = Time.iso8601(value.to_s)
|
|
61
|
-
raise Lifecycle::StateError, "proposal time must be canonical UTC" unless time.utc.iso8601 == value
|
|
62
|
-
time
|
|
63
|
-
rescue ArgumentError
|
|
64
|
-
raise Lifecycle::StateError, "invalid proposal timestamp"
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def acknowledge(record, delivered_at)
|
|
68
|
-
timestamp(delivered_at)
|
|
69
|
-
return record if record["delivered_at"] == delivered_at
|
|
70
|
-
unless record["state"] == "awaiting-delivery" && !record["delivered_at"]
|
|
71
|
-
raise Lifecycle::StateError, "proposal delivery acknowledgement changed"
|
|
72
|
-
end
|
|
73
|
-
record.merge("state" => "awaiting-decision", "delivered_at" => delivered_at,
|
|
74
|
-
"deadline" => (timestamp(delivered_at) + WINDOW).iso8601)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def reply(record, answer:, received_at:, sequence:, claimed:, applied: nil)
|
|
78
|
-
timestamp(received_at)
|
|
79
|
-
raise Lifecycle::StateError, "proposal reply has no admitted ingress" unless sequence.is_a?(Integer) && sequence.positive?
|
|
80
|
-
digest = Digest::SHA256.hexdigest(JSON.generate([sequence, received_at, answer]))
|
|
81
|
-
applied ||= record if record["reply_sequence"] == sequence
|
|
82
|
-
if applied
|
|
83
|
-
raise Lifecycle::StateError, "duplicate ingress sequence changed content" unless applied["reply_digest"] == digest
|
|
84
|
-
return record
|
|
85
|
-
end
|
|
86
|
-
return record if %w[superseded denied].include?(record["state"])
|
|
87
|
-
raise Lifecycle::StateError, "proposal has no acknowledged delivery" unless record["delivered_at"]
|
|
88
|
-
text!(answer, "answer")
|
|
89
|
-
Lifecycle::Kinds.check_answer!("proposal", answer)
|
|
90
|
-
verb, rationale = answer.strip.split(/\s+/, 2)
|
|
91
|
-
decision = {"approve" => "approved-explicitly", "veto" => "denied", "clarify" => "superseded"}[verb.downcase]
|
|
92
|
-
# Scope changes and all unrecognized replies stop automatic approval.
|
|
93
|
-
decision ||= "superseded"
|
|
94
|
-
if claimed
|
|
95
|
-
decision = record["state"]
|
|
96
|
-
elsif APPROVED.include?(record["state"]) && verb.downcase == "approve"
|
|
97
|
-
decision = record["state"]
|
|
98
|
-
end
|
|
99
|
-
result = record.merge("state" => decision, "last_sequence" => [record.fetch("last_sequence", 0), sequence].max,
|
|
100
|
-
"reply_sequence" => sequence, "reply_digest" => digest,
|
|
101
|
-
"captain_answer" => answer, "received_at" => received_at)
|
|
102
|
-
result.delete("captain_rationale")
|
|
103
|
-
result["captain_rationale"] = rationale if rationale && !rationale.strip.empty?
|
|
104
|
-
result["stop_requested"] = true if claimed && verb.downcase != "approve"
|
|
105
|
-
result["resolution"] = claimed ? "effect-already-claimed; stop remaining safely stoppable work" : decision
|
|
106
|
-
result
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def resolve(record, checkpoint:, now:)
|
|
110
|
-
return record unless record["state"] == "awaiting-decision"
|
|
111
|
-
return record unless record["silence_delegated"] == true
|
|
112
|
-
return record if timestamp(now) < timestamp(record.fetch("deadline"))
|
|
113
|
-
valid = checkpoint["schema"] == "ace.hitl.hermes.ingress-checkpoint/v1" &&
|
|
114
|
-
checkpoint["project"].is_a?(String) && checkpoint["project"] == record["project_id"] &&
|
|
115
|
-
checkpoint["request"] == record["request_id"] && checkpoint["revision"] == record["revision_id"] &&
|
|
116
|
-
checkpoint["healthy"] == true && checkpoint["drained"] == true &&
|
|
117
|
-
checkpoint.dig("checkpoint", "through") == record["deadline"] &&
|
|
118
|
-
checkpoint.dig("checkpoint", "sequence").is_a?(Integer) &&
|
|
119
|
-
checkpoint.dig("checkpoint", "sequence") >= record.fetch("last_sequence", 0)
|
|
120
|
-
return record unless valid
|
|
121
|
-
record.merge("state" => "approved-by-silence", "resolution" => "approved-by-silence",
|
|
122
|
-
"resolved_at" => now, "ingress_checkpoint" => checkpoint["checkpoint"])
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/assign/organisms/attempt_coordinator"
|
|
4
|
-
require_relative "../../lifecycle/binding"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Hitl
|
|
8
|
-
module Providers
|
|
9
|
-
class Lab
|
|
10
|
-
# The managed binding policy (spec 8wq.t.34i): requests bind to
|
|
11
|
-
# the exact ACTIVE MANAGED attempt of the requesting actor,
|
|
12
|
-
# verified by the authoritative ace-assign coordinator under the
|
|
13
|
-
# assignment exclusion. with_active HOLDS that exclusion across
|
|
14
|
-
# the caller's locked transition, so an attempt that ends
|
|
15
|
-
# concurrently cancels the request instead of handing it fresh
|
|
16
|
-
# authority. Every doubt maps to the classified BindingError —
|
|
17
|
-
# unknown identity/authority is an error, never permission.
|
|
18
|
-
class AssignmentBinding < Lifecycle::Binding
|
|
19
|
-
def initialize(coordinator: nil, repo_root: nil, project: nil, owner_guard: nil)
|
|
20
|
-
unless (project.nil? && owner_guard.nil?) ||
|
|
21
|
-
(project.is_a?(String) && !project.empty? && owner_guard.respond_to?(:call))
|
|
22
|
-
raise ArgumentError, "installed binding needs both a fixed project and its owner guard"
|
|
23
|
-
end
|
|
24
|
-
@coordinator = coordinator
|
|
25
|
-
@repo_root = repo_root
|
|
26
|
-
@project, @owner_guard = project, owner_guard
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def validate_request(assignment:, attempt:, project:, requester:, caller_pid: nil)
|
|
30
|
-
with_verified(assignment, attempt, project, requester) do
|
|
31
|
-
reverse_address(assignment: assignment, attempt: attempt, project: project, caller_pid: caller_pid)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Read-only owner verification can run inside an already-held
|
|
36
|
-
# assignment exclusion without reacquiring that exclusion.
|
|
37
|
-
def reverse_address(assignment:, attempt:, project:, caller_pid:)
|
|
38
|
-
verify_selection!(project)
|
|
39
|
-
unless caller_pid.is_a?(Integer) && caller_pid.positive?
|
|
40
|
-
raise Lifecycle::BindingError, "kernel requester process identity is unavailable"
|
|
41
|
-
end
|
|
42
|
-
binding = coordinator.runtime_binding(attempt_id: attempt, caller_pid: caller_pid)
|
|
43
|
-
{"schema" => Ref::SCHEMA, "session" => binding.fetch("session"), "pane" => binding.fetch("pane")}
|
|
44
|
-
rescue Ace::Assign::Error => e
|
|
45
|
-
raise Lifecycle::BindingError, "native requester authority is unavailable (#{e.message})"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# The liveness scope: the assignment exclusion is HELD across
|
|
49
|
-
# the yielded transition commit.
|
|
50
|
-
def with_active(assignment:, attempt:, project: nil, requester: nil)
|
|
51
|
-
with_verified(assignment, attempt, project, requester) { yield }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def proposal_journal(project:)
|
|
55
|
-
unless project.is_a?(String) && !project.empty?
|
|
56
|
-
raise Lifecycle::BindingError, "proposal requires an explicit project"
|
|
57
|
-
end
|
|
58
|
-
verify_selection!(project)
|
|
59
|
-
coordinator.send(:journal_for)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def proposal_projects
|
|
63
|
-
if @project
|
|
64
|
-
verify_selection!(@project)
|
|
65
|
-
return [@project]
|
|
66
|
-
end
|
|
67
|
-
coordinator.send(:journal_for).proposals.map { |record| record.fetch("project_id") }.uniq.sort
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def verify_selection!(project)
|
|
73
|
-
return unless @project
|
|
74
|
-
raise Lifecycle::BindingError, "project differs from installed HITL owner" unless project == @project
|
|
75
|
-
@owner_guard.call
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def with_verified(assignment_id, attempt_id, project_id, requester)
|
|
79
|
-
# ONLY the authority verification is error-mapped: the
|
|
80
|
-
# caller's transition (answer hand-off, commits, effects)
|
|
81
|
-
# keeps its own failure classes — mapping it into
|
|
82
|
-
# BindingError would cancel a live request on an unrelated
|
|
83
|
-
# failure (review 8x327bud). The exclusion still spans the
|
|
84
|
-
# whole transition, so the phase flag separates the two.
|
|
85
|
-
verifying = true
|
|
86
|
-
verify_selection!(project_id)
|
|
87
|
-
coordinator.with_verified_attempt(
|
|
88
|
-
assignment_id: assignment_id,
|
|
89
|
-
attempt_id: attempt_id,
|
|
90
|
-
project_id: project_id,
|
|
91
|
-
requester: requester
|
|
92
|
-
) do |attempt|
|
|
93
|
-
verify_selection!(project_id)
|
|
94
|
-
verifying = false
|
|
95
|
-
yield attempt
|
|
96
|
-
end
|
|
97
|
-
rescue Ace::Assign::Error => e
|
|
98
|
-
raise verifying ? map_error(e) : e
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def coordinator
|
|
102
|
-
@coordinator ||= Ace::Assign::Organisms::AttemptCoordinator.new(repo_root: @repo_root)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def map_error(error)
|
|
106
|
-
message = error.message.to_s
|
|
107
|
-
case error.class.name
|
|
108
|
-
when /NotFound/
|
|
109
|
-
# An unknown attempt id is a verifiably dead binding.
|
|
110
|
-
Lifecycle::EndedAttemptError.new("HITL attempt is unknown: #{message}")
|
|
111
|
-
when /ReceiptRejected/
|
|
112
|
-
Lifecycle::EndedAttemptError.new(message)
|
|
113
|
-
when /UnauthorizedIdentity/
|
|
114
|
-
# A foreign actor on the record means the attempt moved on.
|
|
115
|
-
Lifecycle::EndedAttemptError.new("HITL requester does not own the attempt: #{message}")
|
|
116
|
-
when /InvalidState/
|
|
117
|
-
Lifecycle::EndedAttemptError.new(message)
|
|
118
|
-
else
|
|
119
|
-
Lifecycle::BindingError.new(message)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "etc"
|
|
3
|
-
require_relative "../../lifecycle/binding"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Hitl
|
|
7
|
-
module Providers
|
|
8
|
-
class Lab
|
|
9
|
-
# The deployed shared service uses the original protected launch owner;
|
|
10
|
-
# standalone ordinary HITL continues to select its explicit coordinator.
|
|
11
|
-
class ProtectedAssignmentBinding < Lifecycle::Binding
|
|
12
|
-
def initialize(authority:, kernel:, journals:)
|
|
13
|
-
@authority, @kernel = authority, kernel
|
|
14
|
-
@journals = journals.dup.freeze
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def validate_request(assignment:, attempt:, project:, requester:, caller_pid: nil)
|
|
18
|
-
peer = actual_peer!(caller_pid)
|
|
19
|
-
unless Etc.getpwuid(peer.fetch("uid")).name == requester
|
|
20
|
-
raise Lifecycle::BindingError, "HITL requester differs from kernel principal"
|
|
21
|
-
end
|
|
22
|
-
selected(assignment, attempt, project, peer.fetch("uid"), peer) { |reverse| reverse }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def reverse_address(assignment:, attempt:, project:, caller_pid:)
|
|
26
|
-
peer = actual_peer!(caller_pid)
|
|
27
|
-
selected(assignment, attempt, project, peer.fetch("uid"), peer) { |reverse| reverse }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def with_active(assignment:, attempt:, project:, requester:)
|
|
31
|
-
uid = Etc.getpwnam(requester).uid
|
|
32
|
-
selected(assignment, attempt, project, uid, nil) { yield }
|
|
33
|
-
rescue ArgumentError
|
|
34
|
-
raise Lifecycle::BindingError, "HITL requester is unavailable"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def proposal_journal(project:)
|
|
38
|
-
@journals.fetch(project)
|
|
39
|
-
rescue KeyError
|
|
40
|
-
raise Lifecycle::BindingError, "HITL project journal is not installed"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def proposal_projects
|
|
44
|
-
@journals.keys.sort.freeze
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def actual_peer!(pid)
|
|
50
|
-
unless pid.is_a?(Integer) && pid.positive?
|
|
51
|
-
raise Lifecycle::BindingError, "HITL kernel requester is unavailable"
|
|
52
|
-
end
|
|
53
|
-
peer = @kernel.capture(pid)
|
|
54
|
-
@kernel.live!(peer)
|
|
55
|
-
peer
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def selected(assignment, attempt, project, uid, peer, &block)
|
|
59
|
-
proposal_journal(project: project)
|
|
60
|
-
verifying = true
|
|
61
|
-
@authority.with_worker_hitl!(assignment: assignment, attempt: attempt,
|
|
62
|
-
project: project, requester_uid: uid, peer: peer) do |reverse|
|
|
63
|
-
verifying = false
|
|
64
|
-
block.call(reverse)
|
|
65
|
-
end
|
|
66
|
-
rescue Ace::Assign::AttemptErrors::InvalidState, Ace::Assign::AttemptErrors::NotFound => error
|
|
67
|
-
raise verifying ? Lifecycle::EndedAttemptError.new(error.message) : error
|
|
68
|
-
rescue Ace::Assign::Error, Ace::Runtime::RuntimeUnavailableError => error
|
|
69
|
-
raise verifying ? Lifecycle::BindingError.new("protected HITL owner unavailable (#{error.class.name})") : error
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/hitl/contract"
|
|
4
|
-
require_relative "lab"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Hitl
|
|
8
|
-
# HITL provider adapters (spec 8wm.t.vrz): pluggable ask/delivery
|
|
9
|
-
# transports behind one interface. Agent-facing ace-hitl paths resolve
|
|
10
|
-
# adapters through this registry only — never a concrete transport.
|
|
11
|
-
module Providers
|
|
12
|
-
REGISTRY = {
|
|
13
|
-
Lab::PROVIDER_NAME => -> { Lab.new }
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
module_function
|
|
17
|
-
|
|
18
|
-
def available
|
|
19
|
-
REGISTRY.keys.sort
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def resolve(name)
|
|
23
|
-
factory = REGISTRY[name.to_s]
|
|
24
|
-
unless factory
|
|
25
|
-
raise UnknownProviderError,
|
|
26
|
-
"unknown HITL provider '#{name}' (available: #{available.join(', ')})"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
factory.call
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|