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
|
@@ -4,17 +4,22 @@ require "json"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "ace/herdr/molecules/protected_native_control"
|
|
6
6
|
require_relative "client"
|
|
7
|
+
require_relative "candidate_transfer"
|
|
8
|
+
require_relative "launch_control_channel"
|
|
7
9
|
|
|
8
10
|
module Ace
|
|
9
11
|
module Assign
|
|
10
12
|
module Authority
|
|
11
13
|
class LaunchDriver
|
|
14
|
+
LAUNCH_DEADLINE = 30
|
|
12
15
|
def initialize(mapping_id:, deployment: Deployment.load,
|
|
13
|
-
kernel: Ace::Runtime::Molecules::ProtectedLinux.new, client: nil, native: nil
|
|
16
|
+
kernel: Ace::Runtime::Molecules::ProtectedLinux.new, client: nil, native: nil,
|
|
17
|
+
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
14
18
|
@mapping_id, @deployment, @kernel = mapping_id, deployment, kernel
|
|
15
19
|
@map = deployment.mapping(mapping_id)
|
|
16
20
|
@client = client || Client.new(mapping_id: mapping_id, deployment: deployment, kernel: kernel)
|
|
17
21
|
@native = native
|
|
22
|
+
@clock = clock
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def preflight
|
|
@@ -22,40 +27,62 @@ module Ace
|
|
|
22
27
|
@client.call("launch_preflight", {}).data
|
|
23
28
|
end
|
|
24
29
|
|
|
25
|
-
def launch(assignment_id:, definition_bytes:, scope:, base_head:, mutation_id: SecureRandom.hex(16))
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
def launch(assignment_id:, definition_bytes:, prepared_bundle:, scope:, base_head:, mutation_id: SecureRandom.hex(16))
|
|
31
|
+
state = nil
|
|
32
|
+
deadline = @clock.call + LAUNCH_DEADLINE
|
|
33
|
+
@deployment.verify!(@mapping_id, kernel: @kernel)
|
|
34
|
+
launch_call(deadline, "launch_preflight", {})
|
|
35
|
+
registration = launch_call(deadline, "registration_status", {"assignment_id" => assignment_id}).data
|
|
28
36
|
digest = Digest::SHA256.hexdigest(definition_bytes)
|
|
37
|
+
definition = JSON.parse(definition_bytes)
|
|
38
|
+
prepared = definition.fetch("prepared_work")
|
|
39
|
+
unless definition["session_id"] == assignment_id && prepared["scope"] == scope &&
|
|
40
|
+
prepared_bundle.is_a?(String) && prepared_bundle.bytesize.between?(1, CandidateTransfer::MAX_BYTES)
|
|
41
|
+
raise ArgumentError, "prepared launch selection or bundle is invalid"
|
|
42
|
+
end
|
|
43
|
+
bundle_sha = Digest::SHA256.hexdigest(prepared_bundle)
|
|
29
44
|
registered = if registration["definition_digest"] == digest
|
|
45
|
+
unless registration.values_at("prepared_bundle_bytes", "prepared_bundle_sha256") == [prepared_bundle.bytesize, bundle_sha]
|
|
46
|
+
raise AttemptErrors::Conflict, "original registration prepared bundle differs"
|
|
47
|
+
end
|
|
30
48
|
registration
|
|
31
49
|
else
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"expected_generation" => registration.fetch("generation", 0)},
|
|
50
|
+
launch_call(deadline, "register_assignment", {"assignment_id" => assignment_id,
|
|
51
|
+
"definition_digest" => digest, "prepared_head" => prepared.fetch("prepared_head"), "prepared_tree" => prepared.fetch("prepared_tree"),
|
|
52
|
+
"manifest_sha256" => prepared.fetch("manifest_sha256"), "expected_generation" => registration.fetch("generation", 0)},
|
|
53
|
+
mutation_id: "#{mutation_id}-register", upload_parts: [prepared_bundle], purpose: :candidate).data
|
|
35
54
|
end
|
|
36
|
-
reserved =
|
|
55
|
+
reserved = launch_call(deadline, "reserve_attempt", {"assignment_id" => assignment_id, "scope" => scope,
|
|
37
56
|
"worker_uid" => @map.fetch("worker_uid"), "runtime" => "herdr", "base_head" => base_head,
|
|
38
57
|
"launcher_process_binding" => @kernel.capture(Process.pid),
|
|
39
|
-
"expected_generation" => registered.fetch("definition_generation")}, mutation_id: "#{mutation_id}-reserve")
|
|
58
|
+
"expected_generation" => registered.fetch("definition_generation")}, mutation_id: "#{mutation_id}-reserve") { |reply| state = reply.data }
|
|
40
59
|
# Only this actual CAS winner may create. A cached canonical reply is
|
|
41
60
|
# evidence of ownership, never evidence that native creation is fresh.
|
|
42
61
|
return reserved.data.merge("required_action" => "inspect_retained_reservation_no_creation_permission") if reserved.replayed
|
|
43
62
|
state = reserved.data
|
|
44
|
-
inspected =
|
|
63
|
+
inspected = launch_call(deadline, "inspect_launch", state.slice("assignment_id", "attempt_id")).data
|
|
45
64
|
state = inspected
|
|
46
65
|
origin = inspected.fetch("native_binding")
|
|
47
|
-
return close_before_native_release(state, mutation_id) unless origin
|
|
66
|
+
return close_before_native_release(state, mutation_id, deadline: deadline) unless origin
|
|
48
67
|
fixed = JSON.parse(JSON.generate(@map))
|
|
49
68
|
fixed.fetch("native").merge!(origin.slice("server_identity", "socket_identity", "workspace_id"))
|
|
50
69
|
@native ||= Ace::Herdr::Molecules::ProtectedNativeControl.new(mapping: fixed, kernel: @kernel)
|
|
70
|
+
launch_remaining!(deadline)
|
|
51
71
|
binding = @native.create(mapping_id: @mapping_id, ticket: state.fetch("launch_ticket"))
|
|
52
72
|
child_handle = @kernel.pin(binding.fetch("process_identity"))
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
guarded = @native.guarded_binding!(binding)
|
|
74
|
+
unless guarded.reject { |key, _| key == "guarded_origin" } == binding
|
|
75
|
+
raise AttemptErrors::EvidenceUnavailable, "original native capture changed launch binding"
|
|
76
|
+
end
|
|
77
|
+
launch_remaining!(deadline)
|
|
78
|
+
recorded = launch_call(deadline, "record_launch", lifecycle_params(state, binding).merge("guarded_origin" => guarded.fetch("guarded_origin")), mutation_id: "#{mutation_id}-record").data
|
|
79
|
+
bound = launch_call(deadline, "bind_process", lifecycle_params(recorded, binding), mutation_id: "#{mutation_id}-bind").data
|
|
55
80
|
# Exact original child is reobserved after canonical bind, before release.
|
|
56
81
|
@native.observe(binding.fetch("native_origin").merge("process_binding" => binding))
|
|
57
82
|
@kernel.live!(binding.fetch("process_identity"))
|
|
58
|
-
issued =
|
|
83
|
+
issued = launch_call(deadline, "release_launch", lifecycle_params(bound, binding), mutation_id: "#{mutation_id}-release").data
|
|
84
|
+
@issued_state = issued
|
|
85
|
+
@steering_binding = binding.merge("guarded_origin" => recorded.fetch("guarded_origin"))
|
|
59
86
|
issued
|
|
60
87
|
rescue StandardError
|
|
61
88
|
# Response loss or partial creation never retries native creation.
|
|
@@ -68,6 +95,47 @@ module Ace
|
|
|
68
95
|
child_handle&.close
|
|
69
96
|
end
|
|
70
97
|
|
|
98
|
+
# The same original launcher remains foreground. A transient channel
|
|
99
|
+
# failure reconnects this process and never repeats native issuance.
|
|
100
|
+
def serve_control!(state:)
|
|
101
|
+
unless state.equal?(@issued_state) && state["phase"] == "issued" && @steering_binding
|
|
102
|
+
raise AttemptErrors::EvidenceUnavailable, "Only the fresh original launcher may retain control"
|
|
103
|
+
end
|
|
104
|
+
@seen_prompt_intents ||= {}
|
|
105
|
+
@prompt_issuance_refs ||= {}
|
|
106
|
+
@reported_prompt_completions ||= {}
|
|
107
|
+
announced = false
|
|
108
|
+
until @control_cancelled
|
|
109
|
+
begin
|
|
110
|
+
break if announced && authenticated_control_release?(state)
|
|
111
|
+
report_retained_prompt_completions!(state)
|
|
112
|
+
report_retained_input_inhibition!(state)
|
|
113
|
+
@client.with_launch_control(state: state) do |socket, codec, ready|
|
|
114
|
+
if @control_original_binding_digest && ready.fetch("original_binding_digest") != @control_original_binding_digest
|
|
115
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher control binding changed"
|
|
116
|
+
end
|
|
117
|
+
@control_original_binding_digest ||= ready.fetch("original_binding_digest")
|
|
118
|
+
unless announced
|
|
119
|
+
yield({"version" => 1, "type" => "launch_ready", "mapping_id" => @mapping_id,
|
|
120
|
+
"assignment_id" => state.fetch("assignment_id"), "attempt_id" => state.fetch("attempt_id"),
|
|
121
|
+
"generation" => ready.fetch("generation"), "journal_commit" => ready.fetch("journal_commit"),
|
|
122
|
+
"original_binding_digest" => ready.fetch("original_binding_digest")})
|
|
123
|
+
announced = true
|
|
124
|
+
end
|
|
125
|
+
original_prompt_loop(socket, codec, state, ready)
|
|
126
|
+
end
|
|
127
|
+
rescue Ace::Runtime::RuntimeUnavailableError, AttemptErrors::EvidenceUnavailable, AttemptErrors::UnauthorizedIdentity, IOError, SystemCallError
|
|
128
|
+
# Endpoint refusal also cannot authenticate terminal release.
|
|
129
|
+
# EOF does not prove the actor drained or justify a second send.
|
|
130
|
+
sleep(0.25) unless @control_cancelled
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def request_control_cancel
|
|
136
|
+
@control_cancelled = true
|
|
137
|
+
end
|
|
138
|
+
|
|
71
139
|
def terminate(state:, binding:, evidence:, mutation_id: SecureRandom.hex(16))
|
|
72
140
|
observed = state["termination_observation"]
|
|
73
141
|
unless observed.is_a?(Hash) && observed["status"] == "exited" && observed["process_identity"] == binding["process_identity"]
|
|
@@ -83,23 +151,234 @@ module Ace
|
|
|
83
151
|
end
|
|
84
152
|
|
|
85
153
|
private
|
|
86
|
-
|
|
154
|
+
|
|
155
|
+
# Client preparation precedes its transport deadline. Recheck the one
|
|
156
|
+
# original budget after each reply and before subsequent native effects.
|
|
157
|
+
def launch_call(deadline, operation, params, **options)
|
|
158
|
+
remaining = launch_remaining!(deadline)
|
|
159
|
+
result = @client.call(operation, params, **options, timeout: remaining)
|
|
160
|
+
yield result if block_given?
|
|
161
|
+
launch_remaining!(deadline)
|
|
162
|
+
result
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def launch_remaining!(deadline)
|
|
166
|
+
remaining = deadline - @clock.call
|
|
167
|
+
unless remaining.positive?
|
|
168
|
+
raise Ace::Runtime::RuntimeUnavailableError, "original launch deadline expired"
|
|
169
|
+
end
|
|
170
|
+
remaining
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Status is queried before reopening a private stream: synchronous
|
|
174
|
+
# canonical reads cannot consume the original channel's heartbeat wait.
|
|
175
|
+
def authenticated_control_release?(state)
|
|
176
|
+
params = state.slice("assignment_id", "attempt_id")
|
|
177
|
+
composition = @deployment.authority(@map.fetch("authority_id")).fetch("composition")
|
|
178
|
+
params["result_candidate_generation"] = nil if composition == "services"
|
|
179
|
+
data = @client.call("attempt_status", params, mutation_id: nil, timeout: 30).data
|
|
180
|
+
unless data.is_a?(Hash) && data.values_at("mapping_id", "assignment_id", "attempt_id") ==
|
|
181
|
+
[@mapping_id, state.fetch("assignment_id"), state.fetch("attempt_id")] &&
|
|
182
|
+
data["original_binding_digest"] == @control_original_binding_digest &&
|
|
183
|
+
data["process_binding"] == @steering_binding.reject { |key, _| key == "guarded_origin" } &&
|
|
184
|
+
data["launcher_identity"] == state.fetch("launcher_identity") &&
|
|
185
|
+
data["state"].is_a?(String) && %w[reserved running uncertain succeeded failed stopped].include?(data["state"]) &&
|
|
186
|
+
data["generation"].is_a?(Integer) && data["generation"] >= 0 &&
|
|
187
|
+
data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
188
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher canonical status differs"
|
|
189
|
+
end
|
|
190
|
+
terminal = data.fetch("terminal_event_id")
|
|
191
|
+
release = data.fetch("reservation_release_event_id")
|
|
192
|
+
unless [terminal, release].all? { |value| value.nil? || value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/) } &&
|
|
193
|
+
(release.nil? || !terminal.nil? && %w[succeeded failed stopped].include?(data.fetch("state")))
|
|
194
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher terminal release metadata differs"
|
|
195
|
+
end
|
|
196
|
+
%w[succeeded failed stopped].include?(data.fetch("state")) && !terminal.nil? && !release.nil?
|
|
197
|
+
rescue KeyError, TypeError, NoMethodError
|
|
198
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher canonical status is malformed"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def report_retained_prompt_completions!(state)
|
|
202
|
+
@seen_prompt_intents.each do |id, evidence|
|
|
203
|
+
next if @reported_prompt_completions[id]
|
|
204
|
+
next unless %w[submitted not_issued].include?(evidence["outcome"])
|
|
205
|
+
reference = @prompt_issuance_refs.fetch(id)
|
|
206
|
+
accepted = @client.call("launch_prompt_completion", state.slice("assignment_id", "attempt_id").merge(reference).merge(
|
|
207
|
+
"guarded_evidence" => evidence), mutation_id: nil, timeout: 30).data
|
|
208
|
+
unless accepted.keys.sort == %w[intent_event_id journal_commit outcome] && accepted["intent_event_id"] == id && accepted["outcome"] == evidence.fetch("outcome") &&
|
|
209
|
+
accepted["journal_commit"].is_a?(String) && accepted["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
210
|
+
raise AttemptErrors::EvidenceUnavailable, "Retained native completion acknowledgement differs"
|
|
211
|
+
end
|
|
212
|
+
@reported_prompt_completions[id] = accepted.fetch("journal_commit")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
def original_prompt_loop(socket, codec, state, ready)
|
|
216
|
+
wire = Ace::Runtime::Molecules::ProtectedSocket
|
|
217
|
+
until @control_cancelled
|
|
218
|
+
deadline = wire.deadline(30)
|
|
219
|
+
frame = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
220
|
+
if frame.is_a?(Hash) && frame.keys.sort == %w[nonce type version] && frame["type"] == "launch_control_idle" &&
|
|
221
|
+
frame["version"].is_a?(Integer) && frame["version"] == 1 && frame["nonce"].is_a?(String) && frame["nonce"].match?(/\A[0-9a-f]{32}\z/)
|
|
222
|
+
wire.write(socket, frame.merge("type" => "launch_control_idle_ack"), deadline: deadline, limit: 16_384)
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
if frame.is_a?(Hash) && frame["type"] == "launch_input_inhibit"
|
|
226
|
+
original_input_inhibit!(socket, state, ready, frame, deadline)
|
|
227
|
+
next
|
|
228
|
+
end
|
|
229
|
+
raise AttemptErrors::EvidenceUnavailable, "Original launcher input is inhibited" if @input_inhibited
|
|
230
|
+
if frame.is_a?(Hash) && frame["type"] == "launch_review_delegate"
|
|
231
|
+
original_review_delegate!(socket, state, ready, frame, deadline)
|
|
232
|
+
next
|
|
233
|
+
end
|
|
234
|
+
LaunchControlChannel.validate_prompt!(frame)
|
|
235
|
+
unless frame["attempt_id"] == state.fetch("attempt_id") && frame["original_binding_digest"] == ready.fetch("original_binding_digest")
|
|
236
|
+
raise AttemptErrors::EvidenceUnavailable, "Private prompt does not join original launcher"
|
|
237
|
+
end
|
|
238
|
+
evidence = codec.receive_launch_prompt(socket, descriptor: frame.fetch("text_descriptor"),
|
|
239
|
+
transfer_id: frame.fetch("transfer_id"), deadline: deadline) do |input|
|
|
240
|
+
expected = @client.call("launch_prompt_intent", state.slice("assignment_id", "attempt_id").merge(
|
|
241
|
+
frame.slice("mutation_id", "intent_event_id", "journal_commit")), mutation_id: nil).data
|
|
242
|
+
binding = expected.fetch("binding")
|
|
243
|
+
Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(expected.fetch("origin"),
|
|
244
|
+
terminal_id: @steering_binding.fetch("terminal_id"), child: @steering_binding.fetch("process_identity"))
|
|
245
|
+
unless expected["intent_event_id"] == frame["intent_event_id"] && expected["origin"] == @steering_binding.fetch("guarded_origin") &&
|
|
246
|
+
binding.is_a?(Hash) && binding["text_bytes"].is_a?(Integer) &&
|
|
247
|
+
binding.values_at("mapping_id", "assignment_id", "attempt_id", "mutation_id") ==
|
|
248
|
+
[@mapping_id, state.fetch("assignment_id"), state.fetch("attempt_id"), frame.fetch("mutation_id")] &&
|
|
249
|
+
binding["original_binding_digest"] == frame["original_binding_digest"] && binding["text_bytes"] == input.bytes.bytesize &&
|
|
250
|
+
binding["text_sha256"] == Digest::SHA256.hexdigest(input.bytes)
|
|
251
|
+
raise AttemptErrors::EvidenceUnavailable, "Private prompt immutable intent differs"
|
|
252
|
+
end
|
|
253
|
+
id = frame.fetch("intent_event_id")
|
|
254
|
+
@prompt_issuance_refs[id] = frame.slice("mutation_id", "intent_event_id", "original_binding_digest")
|
|
255
|
+
if @seen_prompt_intents.key?(id)
|
|
256
|
+
@seen_prompt_intents.fetch(id)
|
|
257
|
+
else
|
|
258
|
+
@seen_prompt_intents[id] = {"outcome" => "uncertain", "origin" => @steering_binding.fetch("guarded_origin")}
|
|
259
|
+
@native.prompt_preflight!(@steering_binding)
|
|
260
|
+
@seen_prompt_intents[id] = @native.prompt(binding: @steering_binding, text: input.bytes)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
result = frame.slice("mutation_id", "intent_event_id", "original_binding_digest").merge("version" => 1,
|
|
264
|
+
"type" => "prompt_dispatch_outcome", "guarded_evidence" => evidence)
|
|
265
|
+
wire.write(socket, result, deadline: deadline, limit: 16_384)
|
|
266
|
+
recorded = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
267
|
+
LaunchControlChannel.validate_recorded!(recorded, frame, evidence)
|
|
268
|
+
@reported_prompt_completions[frame.fetch("intent_event_id")] = recorded.fetch("journal_commit") if %w[submitted not_issued].include?(evidence.fetch("outcome"))
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def original_review_delegate!(socket, state, ready, frame, deadline)
|
|
273
|
+
LaunchControlChannel.validate_review!(frame)
|
|
274
|
+
unless frame["attempt_id"] == state.fetch("attempt_id") &&
|
|
275
|
+
frame["original_binding_digest"] == ready.fetch("original_binding_digest")
|
|
276
|
+
raise AttemptErrors::EvidenceUnavailable, "Review request belongs to another original launcher"
|
|
277
|
+
end
|
|
278
|
+
selectors = state.slice("assignment_id", "attempt_id").merge(
|
|
279
|
+
frame.slice("mutation_id", "request_event_id", "journal_commit", "original_binding_digest"))
|
|
280
|
+
intent = @client.call("launch_review_intent", selectors, mutation_id: nil,
|
|
281
|
+
timeout: review_exchange_remaining!(deadline)).data
|
|
282
|
+
params = validate_review_intent!(intent, state, frame)
|
|
283
|
+
accepted = @client.call("assign_review", params, mutation_id: "review-delegate.#{frame.fetch('request_event_id')}",
|
|
284
|
+
timeout: review_exchange_remaining!(deadline)).data
|
|
285
|
+
unless accepted.is_a?(Hash) &&
|
|
286
|
+
%w[candidate_generation reviewer_uid].all? { |key| accepted[key].is_a?(Integer) } &&
|
|
287
|
+
%w[head candidate_generation reviewer_uid reviewer_process_binding].all? { |key| accepted[key] == params.fetch(key) } &&
|
|
288
|
+
accepted["generation"].is_a?(Integer) && accepted["generation"] == params.fetch("expected_generation") + 1 &&
|
|
289
|
+
accepted["review_id"].is_a?(String) && accepted["review_id"].match?(/\A[0-9a-f]{32}\z/)
|
|
290
|
+
raise AttemptErrors::EvidenceUnavailable, "Original review assignment reply differs"
|
|
291
|
+
end
|
|
292
|
+
identity = accepted.fetch("reviewer_process_binding")
|
|
293
|
+
boot = params.fetch("reviewer_process_binding").fetch("started_at").split(":", -1)[1]
|
|
294
|
+
Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: boot)
|
|
295
|
+
result = frame.slice("mutation_id", "request_event_id", "original_binding_digest").merge(
|
|
296
|
+
accepted.slice("assignment_event_id", "journal_commit"), "version" => 1, "type" => "launch_review_assigned")
|
|
297
|
+
LaunchControlChannel.validate_review_assigned!(result, frame)
|
|
298
|
+
Ace::Runtime::Molecules::ProtectedSocket.write(socket, result, deadline: deadline, limit: 16_384)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def validate_review_intent!(intent, state, frame)
|
|
302
|
+
fields = %w[assignment_params original_binding_digest request_event_id]
|
|
303
|
+
params = intent.is_a?(Hash) && intent["assignment_params"]
|
|
304
|
+
unless intent.is_a?(Hash) && intent.keys.sort == fields &&
|
|
305
|
+
%w[request_event_id original_binding_digest].all? { |key| intent[key] == frame.fetch(key) } &&
|
|
306
|
+
params.is_a?(Hash) && params.keys.sort == %w[assignment_id attempt_id candidate_generation expected_generation head mapping_id reviewer_process_binding reviewer_uid] &&
|
|
307
|
+
params.values_at("mapping_id", "assignment_id", "attempt_id") == [@mapping_id, state.fetch("assignment_id"), state.fetch("attempt_id")] &&
|
|
308
|
+
params["head"].is_a?(String) && params["head"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
309
|
+
%w[candidate_generation expected_generation reviewer_uid].all? { |key| params[key].is_a?(Integer) && params[key].positive? } &&
|
|
310
|
+
params["reviewer_uid"] != @map.fetch("worker_uid") &&
|
|
311
|
+
params["reviewer_process_binding"].is_a?(Hash) && params["reviewer_process_binding"]["uid"] == params["reviewer_uid"]
|
|
312
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review intent differs"
|
|
313
|
+
end
|
|
314
|
+
identity = params.fetch("reviewer_process_binding")
|
|
315
|
+
boot = identity["started_at"].is_a?(String) && identity["started_at"].split(":", -1)[1]
|
|
316
|
+
Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: boot)
|
|
317
|
+
params
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def review_exchange_remaining!(deadline)
|
|
321
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
322
|
+
raise AttemptErrors::EvidenceUnavailable, "Original review exchange deadline expired" unless remaining.positive?
|
|
323
|
+
[remaining, 30].min
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def original_input_inhibit!(socket, state, ready, frame, deadline)
|
|
327
|
+
LaunchControlChannel.validate_inhibit!(frame)
|
|
328
|
+
unless frame["attempt_id"] == state.fetch("attempt_id") && frame["original_binding_digest"] == ready.fetch("original_binding_digest")
|
|
329
|
+
raise AttemptErrors::EvidenceUnavailable, "Input inhibition does not join original launcher"
|
|
330
|
+
end
|
|
331
|
+
selectors = frame.slice("original_binding_digest", "seal_event_id", "journal_commit")
|
|
332
|
+
selected = @client.call("launch_input_inhibit_selection", state.slice("assignment_id", "attempt_id").merge(selectors), mutation_id: nil).data
|
|
333
|
+
unless selected == frame.slice("attempt_id", "original_binding_digest", "seal_event_id", "journal_commit")
|
|
334
|
+
raise AttemptErrors::EvidenceUnavailable, "Input inhibition original seal selection differs"
|
|
335
|
+
end
|
|
336
|
+
@input_inhibited = true
|
|
337
|
+
@input_inhibition_refs = frame.slice("original_binding_digest", "seal_event_id")
|
|
338
|
+
evidence = @retained_input_inhibition || @native.inhibit_input(binding: @steering_binding)
|
|
339
|
+
if evidence.is_a?(Hash) && evidence["outcome"] == "inhibited"
|
|
340
|
+
@retained_input_inhibition = evidence
|
|
341
|
+
end
|
|
342
|
+
wire = Ace::Runtime::Molecules::ProtectedSocket
|
|
343
|
+
result = @input_inhibition_refs.merge("version" => 1, "type" => "launch_input_inhibit_outcome", "guarded_evidence" => evidence)
|
|
344
|
+
wire.write(socket, result, deadline: deadline, limit: 16_384)
|
|
345
|
+
recorded = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
346
|
+
LaunchControlChannel.validate_inhibit_recorded!(recorded, frame)
|
|
347
|
+
unless @retained_input_inhibition
|
|
348
|
+
raise AttemptErrors::EvidenceUnavailable, "Unconfirmed drain cannot receive positive canonical acknowledgement"
|
|
349
|
+
end
|
|
350
|
+
@reported_input_inhibition = recorded.fetch("journal_commit")
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def report_retained_input_inhibition!(state)
|
|
354
|
+
return unless @retained_input_inhibition && !@reported_input_inhibition
|
|
355
|
+
accepted = @client.call("launch_input_inhibit_completion", state.slice("assignment_id", "attempt_id").merge(@input_inhibition_refs).merge(
|
|
356
|
+
"guarded_evidence" => @retained_input_inhibition), mutation_id: nil, timeout: 30).data
|
|
357
|
+
frame = @input_inhibition_refs.merge("version" => 1, "type" => "launch_input_inhibit")
|
|
358
|
+
unless accepted.is_a?(Hash) && accepted.keys.sort == %w[journal_commit original_binding_digest seal_event_id]
|
|
359
|
+
raise AttemptErrors::EvidenceUnavailable, "Retained input inhibition acknowledgement differs"
|
|
360
|
+
end
|
|
361
|
+
LaunchControlChannel.validate_inhibit_recorded!(accepted.merge("version" => 1, "type" => "launch_input_inhibit_recorded"), frame)
|
|
362
|
+
@reported_input_inhibition = accepted.fetch("journal_commit")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def close_before_native_release(state, mutation_id, deadline:)
|
|
87
366
|
unless state["scope_binding_event_id"]
|
|
88
367
|
return state.merge("required_action" => "inspect_exact_scope")
|
|
89
368
|
end
|
|
90
369
|
selectors = state.slice("assignment_id", "attempt_id")
|
|
91
|
-
sealed =
|
|
370
|
+
sealed = launch_call(deadline, "close_execution_scope", selectors.merge("expected_generation" => state.fetch("generation")),
|
|
92
371
|
mutation_id: "#{mutation_id}-scope-seal").data
|
|
93
|
-
closed =
|
|
372
|
+
closed = launch_call(deadline, "close_execution_scope", selectors.merge("expected_generation" => sealed.fetch("generation")),
|
|
94
373
|
mutation_id: "#{mutation_id}-scope-proof").data
|
|
95
374
|
unless closed["state"] == "closed_no_writers"
|
|
96
375
|
return state.merge("required_action" => "inspect_exact_scope")
|
|
97
376
|
end
|
|
98
|
-
inspected =
|
|
377
|
+
inspected = launch_call(deadline, "inspect_launch", selectors).data
|
|
99
378
|
failure = inspected.fetch("scope_failure_evidence")
|
|
100
379
|
raise AttemptErrors::EvidenceUnavailable, "canonical scope failure selectors unavailable" unless failure
|
|
101
380
|
bytes = JSON.generate(failure)
|
|
102
|
-
|
|
381
|
+
launch_call(deadline, "abort_launch", selectors.merge("launch_ticket" => state.fetch("launch_ticket"),
|
|
103
382
|
"expected_generation" => inspected.fetch("generation"), "failure_evidence" => bytes,
|
|
104
383
|
"failure_digest" => Digest::SHA256.hexdigest(bytes)), mutation_id: "#{mutation_id}-scope-abort").data
|
|
105
384
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class LaunchLifecycle
|
|
7
|
+
def launch_input_inhibit_selection!(request:, peer:, role:)
|
|
8
|
+
params = request.fetch("params")
|
|
9
|
+
strict!(params, %w[mapping_id assignment_id attempt_id original_binding_digest seal_event_id journal_commit])
|
|
10
|
+
raise ArgumentError, "Input inhibition selection requires null mutation ID" unless request.fetch("mutation_id").nil?
|
|
11
|
+
map = steering_principal!(params, peer, role)
|
|
12
|
+
raise AttemptErrors::UnauthorizedIdentity, "Input inhibition selection requires original launcher" unless role == :launcher
|
|
13
|
+
LaunchControlChannel.validate_inhibit!(params.slice("attempt_id", "original_binding_digest", "seal_event_id", "journal_commit").merge(
|
|
14
|
+
"version" => 1, "type" => "launch_input_inhibit"))
|
|
15
|
+
journal = journal_for(map)
|
|
16
|
+
commit = params.fetch("journal_commit")
|
|
17
|
+
journal.verify_canonical_prefix!(commit: commit)
|
|
18
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
19
|
+
authenticate_input_inhibition!(events, params, map, peer: peer)
|
|
20
|
+
{data: params.slice("attempt_id", "original_binding_digest", "seal_event_id", "journal_commit"), replayed: false}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def launch_input_inhibit_completion!(request:, peer:, role:)
|
|
24
|
+
params = request.fetch("params")
|
|
25
|
+
strict!(params, %w[mapping_id assignment_id attempt_id original_binding_digest seal_event_id guarded_evidence])
|
|
26
|
+
raise ArgumentError, "Input inhibition completion requires null mutation ID" unless request.fetch("mutation_id").nil?
|
|
27
|
+
map = steering_principal!(params, peer, role)
|
|
28
|
+
raise AttemptErrors::UnauthorizedIdentity, "Input inhibition completion requires original launcher" unless role == :launcher
|
|
29
|
+
{data: record_original_input_inhibition!(params, map, params.fetch("guarded_evidence"), peer: peer), replayed: false}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def request_original_input_inhibition!(params, map, journal)
|
|
35
|
+
selected = with_containment_exclusion(params, map, journal) do
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
commit = journal.ref_value
|
|
38
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
39
|
+
next true if accepted_input_inhibition?(events, journal, commit)
|
|
40
|
+
state = origin(events, **params.slice("assignment_id", "attempt_id", "mapping_id").transform_keys(&:to_sym))
|
|
41
|
+
# Before native release there is no admitted outside-unit input
|
|
42
|
+
# owner. Its existing native issuer/guarded abort gates remain.
|
|
43
|
+
next true unless issued_input_actor?(events)
|
|
44
|
+
original = original_prompt_record!(events, state, params: params)
|
|
45
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
46
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
47
|
+
raise AttemptErrors::EvidenceUnavailable, "Original scope is not sealed" unless lineage.seal_event
|
|
48
|
+
channel = @control_channels[steering_key(params, map)]
|
|
49
|
+
next false unless channel && !channel.closed?
|
|
50
|
+
frame = {"version" => 1, "type" => "launch_input_inhibit", "attempt_id" => params.fetch("attempt_id"),
|
|
51
|
+
"original_binding_digest" => original.fetch("binding_digest"), "seal_event_id" => lineage.seal_event.fetch("digest"), "journal_commit" => commit}
|
|
52
|
+
[channel, frame]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
return selected if selected == true || selected == false
|
|
56
|
+
channel, frame = selected
|
|
57
|
+
channel.inhibit_input(frame: frame, deadline: wire.deadline(30))
|
|
58
|
+
true
|
|
59
|
+
rescue AttemptErrors::EvidenceUnavailable, Ace::Runtime::RuntimeUnavailableError, IOError, SystemCallError
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def record_original_input_inhibition!(params, map, evidence, peer: nil)
|
|
64
|
+
journal = journal_for(map)
|
|
65
|
+
with_containment_exclusion(params, map, journal) do
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
commit = journal.ref_value
|
|
68
|
+
current = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
69
|
+
@kernel.live!(peer) if peer
|
|
70
|
+
original = authenticate_input_inhibition!(current, params, map, peer: peer)
|
|
71
|
+
journal.verify_input_drained_evidence!(evidence, original.fetch("origin"))
|
|
72
|
+
selection = params.slice("mapping_id", "assignment_id", "attempt_id", "original_binding_digest", "seal_event_id").merge("project_id" => map.fetch("project_id"))
|
|
73
|
+
journal.observe_input_inhibition(selection: selection, evidence: evidence) do |events, _old, _generation|
|
|
74
|
+
@kernel.live!(peer) if peer
|
|
75
|
+
authenticate_input_inhibition!(events, params, map, peer: peer)
|
|
76
|
+
end.fetch(:data).slice("original_binding_digest", "seal_event_id", "journal_commit")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def accepted_input_inhibition?(events, journal, commit)
|
|
82
|
+
proofs = events.select { |event| event["type"] == "input_inhibited" }
|
|
83
|
+
return false if proofs.empty?
|
|
84
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition proof is ambiguous" unless proofs.length == 1
|
|
85
|
+
proof = proofs.first
|
|
86
|
+
payload = proof.fetch("payload")
|
|
87
|
+
unless payload.is_a?(Hash) && payload.keys.sort == (Molecules::JournalInputInhibition::INPUT_INHIBITION_FIELDS + ["guarded_evidence"]).sort
|
|
88
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition proof fields are malformed"
|
|
89
|
+
end
|
|
90
|
+
id = "input-inhibit.#{payload.fetch('seal_event_id')}"
|
|
91
|
+
digest = Digest::SHA256.hexdigest(JSON.generate(journal.send(:canonical_prompt_value, payload)))
|
|
92
|
+
journal.send(:verify_input_inhibition!, payload, mutation_id: id, digest: digest,
|
|
93
|
+
assignment_id: payload.fetch("assignment_id"), attempt_id: payload.fetch("attempt_id"), commit: commit)
|
|
94
|
+
accepted = events.find { |event| event["type"] == "authority_mutation" && event["previous_digest"] == proof.fetch("digest") &&
|
|
95
|
+
event.dig("payload", "operation") == "input_inhibition_observation" && event.dig("payload", "mutation_id") == id }
|
|
96
|
+
expected = payload.slice("original_binding_digest", "seal_event_id")
|
|
97
|
+
data = accepted&.dig("payload", "data")
|
|
98
|
+
unless accepted && accepted.dig("payload", "parameters_digest") == digest &&
|
|
99
|
+
data.is_a?(Hash) && data.reject { |key, _| key == "generation" } == expected
|
|
100
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition acceptance differs"
|
|
101
|
+
end
|
|
102
|
+
true
|
|
103
|
+
rescue KeyError, TypeError, ArgumentError
|
|
104
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition proof is malformed"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def authenticate_input_inhibition!(events, params, map, peer: nil)
|
|
108
|
+
state = origin(events, **params.slice("assignment_id", "attempt_id", "mapping_id").transform_keys(&:to_sym))
|
|
109
|
+
if peer && !@kernel.same?(state.fetch("launcher_identity"), peer)
|
|
110
|
+
raise AttemptErrors::UnauthorizedIdentity, "Input inhibition belongs to another launcher incarnation"
|
|
111
|
+
end
|
|
112
|
+
original = original_prompt_record!(events, state, params: params)
|
|
113
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
114
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
115
|
+
seal = lineage.seal_event
|
|
116
|
+
acceptance = seal && events.find { |event| event["type"] == "authority_mutation" && event["previous_digest"] == seal.fetch("digest") &&
|
|
117
|
+
%w[close_execution_scope stop_attempt].include?(event.dig("payload", "operation")) }
|
|
118
|
+
unless original.fetch("binding_digest") == params.fetch("original_binding_digest") && seal && acceptance &&
|
|
119
|
+
seal.fetch("digest") == params.fetch("seal_event_id")
|
|
120
|
+
raise AttemptErrors::EvidenceUnavailable, "Input inhibition does not join accepted original seal"
|
|
121
|
+
end
|
|
122
|
+
if events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
|
|
123
|
+
raise AttemptErrors::EvidenceUnavailable, "Released scope cannot request original input inhibition"
|
|
124
|
+
end
|
|
125
|
+
original
|
|
126
|
+
rescue KeyError, TypeError, ArgumentError
|
|
127
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition lineage is malformed"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|