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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +162 -0
  3. data/README.md +60 -117
  4. data/docs/usage.md +247 -421
  5. data/handbook/workflow-instructions/hitl.wf.md +55 -6
  6. data/lib/ace/hitl/adapters/registry.rb +65 -0
  7. data/lib/ace/hitl/adapters/terminal.rb +75 -0
  8. data/lib/ace/hitl/adapters.rb +80 -0
  9. data/lib/ace/hitl/atoms/hitl_address.rb +89 -0
  10. data/lib/ace/hitl/atoms/hitl_event_text.rb +54 -0
  11. data/lib/ace/hitl/cli/commands/create.rb +81 -12
  12. data/lib/ace/hitl/cli/commands/lifecycle_command.rb +1 -7
  13. data/lib/ace/hitl/cli/commands/list.rb +22 -1
  14. data/lib/ace/hitl/cli/commands/send.rb +85 -0
  15. data/lib/ace/hitl/cli/commands/serve.rb +25 -44
  16. data/lib/ace/hitl/cli/commands/show.rb +23 -3
  17. data/lib/ace/hitl/cli/commands/update.rb +4 -0
  18. data/lib/ace/hitl/cli/commands/wait.rb +5 -0
  19. data/lib/ace/hitl/cli.rb +8 -18
  20. data/lib/ace/hitl/lifecycle/frame_serving.rb +0 -15
  21. data/lib/ace/hitl/lifecycle/kinds.rb +1 -1
  22. data/lib/ace/hitl/lifecycle/lifecycle_requests.rb +0 -36
  23. data/lib/ace/hitl/lifecycle/policy.rb +0 -44
  24. data/lib/ace/hitl/lifecycle/protocol.rb +1 -2
  25. data/lib/ace/hitl/lifecycle/service.rb +2 -3
  26. data/lib/ace/hitl/lifecycle/store.rb +11 -75
  27. data/lib/ace/hitl/lifecycle/tls_access_policy.rb +0 -14
  28. data/lib/ace/hitl/lifecycle/tls_project_scope.rb +0 -15
  29. data/lib/ace/hitl/lifecycle/tls_service.rb +2 -3
  30. data/lib/ace/hitl/lifecycle.rb +2 -4
  31. data/lib/ace/hitl/live_client.rb +11 -87
  32. data/lib/ace/hitl/models/hitl_event.rb +35 -0
  33. data/lib/ace/hitl/molecules/hitl_address_resolver.rb +80 -0
  34. data/lib/ace/hitl/molecules/hitl_config_loader.rb +22 -1
  35. data/lib/ace/hitl/molecules/hitl_creator.rb +126 -27
  36. data/lib/ace/hitl/molecules/hitl_delivery_state.rb +58 -0
  37. data/lib/ace/hitl/molecules/hitl_display_formatter.rb +3 -2
  38. data/lib/ace/hitl/molecules/hitl_loader.rb +19 -2
  39. data/lib/ace/hitl/molecules/hitl_route_loader.rb +108 -0
  40. data/lib/ace/hitl/organisms/hitl_manager.rb +73 -6
  41. data/lib/ace/hitl/organisms/hitl_sender.rb +148 -0
  42. data/lib/ace/hitl/providers/lab.rb +15 -129
  43. data/lib/ace/hitl/version.rb +1 -1
  44. data/lib/ace/hitl.rb +8 -1
  45. metadata +12 -43
  46. data/lib/ace/hitl/cli/commands/ask.rb +0 -136
  47. data/lib/ace/hitl/cli/commands/overseer_ack.rb +0 -30
  48. data/lib/ace/hitl/cli/commands/overseer_pending.rb +0 -28
  49. data/lib/ace/hitl/cli/commands/overseer_send.rb +0 -35
  50. data/lib/ace/hitl/cli/commands/proposal.rb +0 -75
  51. data/lib/ace/hitl/lifecycle/overseer.rb +0 -111
  52. data/lib/ace/hitl/lifecycle/proposals.rb +0 -297
  53. data/lib/ace/hitl/lifecycle/service_publication_binding.rb +0 -97
  54. data/lib/ace/hitl/proposals/evaluator.rb +0 -29
  55. data/lib/ace/hitl/proposals/policy.rb +0 -127
  56. data/lib/ace/hitl/providers/lab/assignment_binding.rb +0 -126
  57. data/lib/ace/hitl/providers/lab/protected_assignment_binding.rb +0 -75
  58. 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
- # provider=lab adapter (spec 8wm.t.vrz §1; lifecycle migration
14
- # spec 8wm.t.y21 §1; scoped boundary 8wq.t.34i): ONE ask operation
15
- # = local HITL event + relay request created through the scoped
16
- # store boundary with the lab-backed binding policy. Owns every
17
- # lab-specific seam; agent-facing ace-hitl paths must never bypass
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 ace-lab's
28
- # GrantResolver (same format, same trust rules). The PATH may be
29
- # overridden for tests; the document's trust is verified
30
- # absolutely (root-owned, protected traversal), so an override
31
- # cannot inject authority.
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 (ace-lab supplies the deployment facts).
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
- # service identity comes ONLY from the trusted grants document;
63
- # without it the boundary is unconfigured and every lifecycle
64
- # operation fails closed with a visible transport error.
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Hitl
5
- VERSION = "0.17.0"
5
+ VERSION = "0.23.2"
6
6
  end
7
7
  end
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/providers/providers"
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.17.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-16 00:00:00.000000000 Z
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/proposals/evaluator.rb
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