ace-assign 0.64.0 → 0.66.1
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/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/CHANGELOG.md +25 -0
- data/docs/protected-authority.md +23 -7
- data/docs/usage.md +117 -4
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +156 -6
- data/lib/ace/assign/authority/deployment.rb +201 -39
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +131 -18
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +36 -18
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +221 -18
- data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +298 -19
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
- data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
- data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
- data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +5 -2
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/router.rb +24 -2
- data/lib/ace/assign/authority/server.rb +103 -33
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +243 -12
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +44 -12
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
- data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
- data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +2 -0
- data/lib/ace/assign/cli/commands/delivery.rb +54 -4
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +12 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +36 -5
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +3 -3
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -112
- metadata +90 -11
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require_relative "transfer_codec"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Authority
|
|
8
|
+
# A rendezvous for one already authenticated original launcher stream.
|
|
9
|
+
# Canonical issue permits belong to LaunchLifecycle/JournalMutation.
|
|
10
|
+
# This queue owns no replay, authorization, terminal state or effects.
|
|
11
|
+
class LaunchControlChannel
|
|
12
|
+
Pending = Struct.new(:frame, :bytes, :deadline, :result, :error, :finished, keyword_init: true)
|
|
13
|
+
PROMPT_FIELDS = %w[attempt_id intent_event_id journal_commit mutation_id original_binding_digest text_descriptor transfer_id type version].freeze
|
|
14
|
+
OUTCOME_FIELDS = %w[guarded_evidence intent_event_id mutation_id original_binding_digest type version].freeze
|
|
15
|
+
RECORDED_FIELDS = %w[intent_event_id journal_commit mutation_id original_binding_digest outcome type version].freeze
|
|
16
|
+
REVIEW_FIELDS = %w[attempt_id journal_commit mutation_id original_binding_digest request_event_id type version].freeze
|
|
17
|
+
REVIEW_ASSIGNED_FIELDS = %w[assignment_event_id journal_commit mutation_id original_binding_digest request_event_id type version].freeze
|
|
18
|
+
INHIBIT_FIELDS = %w[attempt_id journal_commit original_binding_digest seal_event_id type version].freeze
|
|
19
|
+
INHIBIT_OUTCOME_FIELDS = %w[guarded_evidence original_binding_digest seal_event_id type version].freeze
|
|
20
|
+
INHIBIT_RECORDED_FIELDS = %w[journal_commit original_binding_digest seal_event_id type version].freeze
|
|
21
|
+
|
|
22
|
+
def initialize(socket:, codec:, outcome:, input_inhibition: nil)
|
|
23
|
+
@socket, @codec, @outcome = socket, codec, outcome
|
|
24
|
+
@input_inhibition = input_inhibition
|
|
25
|
+
@mutex, @changed = Mutex.new, ConditionVariable.new
|
|
26
|
+
@pending = nil
|
|
27
|
+
@closed = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reserve_dispatch!
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher control channel is unavailable" if @closed
|
|
33
|
+
raise AttemptErrors::Conflict, "Original launcher input is inhibited" if @inhibiting
|
|
34
|
+
raise AttemptErrors::Conflict, "Original launcher control channel is busy" if @reservation || @pending
|
|
35
|
+
@reservation = Object.new.freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inhibit_input(frame:, deadline:)
|
|
40
|
+
self.class.validate_inhibit!(frame)
|
|
41
|
+
pending = Pending.new(frame: frame, deadline: deadline, finished: false)
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
@inhibiting = true
|
|
44
|
+
if @inhibition_pending
|
|
45
|
+
unless @inhibition_pending.frame.slice("attempt_id", "original_binding_digest", "seal_event_id") == frame.slice("attempt_id", "original_binding_digest", "seal_event_id")
|
|
46
|
+
raise AttemptErrors::Conflict, "Original input inhibition names another seal"
|
|
47
|
+
end
|
|
48
|
+
pending = @inhibition_pending
|
|
49
|
+
else
|
|
50
|
+
@inhibition_pending = pending
|
|
51
|
+
while @reservation || @pending
|
|
52
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher control channel is unavailable" if @closed
|
|
53
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
54
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition is uncertain" unless remaining.positive?
|
|
55
|
+
@changed.wait(@mutex, remaining)
|
|
56
|
+
end
|
|
57
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher control channel is unavailable" if @closed
|
|
58
|
+
@pending = pending
|
|
59
|
+
@changed.broadcast
|
|
60
|
+
end
|
|
61
|
+
until pending.finished
|
|
62
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
63
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition is uncertain" unless remaining.positive?
|
|
64
|
+
@changed.wait(@mutex, remaining)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
raise pending.error if pending.error
|
|
68
|
+
pending.result
|
|
69
|
+
rescue AttemptErrors::EvidenceUnavailable
|
|
70
|
+
close
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def dispatch_reserved?(reservation)
|
|
75
|
+
@mutex.synchronize { !@closed && !reservation.nil? && @reservation.equal?(reservation) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def release_dispatch!(reservation)
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
@reservation = nil if @reservation.equal?(reservation) && !@pending
|
|
81
|
+
@changed.broadcast if @inhibiting
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def dispatch_prompt(frame:, bytes:, deadline:, reservation:)
|
|
86
|
+
validate_prompt!(frame)
|
|
87
|
+
dispatch_pending(Pending.new(frame: frame, bytes: bytes, deadline: deadline, finished: false), reservation)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def dispatch_review(frame:, deadline:, reservation:)
|
|
91
|
+
self.class.validate_review!(frame)
|
|
92
|
+
dispatch_pending(Pending.new(frame: frame, deadline: deadline, finished: false), reservation)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def dispatch_pending(pending, reservation)
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher control channel is unavailable" if @closed
|
|
98
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher dispatch capacity is not held" unless reservation && @reservation.equal?(reservation)
|
|
99
|
+
raise AttemptErrors::Conflict, "Original launcher control channel is busy" if @pending
|
|
100
|
+
@pending = pending
|
|
101
|
+
@changed.broadcast
|
|
102
|
+
until pending.finished
|
|
103
|
+
remaining = pending.deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
104
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher dispatch outcome is uncertain" unless remaining.positive?
|
|
105
|
+
@changed.wait(@mutex, remaining)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
raise pending.error if pending.error
|
|
109
|
+
pending.result
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private :dispatch_pending
|
|
113
|
+
|
|
114
|
+
# Only the existing Server connection handler calls serve. No slot,
|
|
115
|
+
# assignment, journal or channel mutex is held over a socket wait or
|
|
116
|
+
# the original owner's canonical outcome ingestion callback.
|
|
117
|
+
def serve
|
|
118
|
+
loop do
|
|
119
|
+
pending = @mutex.synchronize do
|
|
120
|
+
@changed.wait(@mutex, 1) unless @closed || @pending
|
|
121
|
+
break if @closed
|
|
122
|
+
@pending
|
|
123
|
+
end
|
|
124
|
+
break if closed?
|
|
125
|
+
unless pending
|
|
126
|
+
ping
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
begin
|
|
130
|
+
wire.write(@socket, pending.frame, deadline: pending.deadline, limit: 16_384)
|
|
131
|
+
if pending.frame.fetch("type") == "launch_review_delegate"
|
|
132
|
+
result = wire.read(@socket, deadline: pending.deadline, limit: 16_384)
|
|
133
|
+
self.class.validate_review_assigned!(result, pending.frame)
|
|
134
|
+
# This transport correlation is not assignment acceptance.
|
|
135
|
+
# The canonical request owner must authenticate the returned
|
|
136
|
+
# event/commit before recording the public response.
|
|
137
|
+
complete(pending, result: result)
|
|
138
|
+
next
|
|
139
|
+
end
|
|
140
|
+
if pending.frame.fetch("type") == "launch_input_inhibit"
|
|
141
|
+
result = wire.read(@socket, deadline: pending.deadline, limit: 16_384)
|
|
142
|
+
self.class.validate_inhibit_outcome!(result, pending.frame)
|
|
143
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical input inhibition owner unavailable" unless @input_inhibition
|
|
144
|
+
accepted = @input_inhibition.call(result)
|
|
145
|
+
unless accepted.is_a?(Hash) && accepted.keys.sort == %w[journal_commit original_binding_digest seal_event_id] &&
|
|
146
|
+
%w[original_binding_digest seal_event_id].all? { |key| accepted[key] == pending.frame.fetch(key) }
|
|
147
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical input inhibition acceptance differs"
|
|
148
|
+
end
|
|
149
|
+
recorded = pending.frame.slice("original_binding_digest", "seal_event_id").merge("version" => 1,
|
|
150
|
+
"type" => "launch_input_inhibit_recorded", "journal_commit" => accepted.fetch("journal_commit"))
|
|
151
|
+
self.class.validate_inhibit_recorded!(recorded, pending.frame)
|
|
152
|
+
wire.write(@socket, recorded, deadline: pending.deadline, limit: 16_384)
|
|
153
|
+
complete(pending, result: result)
|
|
154
|
+
next
|
|
155
|
+
end
|
|
156
|
+
@codec.send_launch_prompt(@socket, bytes: pending.bytes, descriptor: pending.frame.fetch("text_descriptor"),
|
|
157
|
+
transfer_id: pending.frame.fetch("transfer_id"), deadline: pending.deadline)
|
|
158
|
+
pending.bytes = nil
|
|
159
|
+
result = wire.read(@socket, deadline: pending.deadline, limit: 16_384)
|
|
160
|
+
validate_outcome!(result, pending.frame)
|
|
161
|
+
accepted = @outcome.call(result)
|
|
162
|
+
unless accepted.is_a?(Hash) && accepted.keys.sort == %w[journal_commit outcome] &&
|
|
163
|
+
accepted["outcome"] == result.dig("guarded_evidence", "outcome") &&
|
|
164
|
+
accepted["journal_commit"].is_a?(String) && accepted["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
165
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical prompt completion acknowledgement is unavailable"
|
|
166
|
+
end
|
|
167
|
+
recorded = pending.frame.slice("mutation_id", "intent_event_id", "original_binding_digest").merge(accepted).merge(
|
|
168
|
+
"version" => 1, "type" => "prompt_outcome_recorded")
|
|
169
|
+
wire.write(@socket, recorded, deadline: pending.deadline, limit: 16_384)
|
|
170
|
+
complete(pending, result: result)
|
|
171
|
+
rescue StandardError => error
|
|
172
|
+
complete(pending, error: error)
|
|
173
|
+
break
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
rescue IOError, SystemCallError, Ace::Runtime::RuntimeUnavailableError
|
|
177
|
+
nil
|
|
178
|
+
ensure
|
|
179
|
+
close
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def close
|
|
183
|
+
@mutex.synchronize do
|
|
184
|
+
@closed = true
|
|
185
|
+
@reservation = nil
|
|
186
|
+
if @pending && !@pending.finished
|
|
187
|
+
@pending.error = AttemptErrors::EvidenceUnavailable.new("Original launcher control channel closed with uncertain outcome")
|
|
188
|
+
@pending.finished = true
|
|
189
|
+
@pending.bytes = nil
|
|
190
|
+
end
|
|
191
|
+
if @inhibition_pending && !@inhibition_pending.finished
|
|
192
|
+
@inhibition_pending.error = AttemptErrors::EvidenceUnavailable.new("Original input inhibition is uncertain")
|
|
193
|
+
@inhibition_pending.finished = true
|
|
194
|
+
end
|
|
195
|
+
@changed.broadcast
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def closed?
|
|
200
|
+
@mutex.synchronize { @closed }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.validate_review!(frame)
|
|
204
|
+
unless frame.is_a?(Hash) && frame.keys.sort == REVIEW_FIELDS &&
|
|
205
|
+
frame["version"].is_a?(Integer) && frame["version"] == 1 && frame["type"] == "launch_review_delegate" &&
|
|
206
|
+
%w[attempt_id mutation_id].all? { |key| frame[key].is_a?(String) && frame[key].match?(Molecules::JournalMutation::ID) } &&
|
|
207
|
+
%w[request_event_id original_binding_digest].all? { |key| frame[key].is_a?(String) && frame[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
208
|
+
frame["journal_commit"].is_a?(String) && frame["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
209
|
+
raise AttemptErrors::EvidenceUnavailable, "Private review delegation differs"
|
|
210
|
+
end
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.validate_review_assigned!(result, frame)
|
|
215
|
+
unless result.is_a?(Hash) && result.keys.sort == REVIEW_ASSIGNED_FIELDS &&
|
|
216
|
+
result["version"].is_a?(Integer) && result["version"] == 1 && result["type"] == "launch_review_assigned" &&
|
|
217
|
+
%w[mutation_id request_event_id original_binding_digest].all? { |key| result[key] == frame.fetch(key) } &&
|
|
218
|
+
result["assignment_event_id"].is_a?(String) && result["assignment_event_id"].match?(/\A[0-9a-f]{64}\z/) &&
|
|
219
|
+
result["journal_commit"].is_a?(String) && result["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
220
|
+
raise AttemptErrors::EvidenceUnavailable, "Private review assignment does not join original request"
|
|
221
|
+
end
|
|
222
|
+
true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.validate_prompt!(frame)
|
|
226
|
+
unless frame.is_a?(Hash) && frame.keys.all? { |key| key.is_a?(String) } && frame.keys.sort == PROMPT_FIELDS &&
|
|
227
|
+
frame["version"].is_a?(Integer) && frame["version"] == 1 && frame["type"] == "prompt_dispatch" &&
|
|
228
|
+
%w[intent_event_id original_binding_digest].all? { |key| frame[key].is_a?(String) && frame[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
229
|
+
frame["journal_commit"].is_a?(String) && frame["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
230
|
+
%w[attempt_id mutation_id].all? { |key| frame[key].is_a?(String) && frame[key].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) } &&
|
|
231
|
+
frame["transfer_id"].is_a?(String) && frame["transfer_id"].match?(/\A[0-9a-f]{32}\z/)
|
|
232
|
+
raise AttemptErrors::EvidenceUnavailable, "Private prompt dispatch frame is malformed"
|
|
233
|
+
end
|
|
234
|
+
TransferCodec.new.validate!(frame.fetch("text_descriptor"), :prompt_text)
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.validate_recorded!(recorded, frame, evidence)
|
|
239
|
+
unless recorded.is_a?(Hash) && recorded.keys.sort == RECORDED_FIELDS && recorded["version"].is_a?(Integer) && recorded["version"] == 1 &&
|
|
240
|
+
recorded["type"] == "prompt_outcome_recorded" && recorded["outcome"] == evidence.fetch("outcome") &&
|
|
241
|
+
%w[mutation_id intent_event_id original_binding_digest].all? { |key| recorded[key] == frame.fetch(key) } &&
|
|
242
|
+
recorded["journal_commit"].is_a?(String) && recorded["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
243
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical prompt completion acknowledgement differs"
|
|
244
|
+
end
|
|
245
|
+
true
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def self.validate_inhibit!(frame)
|
|
249
|
+
unless frame.is_a?(Hash) && frame.keys.sort == INHIBIT_FIELDS && frame["version"].is_a?(Integer) && frame["version"] == 1 &&
|
|
250
|
+
frame["type"] == "launch_input_inhibit" && frame["attempt_id"].is_a?(String) && frame["attempt_id"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
|
|
251
|
+
%w[original_binding_digest seal_event_id].all? { |key| frame[key].is_a?(String) && frame[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
252
|
+
frame["journal_commit"].is_a?(String) && frame["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
253
|
+
raise AttemptErrors::EvidenceUnavailable, "Private input inhibition frame is malformed"
|
|
254
|
+
end
|
|
255
|
+
true
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.validate_inhibit_outcome!(result, frame)
|
|
259
|
+
unless result.is_a?(Hash) && result.keys.sort == INHIBIT_OUTCOME_FIELDS && result["version"].is_a?(Integer) && result["version"] == 1 &&
|
|
260
|
+
result["type"] == "launch_input_inhibit_outcome" && result["guarded_evidence"].is_a?(Hash) &&
|
|
261
|
+
%w[original_binding_digest seal_event_id].all? { |key| result[key] == frame.fetch(key) }
|
|
262
|
+
raise AttemptErrors::EvidenceUnavailable, "Private input inhibition outcome differs"
|
|
263
|
+
end
|
|
264
|
+
true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def self.validate_inhibit_recorded!(result, frame)
|
|
268
|
+
unless result.is_a?(Hash) && result.keys.sort == INHIBIT_RECORDED_FIELDS && result["version"].is_a?(Integer) && result["version"] == 1 &&
|
|
269
|
+
result["type"] == "launch_input_inhibit_recorded" && %w[original_binding_digest seal_event_id].all? { |key| result[key] == frame.fetch(key) } &&
|
|
270
|
+
result["journal_commit"].is_a?(String) && result["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
271
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical input inhibition acknowledgement differs"
|
|
272
|
+
end
|
|
273
|
+
true
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
private
|
|
277
|
+
|
|
278
|
+
def validate_prompt!(frame) = self.class.validate_prompt!(frame)
|
|
279
|
+
|
|
280
|
+
def validate_outcome!(result, frame)
|
|
281
|
+
unless result.is_a?(Hash) && result.keys.all? { |key| key.is_a?(String) } && result.keys.sort == OUTCOME_FIELDS &&
|
|
282
|
+
result["version"].is_a?(Integer) && result["version"] == 1 && result["type"] == "prompt_dispatch_outcome" &&
|
|
283
|
+
%w[mutation_id intent_event_id original_binding_digest].all? { |key| result[key] == frame.fetch(key) } &&
|
|
284
|
+
result["guarded_evidence"].is_a?(Hash)
|
|
285
|
+
raise AttemptErrors::EvidenceUnavailable, "Private prompt outcome does not join original dispatch"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def complete(pending, result: nil, error: nil)
|
|
290
|
+
@mutex.synchronize do
|
|
291
|
+
@closed = true if error
|
|
292
|
+
pending.result, pending.error, pending.finished = result, error, true
|
|
293
|
+
pending.bytes = nil
|
|
294
|
+
@pending = nil if @pending.equal?(pending)
|
|
295
|
+
@reservation = nil
|
|
296
|
+
@changed.broadcast
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def ping
|
|
301
|
+
nonce, deadline = SecureRandom.hex(16), wire.deadline(5)
|
|
302
|
+
wire.write(@socket, {"version" => 1, "type" => "launch_control_idle", "nonce" => nonce}, deadline: deadline, limit: 16_384)
|
|
303
|
+
result = wire.read(@socket, deadline: deadline, limit: 16_384)
|
|
304
|
+
unless result.is_a?(Hash) && result["version"].is_a?(Integer) && result == {"version" => 1, "type" => "launch_control_idle_ack", "nonce" => nonce}
|
|
305
|
+
raise AttemptErrors::EvidenceUnavailable, "Private launcher idle response differs"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def wire = Ace::Runtime::Molecules::ProtectedSocket
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../molecules/protected_control_exclusion"
|
|
3
|
+
require_relative "campaign_execution"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Authority
|
|
8
|
+
class LaunchLifecycle
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def control_registration_context!(params, map, journal, commit: journal.ref_value)
|
|
12
|
+
assignment = params.fetch("assignment_id")
|
|
13
|
+
mapping = params.fetch("mapping_id")
|
|
14
|
+
inventory = if commit.nil?
|
|
15
|
+
unless params.key?("definition_bytes") && journal.ref_value.nil?
|
|
16
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control canonical ref is unavailable"
|
|
17
|
+
end
|
|
18
|
+
# Existing first-registration bootstrap. The lower CAS initializes
|
|
19
|
+
# and authenticates its seed prefix before accepting any event.
|
|
20
|
+
{"commit" => nil, "events" => {}, "introductions" => {}}
|
|
21
|
+
else
|
|
22
|
+
journal.canonical_event_inventory!(commit: commit)
|
|
23
|
+
end
|
|
24
|
+
unless inventory.fetch("commit") == commit
|
|
25
|
+
raise AttemptErrors::EvidenceUnavailable, "Control canonical snapshot differs"
|
|
26
|
+
end
|
|
27
|
+
events = inventory.fetch("events").fetch(assignment, [])
|
|
28
|
+
introductions = inventory.fetch("introductions").fetch(assignment, {})
|
|
29
|
+
registrations = events.select { |event| event["type"] == "authority_mutation" &&
|
|
30
|
+
event.dig("payload", "operation") == "register_assignment" }
|
|
31
|
+
first = registrations.first&.dig("payload", "data")
|
|
32
|
+
registration = registrations.last&.dig("payload", "data")
|
|
33
|
+
if params["attempt_id"]
|
|
34
|
+
chain = events.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
35
|
+
reserves = chain.select { |event| event.dig("payload", "operation") == "reserve_attempt" }
|
|
36
|
+
unless reserves.one? && reserves.first.dig("payload", "data", "mapping_id") == mapping
|
|
37
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control reservation is unavailable"
|
|
38
|
+
end
|
|
39
|
+
prefix = introductions.fetch(reserves.first.fetch("digest"))
|
|
40
|
+
registration = definition(journal, assignment, commit: prefix)
|
|
41
|
+
end
|
|
42
|
+
unless registration || params.key?("definition_bytes")
|
|
43
|
+
raise AttemptErrors::NotFound, "canonical assignment registration is missing"
|
|
44
|
+
end
|
|
45
|
+
selection = first&.fetch("lifecycle_control")
|
|
46
|
+
selected = selection ? control_original_descriptor!(selection) : @deployment
|
|
47
|
+
original_map = selected.mapping(mapping)
|
|
48
|
+
project = selected.project(original_map.fetch("project_id"))
|
|
49
|
+
unless original_map.fetch("project_id") == map.fetch("project_id") &&
|
|
50
|
+
project.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
|
|
51
|
+
[journal.repo_root, journal.ref, journal.checkout_root]
|
|
52
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control journal association differs"
|
|
53
|
+
end
|
|
54
|
+
control_owner_unchanged!(selected, original_map, map)
|
|
55
|
+
if first
|
|
56
|
+
authenticate_inventory_registration!(journal, assignment, events, first, mapping, original_map,
|
|
57
|
+
commit, introductions)
|
|
58
|
+
authenticate_inventory_registration!(journal, assignment, events, registration, mapping, original_map,
|
|
59
|
+
commit, introductions) unless registration == first
|
|
60
|
+
unless registrations.all? { |event| event.dig("payload", "data", "lifecycle_control") == selection }
|
|
61
|
+
raise AttemptErrors::EvidenceUnavailable, "Assignment control owner was replaced"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
control = exclusion_for(original_map, journal, deployment: selected, selection: selection)
|
|
65
|
+
actual_selection = control.selection!
|
|
66
|
+
if actual_selection.fetch("descriptor_sha256") != selected.artifact_reference.fetch("sha256") ||
|
|
67
|
+
selection && actual_selection != selection
|
|
68
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control root differs"
|
|
69
|
+
end
|
|
70
|
+
proposed = params.key?("definition_bytes") ? validated_registration_definition!(params, map) : nil
|
|
71
|
+
tasks = [registration&.fetch("task_id"), proposed&.task_id].compact.uniq.sort
|
|
72
|
+
tasks.each { |task| token!(task) }
|
|
73
|
+
keys = tasks.map { |task| control.task_key(task) } + [control.assignment_key(assignment)]
|
|
74
|
+
{commit: commit, registration: registration, descriptor: selected, map: original_map,
|
|
75
|
+
selection: actual_selection, exclusion: control, keys: keys.freeze}.freeze
|
|
76
|
+
rescue KeyError, TypeError, JSON::ParserError
|
|
77
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control registration is unavailable"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def control_original_descriptor!(selection)
|
|
81
|
+
unless selection.is_a?(Hash) && selection.keys.sort == %w[descriptor_sha256 root_identity] &&
|
|
82
|
+
selection["descriptor_sha256"].is_a?(String) && selection["descriptor_sha256"].match?(/\A[0-9a-f]{64}\z/)
|
|
83
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control selection is malformed"
|
|
84
|
+
end
|
|
85
|
+
sha = selection.fetch("descriptor_sha256")
|
|
86
|
+
return @deployment if sha == protected_descriptor_sha256!
|
|
87
|
+
unless @deployment_history
|
|
88
|
+
raise AttemptErrors::EvidenceUnavailable, "Original control descriptor is unavailable"
|
|
89
|
+
end
|
|
90
|
+
@deployment_history.descriptor!(sha256: sha)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def control_owner_unchanged!(selected, original_map, current_map)
|
|
94
|
+
old_authority = selected.authority(original_map.fetch("authority_id"))
|
|
95
|
+
current_authority = @deployment.authority(current_map.fetch("authority_id"))
|
|
96
|
+
slot_fields = %w[slot_id slice_unit service_unit root_directory runtime_directory network_namespace_path]
|
|
97
|
+
unless original_map.fetch("authority_id") == current_map.fetch("authority_id") &&
|
|
98
|
+
old_authority.values_at("state_root", "uid", "gid") == current_authority.values_at("state_root", "uid", "gid") &&
|
|
99
|
+
original_map.fetch("execution_scope").slice(*slot_fields) == current_map.fetch("execution_scope").slice(*slot_fields)
|
|
100
|
+
raise AttemptErrors::Conflict, "Control ownership changes require a fresh assignment"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def recheck_control_registration!(context, params, map, journal, commit: journal.ref_value, held_exclusion: context.fetch(:exclusion))
|
|
105
|
+
fresh = if context.fetch(:commit) == commit
|
|
106
|
+
context
|
|
107
|
+
else
|
|
108
|
+
control_registration_context!(params, map, journal, commit: commit)
|
|
109
|
+
end
|
|
110
|
+
unless fresh.fetch(:registration) == context.fetch(:registration) &&
|
|
111
|
+
fresh.fetch(:selection) == context.fetch(:selection) && fresh.fetch(:keys) == context.fetch(:keys) &&
|
|
112
|
+
journal.ref_value == commit && context.fetch(:exclusion).selection! == context.fetch(:selection)
|
|
113
|
+
raise AttemptErrors::Conflict, "Control registration changed while entering exclusion"
|
|
114
|
+
end
|
|
115
|
+
held_exclusion.verify_unchanged!
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validated_registration_definition!(params, map)
|
|
120
|
+
bytes = params.fetch("definition_bytes")
|
|
121
|
+
unless bytes.is_a?(String) && bytes.bytesize <= 32_768 && params["definition_digest"].is_a?(String) &&
|
|
122
|
+
Digest::SHA256.hexdigest(bytes) == params["definition_digest"]
|
|
123
|
+
raise ArgumentError, "invalid assignment definition digest or size"
|
|
124
|
+
end
|
|
125
|
+
value = JSON.parse(bytes)
|
|
126
|
+
allowed = %w[session_id name description created_at updated_at source_config parent task_id project_id prepared_work review_campaign campaign_execution]
|
|
127
|
+
unless value.is_a?(Hash) && (value.keys - allowed).empty? &&
|
|
128
|
+
%w[session_id name created_at source_config task_id project_id].all? { |key| value[key].is_a?(String) && !value[key].empty? } &&
|
|
129
|
+
value["session_id"] == params["assignment_id"] && value["project_id"] == map["project_id"]
|
|
130
|
+
raise ArgumentError, "invalid managed assignment definition"
|
|
131
|
+
end
|
|
132
|
+
CampaignExecution.validate_parent!(value.fetch("review_campaign")) if value.key?("review_campaign")
|
|
133
|
+
assignment = Models::Assignment.from_h(value)
|
|
134
|
+
raise ArgumentError, "definition is not managed" unless assignment.managed?
|
|
135
|
+
token!(assignment.task_id)
|
|
136
|
+
assignment
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|