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,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "ace/support/items"
|
|
5
|
+
require_relative "../adapters"
|
|
6
|
+
require_relative "../atoms/hitl_event_text"
|
|
7
|
+
require_relative "../molecules/hitl_route_loader"
|
|
8
|
+
require_relative "../molecules/hitl_delivery_state"
|
|
9
|
+
require_relative "hitl_manager"
|
|
10
|
+
|
|
11
|
+
module Ace
|
|
12
|
+
module Hitl
|
|
13
|
+
module Organisms
|
|
14
|
+
# Delivers one persisted HITL event through the configured adapter
|
|
15
|
+
# route set. The sender's interface is adapter-independent: routes
|
|
16
|
+
# come from configuration, every route is invoked independently, and
|
|
17
|
+
# per-route failures never prevent or substitute another route's
|
|
18
|
+
# delivery (no silent rerouting).
|
|
19
|
+
#
|
|
20
|
+
# Retention (8xg.t.sbz.1.3): each route's last outcome, failure
|
|
21
|
+
# reason, and attempt timestamp are persisted on the event file
|
|
22
|
+
# itself (`delivery.<route>` frontmatter) — the single state owner.
|
|
23
|
+
# Default redelivery retries only undelivered/unknown routes;
|
|
24
|
+
# `force:` redelivers every route with the SAME case ID and content
|
|
25
|
+
# (at-least-once: recipients dedup by case ID). Redelivery retries
|
|
26
|
+
# the delivery, never the assignment.
|
|
27
|
+
class HitlSender
|
|
28
|
+
SendReport = Struct.new(:event_id, :results, :skipped, keyword_init: true) do
|
|
29
|
+
def all_delivered?
|
|
30
|
+
results.all?(&:delivered?) && skipped.all?(&:delivered?)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
SkippedRoute = Struct.new(:route, :adapter, :status, :detail, keyword_init: true) do
|
|
35
|
+
def delivered?
|
|
36
|
+
status == :delivered
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(manager: nil, config: nil, adapters: Adapters, now_proc: nil)
|
|
41
|
+
@manager = manager || HitlManager.new(config: config)
|
|
42
|
+
@config = config || @manager.config
|
|
43
|
+
@adapters = adapters
|
|
44
|
+
@now_proc = now_proc || -> { Time.now.utc }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param ref [String] event reference (exact ID or unique shortcut)
|
|
48
|
+
# @param force [Boolean] redeliver every configured route
|
|
49
|
+
# @param dry_run [Boolean] report decisions only; no delivery, no write
|
|
50
|
+
# @return [SendReport]
|
|
51
|
+
# @raise [Molecules::HitlRouteLoader::NoRoutesError,
|
|
52
|
+
# Molecules::HitlRouteLoader::InvalidRouteError] before any delivery attempt
|
|
53
|
+
def send(ref, scope: nil, force: false, dry_run: false)
|
|
54
|
+
routes = Molecules::HitlRouteLoader.load(@config)
|
|
55
|
+
|
|
56
|
+
current = @manager.show(ref, scope: scope)
|
|
57
|
+
raise Adapters::Error, "HITL event '#{ref}' not found" unless current
|
|
58
|
+
|
|
59
|
+
event = current[:event]
|
|
60
|
+
routes = Molecules::HitlRouteLoader.for_recipient(routes, recipient: event.recipient, config: @config)
|
|
61
|
+
text = Atoms::HitlEventText.call(event)
|
|
62
|
+
delivery = event.metadata["delivery"]
|
|
63
|
+
|
|
64
|
+
attempted, skipped = partition_routes(routes, delivery, force)
|
|
65
|
+
|
|
66
|
+
if dry_run
|
|
67
|
+
results = attempted.map do |route|
|
|
68
|
+
reason = if force
|
|
69
|
+
"force - will redeliver"
|
|
70
|
+
elsif Molecules::HitlDeliveryState.unknown?(delivery&.dig(route["route"]))
|
|
71
|
+
"unknown result - will retry"
|
|
72
|
+
else
|
|
73
|
+
"undelivered - will retry"
|
|
74
|
+
end
|
|
75
|
+
Adapters::RouteResult.new(route: route["route"], adapter: route["adapter"],
|
|
76
|
+
status: :dry_run, detail: reason)
|
|
77
|
+
end
|
|
78
|
+
return SendReport.new(event_id: event.id, results: results, skipped: skipped)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
results = attempted.map do |route|
|
|
82
|
+
persist_delivery(event, route, "in_flight", nil)
|
|
83
|
+
deliver_route(route, event: event, text: text).tap do |result|
|
|
84
|
+
persist_delivery(event, route, result.status.to_s, result.detail)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
SendReport.new(event_id: event.id, results: results, skipped: skipped)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def partition_routes(routes, delivery, force)
|
|
94
|
+
attempted = []
|
|
95
|
+
skipped = []
|
|
96
|
+
routes.each do |route|
|
|
97
|
+
record = delivery.is_a?(Hash) ? delivery[route["route"]] : nil
|
|
98
|
+
if !force && !Molecules::HitlDeliveryState.retry?(record)
|
|
99
|
+
skipped << SkippedRoute.new(route: route["route"], adapter: route["adapter"],
|
|
100
|
+
status: :delivered, detail: "already delivered (#{record["attempted_at"]})")
|
|
101
|
+
else
|
|
102
|
+
attempted << route
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
[attempted, skipped]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Nested dot-keys through FieldUpdater: exclusive-flock atomic
|
|
109
|
+
# rewrite of the event's own file (single state owner).
|
|
110
|
+
def persist_delivery(event, route, status, detail)
|
|
111
|
+
Ace::Support::Items::Molecules::FieldUpdater.update(event.file_path,
|
|
112
|
+
set: {
|
|
113
|
+
"delivery.#{route["route"]}.status" => status,
|
|
114
|
+
"delivery.#{route["route"]}.adapter" => route["adapter"],
|
|
115
|
+
"delivery.#{route["route"]}.detail" => detail,
|
|
116
|
+
"delivery.#{route["route"]}.attempted_at" => @now_proc.call.iso8601
|
|
117
|
+
})
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# One route's failure is contained to its own result; the remaining
|
|
121
|
+
# routes still run and nothing is ever sent to a different
|
|
122
|
+
# destination than the route's own configuration.
|
|
123
|
+
def deliver_route(route, event:, text:)
|
|
124
|
+
adapter_name = route["adapter"]
|
|
125
|
+
route = route.merge("machine" => @config.dig("hitl", "machine").to_s) do |_key, route_val, config_val|
|
|
126
|
+
route_val.to_s.strip.empty? ? config_val : route_val
|
|
127
|
+
end
|
|
128
|
+
adapter = @adapters.resolve(adapter_name, route: route)
|
|
129
|
+
adapter.deliver(event: event, text: text,
|
|
130
|
+
recipient: event.recipient, reply_to: event.reply_to)
|
|
131
|
+
rescue Adapters::UnknownAdapterError => e
|
|
132
|
+
# The route's adapter cannot be resolved: record it as
|
|
133
|
+
# unavailable with the reason (never a bare in_flight marker)
|
|
134
|
+
# and keep going so healthy routes still deliver.
|
|
135
|
+
Adapters::RouteResult.new(
|
|
136
|
+
route: route["route"], adapter: adapter_name,
|
|
137
|
+
status: :unavailable, detail: e.message
|
|
138
|
+
)
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
Adapters::RouteResult.new(
|
|
141
|
+
route: route["route"], adapter: adapter_name,
|
|
142
|
+
status: :failed, detail: "#{e.class}: #{e.message}"
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "securerandom"
|
|
4
3
|
require "ace/hitl/contract"
|
|
5
|
-
require_relative "lab/assignment_binding"
|
|
6
4
|
require_relative "../lifecycle/store"
|
|
7
5
|
require_relative "../lifecycle/policy"
|
|
8
6
|
require_relative "../lifecycle/client"
|
|
@@ -10,47 +8,26 @@ require_relative "../lifecycle/client"
|
|
|
10
8
|
module Ace
|
|
11
9
|
module Hitl
|
|
12
10
|
module Providers
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# it (guard-tested).
|
|
11
|
+
# Deployment facts for the host-installed HITL lifecycle transport:
|
|
12
|
+
# the trusted grants policy and the authenticated boundary client.
|
|
13
|
+
# The managed `ask`/native-delivery path and its attempt-bound
|
|
14
|
+
# assignment binding were removed with the old authority model; the
|
|
15
|
+
# local file-based commands and the lifecycle/relay transport remain.
|
|
19
16
|
class Lab
|
|
20
|
-
DEFAULT_PROJECT = "ace"
|
|
21
|
-
DEFAULT_HARNESS = "lab-admin"
|
|
22
|
-
DEFAULT_PLAN = "ace-hitl ask"
|
|
23
17
|
DEFAULT_STORE_ROOT = "/run/lab/hitl"
|
|
24
18
|
STORE_ROOT_ENV = "ACE_HITL_STORE_ROOT"
|
|
25
19
|
DEFAULT_SOCKET_PATH = "/run/lab/hitl.sock"
|
|
26
20
|
SOCKET_PATH_ENV = "ACE_HITL_SOCKET"
|
|
27
|
-
# The trusted deployment grants document, shared with
|
|
28
|
-
#
|
|
29
|
-
# overridden for tests; the document's trust is verified
|
|
30
|
-
#
|
|
31
|
-
#
|
|
21
|
+
# The trusted deployment grants document, shared with the operator
|
|
22
|
+
# question and transport authorization facts. The PATH may be
|
|
23
|
+
# overridden for tests; the document's trust is verified absolutely
|
|
24
|
+
# (root-owned, protected traversal), so an override cannot inject
|
|
25
|
+
# authority.
|
|
32
26
|
DEFAULT_GRANTS_PATH = "/etc/lab/ace-lab/authorization.yml"
|
|
33
27
|
GRANTS_PATH_ENV = "ACE_HITL_GRANTS_PATH"
|
|
34
28
|
|
|
35
|
-
PROVIDER_NAME = "lab"
|
|
36
|
-
|
|
37
|
-
def initialize(manager: nil, store: nil, binding: nil, live_client: nil)
|
|
38
|
-
@manager = manager
|
|
39
|
-
# The boundary dependency: an injected store (unit wiring) or
|
|
40
|
-
# the authenticated boundary client (review 8x333sqz).
|
|
41
|
-
@boundary = store
|
|
42
|
-
@binding = binding
|
|
43
|
-
@live_client = live_client
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# The managed binding authority (spec 8wq.t.34i): assignment
|
|
47
|
-
# attempts verified through the ace-assign coordinator.
|
|
48
|
-
def self.assignment_binding(repo_root: nil)
|
|
49
|
-
AssignmentBinding.new(repo_root: repo_root)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
29
|
# The transport/service authorization policy from the trusted
|
|
53
|
-
# grants document
|
|
30
|
+
# grants document.
|
|
54
31
|
def self.grants_policy(grants_path: nil, document: nil)
|
|
55
32
|
Lifecycle::GrantsPolicy.new(
|
|
56
33
|
grants_path: grants_path || ENV[GRANTS_PATH_ENV] || DEFAULT_GRANTS_PATH,
|
|
@@ -58,10 +35,10 @@ module Ace
|
|
|
58
35
|
)
|
|
59
36
|
end
|
|
60
37
|
|
|
61
|
-
# The authenticated boundary client for CLI operations. The
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
38
|
+
# The authenticated boundary client for CLI operations. The service
|
|
39
|
+
# identity comes ONLY from the trusted grants document; without it
|
|
40
|
+
# the boundary is unconfigured and every lifecycle operation fails
|
|
41
|
+
# closed with a visible transport error.
|
|
65
42
|
def self.boundary_client(socket_path: nil, policy: nil)
|
|
66
43
|
policy ||= grants_policy
|
|
67
44
|
service_uid = policy.service_uid
|
|
@@ -76,97 +53,6 @@ module Ace
|
|
|
76
53
|
service_uid: service_uid
|
|
77
54
|
)
|
|
78
55
|
end
|
|
79
|
-
|
|
80
|
-
# Local event + scoped request in ONE operation. The authenticated
|
|
81
|
-
# managed binding supplies the verified reverse; callers cannot choose it.
|
|
82
|
-
def ask(question:, attempt:, assignment:, title: nil,
|
|
83
|
-
kind: "text", otp: nil, project: DEFAULT_PROJECT, harness: DEFAULT_HARNESS,
|
|
84
|
-
plan: DEFAULT_PLAN, effect: {})
|
|
85
|
-
effect = effect.to_h
|
|
86
|
-
request_id = "hitl-#{SecureRandom.hex(8)}"
|
|
87
|
-
manager = build_manager
|
|
88
|
-
event = manager.create(title || question, questions: [question])
|
|
89
|
-
|
|
90
|
-
# The ask reaches the store through the AUTHENTICATED BOUNDARY
|
|
91
|
-
# (spec 8wq.t.34i): requesters never write shared store files.
|
|
92
|
-
# An injected store stays available for unit-level wiring.
|
|
93
|
-
begin
|
|
94
|
-
created = boundary.create(
|
|
95
|
-
id: request_id,
|
|
96
|
-
assignment: assignment,
|
|
97
|
-
attempt: attempt,
|
|
98
|
-
kind: kind,
|
|
99
|
-
otp: otp,
|
|
100
|
-
project: project,
|
|
101
|
-
harness: harness,
|
|
102
|
-
plan: plan,
|
|
103
|
-
question: question,
|
|
104
|
-
ace_hitl_id: event.id,
|
|
105
|
-
effect: effect_declared?(effect) ? effect : nil
|
|
106
|
-
)
|
|
107
|
-
envelope = Contract::ManagedEnvelope.load(created.fetch("envelope"), expected: {
|
|
108
|
-
request_id: request_id, assignment_id: assignment, attempt_id: attempt, project: project,
|
|
109
|
-
correlation_id: request_id, kind: kind
|
|
110
|
-
})
|
|
111
|
-
address = envelope["reverse"]
|
|
112
|
-
raise Lifecycle::BindingError, "managed ask has no verified native reverse address" unless address
|
|
113
|
-
ref = Ref.new(session: address.fetch("session"), pane: address.fetch("pane"))
|
|
114
|
-
rescue Lifecycle::Error, Contract::InvalidEnvelope, KeyError => e
|
|
115
|
-
raise ProviderUnavailableError,
|
|
116
|
-
"#{e.message}; local HITL event #{event.id} was created but never bound to a " \
|
|
117
|
-
"Lab request (orphan) - inspect it with ace-hitl show #{event.id} and delete " \
|
|
118
|
-
"or re-ask as needed"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
manager.update(event.id, set: {
|
|
122
|
-
"provider" => PROVIDER_NAME,
|
|
123
|
-
"ref_schema" => ref.to_h[:schema],
|
|
124
|
-
"ref_session" => ref.session,
|
|
125
|
-
"ref_pane" => ref.pane,
|
|
126
|
-
"lab_request_id" => request_id,
|
|
127
|
-
"lab_request_state" => "created",
|
|
128
|
-
"lab_request_effect" => effect_declared?(effect) ? "declared" : "none"
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
AskResult.new(event_id: event.id, request_id: request_id, ref: ref)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def deliver(request:, timeout: 0)
|
|
135
|
-
live_client.deliver(request: request, timeout: timeout)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def watch(request:, timeout: 0, &observer)
|
|
139
|
-
live_client.watch(request: request, timeout: timeout, &observer)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def wait(request:, timeout: 0, operation: nil)
|
|
143
|
-
live_client.wait(request: request, timeout: timeout, operation: operation)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
private
|
|
147
|
-
|
|
148
|
-
def live_client
|
|
149
|
-
require_relative "../live_client"
|
|
150
|
-
@live_client ||= LiveClient.new(boundary: boundary)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def effect_declared?(effect)
|
|
154
|
-
!!(effect[:match] || effect[:effect_cwd] || effect[:effect_timeout] ||
|
|
155
|
-
Array(effect[:effect_args]).any?)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def boundary
|
|
159
|
-
@boundary || self.class.boundary_client
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def build_manager
|
|
163
|
-
return @manager if @manager
|
|
164
|
-
|
|
165
|
-
# Boundary-only consumers use fixed grants and authenticated transport;
|
|
166
|
-
# local event configuration belongs to the ask operation alone.
|
|
167
|
-
require_relative "../organisms/hitl_manager"
|
|
168
|
-
Organisms::HitlManager.new
|
|
169
|
-
end
|
|
170
56
|
end
|
|
171
57
|
end
|
|
172
58
|
end
|
data/lib/ace/hitl/version.rb
CHANGED
data/lib/ace/hitl.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "ace/hitl/contract"
|
|
|
5
5
|
require_relative "hitl/version"
|
|
6
6
|
require_relative "hitl/atoms/hitl_file_pattern"
|
|
7
7
|
require_relative "hitl/atoms/hitl_id_formatter"
|
|
8
|
+
require_relative "hitl/atoms/hitl_address"
|
|
8
9
|
require_relative "hitl/models/hitl_event"
|
|
9
10
|
require_relative "hitl/lifecycle"
|
|
10
11
|
require_relative "hitl/molecules/hitl_config_loader"
|
|
@@ -16,8 +17,14 @@ require_relative "hitl/molecules/hitl_answer_editor"
|
|
|
16
17
|
require_relative "hitl/molecules/hitl_creator"
|
|
17
18
|
require_relative "hitl/molecules/resume_dispatcher"
|
|
18
19
|
require_relative "hitl/molecules/worktree_scope_resolver"
|
|
20
|
+
require_relative "hitl/molecules/hitl_address_resolver"
|
|
21
|
+
require_relative "hitl/atoms/hitl_event_text"
|
|
22
|
+
require_relative "hitl/molecules/hitl_route_loader"
|
|
23
|
+
require_relative "hitl/adapters"
|
|
24
|
+
require_relative "hitl/adapters/terminal"
|
|
19
25
|
require_relative "hitl/organisms/hitl_manager"
|
|
20
|
-
require_relative "hitl/
|
|
26
|
+
require_relative "hitl/organisms/hitl_sender"
|
|
27
|
+
require_relative "hitl/providers/lab"
|
|
21
28
|
require_relative "hitl/live_client"
|
|
22
29
|
require_relative "hitl/cli"
|
|
23
30
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-hitl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.23.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-hitl-contract
|
|
@@ -107,34 +107,6 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '0.6'
|
|
110
|
-
- !ruby/object:Gem::Dependency
|
|
111
|
-
name: ace-assign
|
|
112
|
-
requirement: !ruby/object:Gem::Requirement
|
|
113
|
-
requirements:
|
|
114
|
-
- - "~>"
|
|
115
|
-
- !ruby/object:Gem::Version
|
|
116
|
-
version: '0.70'
|
|
117
|
-
type: :runtime
|
|
118
|
-
prerelease: false
|
|
119
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
-
requirements:
|
|
121
|
-
- - "~>"
|
|
122
|
-
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0.70'
|
|
124
|
-
- !ruby/object:Gem::Dependency
|
|
125
|
-
name: ace-herdr
|
|
126
|
-
requirement: !ruby/object:Gem::Requirement
|
|
127
|
-
requirements:
|
|
128
|
-
- - "~>"
|
|
129
|
-
- !ruby/object:Gem::Version
|
|
130
|
-
version: '0.6'
|
|
131
|
-
type: :runtime
|
|
132
|
-
prerelease: false
|
|
133
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
-
requirements:
|
|
135
|
-
- - "~>"
|
|
136
|
-
- !ruby/object:Gem::Version
|
|
137
|
-
version: '0.6'
|
|
138
110
|
- !ruby/object:Gem::Dependency
|
|
139
111
|
name: ace-runtime
|
|
140
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -172,11 +144,15 @@ files:
|
|
|
172
144
|
- handbook/skills/as-hitl/SKILL.md
|
|
173
145
|
- handbook/workflow-instructions/hitl.wf.md
|
|
174
146
|
- lib/ace/hitl.rb
|
|
147
|
+
- lib/ace/hitl/adapters.rb
|
|
148
|
+
- lib/ace/hitl/adapters/registry.rb
|
|
149
|
+
- lib/ace/hitl/adapters/terminal.rb
|
|
150
|
+
- lib/ace/hitl/atoms/hitl_address.rb
|
|
175
151
|
- lib/ace/hitl/atoms/hitl_effect_validator.rb
|
|
152
|
+
- lib/ace/hitl/atoms/hitl_event_text.rb
|
|
176
153
|
- lib/ace/hitl/atoms/hitl_file_pattern.rb
|
|
177
154
|
- lib/ace/hitl/atoms/hitl_id_formatter.rb
|
|
178
155
|
- lib/ace/hitl/cli.rb
|
|
179
|
-
- lib/ace/hitl/cli/commands/ask.rb
|
|
180
156
|
- lib/ace/hitl/cli/commands/cancel.rb
|
|
181
157
|
- lib/ace/hitl/cli/commands/consume.rb
|
|
182
158
|
- lib/ace/hitl/cli/commands/create.rb
|
|
@@ -185,12 +161,9 @@ files:
|
|
|
185
161
|
- lib/ace/hitl/cli/commands/lifecycle_command.rb
|
|
186
162
|
- lib/ace/hitl/cli/commands/list.rb
|
|
187
163
|
- lib/ace/hitl/cli/commands/operator_question.rb
|
|
188
|
-
- lib/ace/hitl/cli/commands/overseer_ack.rb
|
|
189
|
-
- lib/ace/hitl/cli/commands/overseer_pending.rb
|
|
190
|
-
- lib/ace/hitl/cli/commands/overseer_send.rb
|
|
191
164
|
- lib/ace/hitl/cli/commands/pending.rb
|
|
192
|
-
- lib/ace/hitl/cli/commands/proposal.rb
|
|
193
165
|
- lib/ace/hitl/cli/commands/read.rb
|
|
166
|
+
- lib/ace/hitl/cli/commands/send.rb
|
|
194
167
|
- lib/ace/hitl/cli/commands/serve.rb
|
|
195
168
|
- lib/ace/hitl/cli/commands/show.rb
|
|
196
169
|
- lib/ace/hitl/cli/commands/states.rb
|
|
@@ -211,14 +184,11 @@ files:
|
|
|
211
184
|
- lib/ace/hitl/lifecycle/lifecycle_requests.rb
|
|
212
185
|
- lib/ace/hitl/lifecycle/operator_questions.rb
|
|
213
186
|
- lib/ace/hitl/lifecycle/otp_vault.rb
|
|
214
|
-
- lib/ace/hitl/lifecycle/overseer.rb
|
|
215
187
|
- lib/ace/hitl/lifecycle/peer.rb
|
|
216
188
|
- lib/ace/hitl/lifecycle/policy.rb
|
|
217
|
-
- lib/ace/hitl/lifecycle/proposals.rb
|
|
218
189
|
- lib/ace/hitl/lifecycle/protocol.rb
|
|
219
190
|
- lib/ace/hitl/lifecycle/remote_peer.rb
|
|
220
191
|
- lib/ace/hitl/lifecycle/service.rb
|
|
221
|
-
- lib/ace/hitl/lifecycle/service_publication_binding.rb
|
|
222
192
|
- lib/ace/hitl/lifecycle/store.rb
|
|
223
193
|
- lib/ace/hitl/lifecycle/tls_access_policy.rb
|
|
224
194
|
- lib/ace/hitl/lifecycle/tls_authority.rb
|
|
@@ -228,22 +198,21 @@ files:
|
|
|
228
198
|
- lib/ace/hitl/lifecycle/tls_service.rb
|
|
229
199
|
- lib/ace/hitl/live_client.rb
|
|
230
200
|
- lib/ace/hitl/models/hitl_event.rb
|
|
201
|
+
- lib/ace/hitl/molecules/hitl_address_resolver.rb
|
|
231
202
|
- lib/ace/hitl/molecules/hitl_answer_editor.rb
|
|
232
203
|
- lib/ace/hitl/molecules/hitl_config_loader.rb
|
|
233
204
|
- lib/ace/hitl/molecules/hitl_creator.rb
|
|
205
|
+
- lib/ace/hitl/molecules/hitl_delivery_state.rb
|
|
234
206
|
- lib/ace/hitl/molecules/hitl_display_formatter.rb
|
|
235
207
|
- lib/ace/hitl/molecules/hitl_loader.rb
|
|
236
208
|
- lib/ace/hitl/molecules/hitl_resolver.rb
|
|
209
|
+
- lib/ace/hitl/molecules/hitl_route_loader.rb
|
|
237
210
|
- lib/ace/hitl/molecules/hitl_scanner.rb
|
|
238
211
|
- lib/ace/hitl/molecules/resume_dispatcher.rb
|
|
239
212
|
- lib/ace/hitl/molecules/worktree_scope_resolver.rb
|
|
240
213
|
- lib/ace/hitl/organisms/hitl_manager.rb
|
|
241
|
-
- lib/ace/hitl/
|
|
242
|
-
- lib/ace/hitl/proposals/policy.rb
|
|
214
|
+
- lib/ace/hitl/organisms/hitl_sender.rb
|
|
243
215
|
- lib/ace/hitl/providers/lab.rb
|
|
244
|
-
- lib/ace/hitl/providers/lab/assignment_binding.rb
|
|
245
|
-
- lib/ace/hitl/providers/lab/protected_assignment_binding.rb
|
|
246
|
-
- lib/ace/hitl/providers/providers.rb
|
|
247
216
|
- lib/ace/hitl/version.rb
|
|
248
217
|
homepage: https://github.com/cs3b/ace
|
|
249
218
|
licenses:
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/support/cli"
|
|
4
|
-
require_relative "../../lifecycle"
|
|
5
|
-
require_relative "../../atoms/hitl_effect_validator"
|
|
6
|
-
require_relative "../../providers/providers"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Hitl
|
|
10
|
-
module CLI
|
|
11
|
-
module Commands
|
|
12
|
-
class Ask < Ace::Support::Cli::Command
|
|
13
|
-
include Ace::Support::Cli::Base
|
|
14
|
-
|
|
15
|
-
desc "Ask a human via HITL and forward the request through a provider adapter"
|
|
16
|
-
|
|
17
|
-
option :question, type: :string, required: true, desc: "Question text for the human"
|
|
18
|
-
|
|
19
|
-
option :title, type: :string, desc: "Local HITL event title (defaults to the question)"
|
|
20
|
-
option :provider, type: :string, desc: "HITL provider adapter (default: ACE_HITL_PROVIDER or lab)"
|
|
21
|
-
# Managed binding (spec 8wq.t.34i): the request binds to the
|
|
22
|
-
# exact active managed attempt of the calling identity.
|
|
23
|
-
option :assignment, type: :string, desc: "Managed assignment id (compact id)"
|
|
24
|
-
option :kind, type: :string, desc: "Request kind (default: text; otp requires the challenge evidence)"
|
|
25
|
-
option :"otp-operation", type: :string, desc: "OTP challenge: the ONE authorized operation name"
|
|
26
|
-
option :"otp-result-ref", type: :string, desc: "OTP challenge: OTP-required publisher result reference"
|
|
27
|
-
option :"otp-input-digest", type: :string, desc: "OTP challenge: sha256 input digest of the authorized input"
|
|
28
|
-
option :"otp-expires-at", type: :string, desc: "OTP challenge: expiry as unix seconds (<= 24h ahead)"
|
|
29
|
-
option :attempt, type: :string, desc: "Attempt id of the exact active attempt"
|
|
30
|
-
option :project, type: :string, desc: "Lab project label (default: ace)"
|
|
31
|
-
option :harness, type: :string, desc: "Lab harness label (default: lab-admin)"
|
|
32
|
-
option :plan, type: :string, desc: "Lab plan label (default: ace-hitl ask)"
|
|
33
|
-
|
|
34
|
-
option :"effect-match", type: :string, desc: "Effect callback regex gate on the answer (<= 200 chars)"
|
|
35
|
-
option :"effect-arg", type: :string, repeat: true, desc: "Effect callback argv element, repeatable (1..16 x 1..512 chars)"
|
|
36
|
-
option :"effect-cwd", type: :string, desc: "Effect callback working directory (absolute, must exist)"
|
|
37
|
-
option :"effect-timeout-s", type: :string, desc: "Effect callback timeout in seconds (1..600)"
|
|
38
|
-
|
|
39
|
-
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
40
|
-
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
41
|
-
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
42
|
-
|
|
43
|
-
def call(question:, **options)
|
|
44
|
-
effect = build_effect(options)
|
|
45
|
-
validate_effect!(effect)
|
|
46
|
-
|
|
47
|
-
assignment = require_binding!(options)
|
|
48
|
-
attempt = require_attempt!(options)
|
|
49
|
-
otp = build_otp_challenge!(options)
|
|
50
|
-
provider = resolve_provider(options[:provider])
|
|
51
|
-
|
|
52
|
-
result = provider.ask(
|
|
53
|
-
question: question,
|
|
54
|
-
title: options[:title],
|
|
55
|
-
assignment: assignment,
|
|
56
|
-
attempt: attempt,
|
|
57
|
-
kind: options[:kind] || "text",
|
|
58
|
-
otp: otp,
|
|
59
|
-
project: options[:project] || Providers::Lab::DEFAULT_PROJECT,
|
|
60
|
-
harness: options[:harness] || Providers::Lab::DEFAULT_HARNESS,
|
|
61
|
-
plan: options[:plan] || Providers::Lab::DEFAULT_PLAN,
|
|
62
|
-
effect: effect
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
puts "HITL event: #{result.event_id}"
|
|
66
|
-
ref = result.ref
|
|
67
|
-
puts "Provider: #{Providers::Lab::PROVIDER_NAME} (ref #{ref.session}/#{ref.pane}, #{Providers::Ref::SCHEMA})"
|
|
68
|
-
puts "Lab request: #{result.request_id}"
|
|
69
|
-
rescue Providers::ProviderUnavailableError => e
|
|
70
|
-
raise_cli_error(e.message)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def build_effect(options)
|
|
76
|
-
{
|
|
77
|
-
match: options[:"effect-match"],
|
|
78
|
-
effect_args: Array(options[:"effect-arg"]),
|
|
79
|
-
effect_cwd: options[:"effect-cwd"],
|
|
80
|
-
effect_timeout: options[:"effect-timeout-s"]
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def validate_effect!(effect)
|
|
85
|
-
Atoms::HitlEffectValidator.validate!(**effect)
|
|
86
|
-
rescue Atoms::HitlEffectValidator::ValidationError => e
|
|
87
|
-
raise_cli_error(e.message)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def require_binding!(options)
|
|
91
|
-
assignment = options[:assignment]
|
|
92
|
-
raise_cli_error("--assignment required (the managed assignment id)") if assignment.nil? || assignment.strip.empty?
|
|
93
|
-
assignment
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# The OTP challenge evidence is non-secret, structurally
|
|
97
|
-
# validated store-side; the CLI only assembles it (spec
|
|
98
|
-
# 8wq.t.34i: no publisher result, no OTP request).
|
|
99
|
-
def build_otp_challenge!(options)
|
|
100
|
-
kind = options[:kind] || "text"
|
|
101
|
-
return nil unless kind == "otp"
|
|
102
|
-
|
|
103
|
-
operation = options[:"otp-operation"]
|
|
104
|
-
result_ref = options[:"otp-result-ref"]
|
|
105
|
-
input_digest = options[:"otp-input-digest"]
|
|
106
|
-
expires_at = options[:"otp-expires-at"]
|
|
107
|
-
missing = %w[--otp-operation --otp-result-ref --otp-input-digest --otp-expires-at]
|
|
108
|
-
.zip([operation, result_ref, input_digest, expires_at])
|
|
109
|
-
.select { |_flag, value| value.nil? || value.to_s.strip.empty? }
|
|
110
|
-
.map(&:first)
|
|
111
|
-
raise_cli_error("OTP requests require #{missing.join(", ")} (the OTP-required publisher evidence)") unless missing.empty?
|
|
112
|
-
|
|
113
|
-
{operation: operation, result_ref: result_ref, input_digest: input_digest, expires_at: expires_at}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def require_attempt!(options)
|
|
117
|
-
attempt = options[:attempt]
|
|
118
|
-
unless attempt && !attempt.strip.empty?
|
|
119
|
-
raise_cli_error("--attempt required (the exact active attempt id)")
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
attempt
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def resolve_provider(raw)
|
|
126
|
-
name = raw || ENV["ACE_HITL_PROVIDER"] || Providers::Lab::PROVIDER_NAME
|
|
127
|
-
Providers.resolve(name)
|
|
128
|
-
rescue Providers::UnknownProviderError => e
|
|
129
|
-
raise_cli_error(e.message)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/support/cli"
|
|
4
|
-
require_relative "lifecycle_command"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Hitl
|
|
8
|
-
module CLI
|
|
9
|
-
module Commands
|
|
10
|
-
# Host-broker acknowledgement of one relayed Overseer response.
|
|
11
|
-
class OverseerAck < Ace::Support::Cli::Command
|
|
12
|
-
include Ace::Support::Cli::Base
|
|
13
|
-
include LifecycleCommand
|
|
14
|
-
|
|
15
|
-
desc "Acknowledge (remove) one relayed Overseer response"
|
|
16
|
-
|
|
17
|
-
argument :id, required: true, desc: "Overseer response message id"
|
|
18
|
-
|
|
19
|
-
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
20
|
-
|
|
21
|
-
def call(id:, **options)
|
|
22
|
-
emit(overseer.ack(id))
|
|
23
|
-
rescue Lifecycle::Error => e
|
|
24
|
-
raise_lifecycle_error(e.message)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|