ace-assign 0.70.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/handbook/skills/as-assign-drive/SKILL.md +5 -2
- data/handbook/workflow-instructions/assign/drive.wf.md +77 -0
- data/lib/ace/assign/atoms/candidate_limits.rb +20 -0
- data/lib/ace/assign/atoms/git_blob_batch.rb +59 -0
- data/lib/ace/assign/authority/candidate_blob_batch.rb +55 -0
- data/lib/ace/assign/authority/candidate_head_comparison.rb +139 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +55 -25
- data/lib/ace/assign/authority/command_execution.rb +180 -0
- data/lib/ace/assign/authority/endcap.rb +4 -2
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +1 -1
- data/lib/ace/assign/authority/endcap_publication.rb +0 -166
- data/lib/ace/assign/authority/endcap_service_context.rb +38 -0
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +176 -0
- data/lib/ace/assign/authority/{endcap_services.rb → fixed_service_lifecycle.rb} +42 -46
- data/lib/ace/assign/authority/ordinary_completion.rb +13 -3
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +40 -4
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +81 -0
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +143 -0
- data/lib/ace/assign/authority/ordinary_results.rb +11 -1
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +180 -0
- data/lib/ace/assign/authority/ordinary_reviews.rb +214 -0
- data/lib/ace/assign/authority/ordinary_supervisor.rb +99 -0
- data/lib/ace/assign/authority/prepared_work.rb +7 -2
- data/lib/ace/assign/authority/release_artifact_manifest.rb +91 -0
- data/lib/ace/assign/authority/retained_input.rb +53 -0
- data/lib/ace/assign/authority/transfer_codec.rb +5 -3
- data/lib/ace/assign/cli/commands/submit_candidate.rb +1 -1
- data/lib/ace/assign/molecules/canonical_evidence.rb +25 -8
- data/lib/ace/assign/molecules/evidence_journal.rb +17 -30
- data/lib/ace/assign/molecules/journal_mutation.rb +2 -1
- data/lib/ace/assign/molecules/receipt_verifier.rb +8 -2
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- data/lib/ace/assign/organisms/attempt_coordinator.rb +40 -3
- data/lib/ace/assign/organisms/ordinary_driver.rb +310 -0
- data/lib/ace/assign/version.rb +1 -1
- metadata +19 -4
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Ace
|
|
4
4
|
module Assign
|
|
5
5
|
module Authority
|
|
6
|
-
|
|
6
|
+
module FixedServiceLifecycle
|
|
7
7
|
SERVICE_IDENTITY = %w[request_id assignment_id attempt_id project_id operation input_digest target
|
|
8
8
|
authorization service_id caller_uid candidate_head candidate_generation executor_uid transport policy_digest
|
|
9
9
|
worker_process_binding launch_ticket reservation_generation mapping_id].freeze
|
|
@@ -17,7 +17,7 @@ module Ace
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def service_settlement_evidence!(journal:, events:, params:, map:, commit:)
|
|
20
|
-
|
|
20
|
+
service_journal!(journal)
|
|
21
21
|
journal.verify_commit!(commit)
|
|
22
22
|
retained = journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
23
23
|
.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
@@ -80,7 +80,7 @@ module Ace
|
|
|
80
80
|
# Called only by the installed journal composition, including lower
|
|
81
81
|
# service writers. It never reacquires lifecycle/authority locks.
|
|
82
82
|
def authorize_service_update!(journal:, existing:, replacement:, pending:)
|
|
83
|
-
|
|
83
|
+
service_journal!(journal)
|
|
84
84
|
if existing && existing.key?("executor_process_binding") &&
|
|
85
85
|
Atoms::EvidenceDigest.digest(existing.fetch("executor_process_binding")) != Atoms::EvidenceDigest.digest(replacement["executor_process_binding"])
|
|
86
86
|
raise AttemptErrors::InvalidState, "original cleanup executor cannot change"
|
|
@@ -114,28 +114,27 @@ module Ace
|
|
|
114
114
|
end
|
|
115
115
|
bytes = pending && pending.fetch(:service_inputs, {})[replacement.fetch("request_id")]
|
|
116
116
|
raise AttemptErrors::UnauthorizedIdentity, "fresh service effect requires its exact original input" unless bytes.is_a?(String)
|
|
117
|
-
map =
|
|
117
|
+
map = service_mapping!(replacement.fetch("mapping_id"))
|
|
118
118
|
unless replacement["project_id"] == map["project_id"] && replacement["caller_uid"] == map["worker_uid"]
|
|
119
119
|
raise AttemptErrors::UnauthorizedIdentity, "protected service mapping differs"
|
|
120
120
|
end
|
|
121
121
|
params = replacement.slice("mapping_id", "assignment_id", "attempt_id", "candidate_generation").merge("head" => replacement.fetch("candidate_head"))
|
|
122
122
|
events = pending.fetch(:current_events)
|
|
123
|
-
|
|
124
|
-
origin =
|
|
125
|
-
unless replacement
|
|
126
|
-
replacement["launch_ticket"] == origin.fetch("launch_ticket") && replacement["reservation_generation"] == origin.fetch("reservation_generation")
|
|
123
|
+
service_scope_open!(events: events, params: params, map: map)
|
|
124
|
+
origin = service_origin!(events, params)
|
|
125
|
+
unless service_origin_matches!(replacement, origin)
|
|
127
126
|
raise AttemptErrors::UnauthorizedIdentity, "protected service origin differs"
|
|
128
127
|
end
|
|
129
|
-
receiver =
|
|
128
|
+
receiver = service_project!(map.fetch("project_id")).fetch("service_receivers").fetch(replacement.fetch("service_id"))
|
|
130
129
|
raise AttemptErrors::UnauthorizedIdentity, "protected service executor differs" unless receiver.fetch("executor_uid") == replacement.fetch("executor_uid")
|
|
131
130
|
current = exact_candidate!(candidate(events), params)
|
|
132
|
-
|
|
131
|
+
service_review!(journal, events, params, map, current, service_operation: replacement.fetch("operation")) unless %w[create update].include?(replacement.fetch("operation"))
|
|
133
132
|
service_policy!.prepare!(replacement, input_bytes: bytes)
|
|
134
133
|
if begin_dispatch && %w[prune-preserved-workspace publish].include?(replacement.fetch("operation"))
|
|
135
134
|
executor = replacement.fetch("executor_process_binding")
|
|
136
135
|
boot = replacement.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
|
|
137
136
|
Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
|
|
138
|
-
credentials =
|
|
137
|
+
credentials = service_project!(map.fetch("project_id")).fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
|
|
139
138
|
unless executor.slice("uid", "gid", "groups") == credentials.slice("gid", "groups").merge("uid" => receiver.fetch("executor_uid"))
|
|
140
139
|
raise AttemptErrors::UnauthorizedIdentity, "original cleanup executor credentials differ"
|
|
141
140
|
end
|
|
@@ -164,8 +163,8 @@ module Ace
|
|
|
164
163
|
def authorize_service_transfer!(request, params, map, peer, role)
|
|
165
164
|
service_policy!
|
|
166
165
|
service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid")) unless request.fetch("operation") == "complete_service"
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
with_service_scope(params: params, map: map) do |journal, _registration|
|
|
167
|
+
service_journal!(journal)
|
|
169
168
|
if request.fetch("operation") == "request_service"
|
|
170
169
|
existing = journal.service_request(params.fetch("request_id"))
|
|
171
170
|
if existing
|
|
@@ -173,8 +172,8 @@ module Ace
|
|
|
173
172
|
service_replay_binding!(existing, params, map)
|
|
174
173
|
else
|
|
175
174
|
events = attempt_events(journal, params)
|
|
176
|
-
origin =
|
|
177
|
-
|
|
175
|
+
origin = service_origin!(events, params)
|
|
176
|
+
service_worker!(params.fetch("worker_process_binding"), :worker, map, origin)
|
|
178
177
|
service_receiver!(peer, role, map, params.fetch("service_id"))
|
|
179
178
|
end
|
|
180
179
|
else
|
|
@@ -191,7 +190,7 @@ module Ace
|
|
|
191
190
|
# This immutable observation grants no service claim or dispatch permit.
|
|
192
191
|
def workspace_prune_preview_context(_request, params, map, peer, role, transfer)
|
|
193
192
|
raise ArgumentError, "preview context forbids upload" unless transfer.nil?
|
|
194
|
-
|
|
193
|
+
with_service_scope(params: params, map: map) do |journal, _registration|
|
|
195
194
|
{data: workspace_prune_preview_context_at!(journal: journal, params: params, map: map, peer: peer, role: role), replayed: false}
|
|
196
195
|
end
|
|
197
196
|
end
|
|
@@ -203,11 +202,11 @@ module Ace
|
|
|
203
202
|
|
|
204
203
|
def workspace_prune_preview_context_at!(journal:, params:, map:, peer:, role:)
|
|
205
204
|
service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid"))
|
|
206
|
-
|
|
205
|
+
service_journal!(journal)
|
|
207
206
|
commit = journal.ref_value
|
|
208
|
-
events =
|
|
209
|
-
origin =
|
|
210
|
-
|
|
207
|
+
events = service_preview_events!(journal: journal, commit: commit, params: params, map: map)
|
|
208
|
+
origin = service_origin!(events, params)
|
|
209
|
+
service_worker!(params.fetch("worker_process_binding"), :worker, map, origin)
|
|
211
210
|
receiver = service_receiver!(peer, role, map, params.fetch("service_id"))
|
|
212
211
|
service_policy!.workspace_prune_receiver!(project: map.fetch("project_id"), uid: map.fetch("worker_uid"),
|
|
213
212
|
service_id: params.fetch("service_id"), executor_uid: receiver.fetch("executor_uid"))
|
|
@@ -220,7 +219,7 @@ module Ace
|
|
|
220
219
|
"journal_commit" => commit,
|
|
221
220
|
"maintenance" => params.slice("mapping_id", "assignment_id", "attempt_id").merge("project_id" => map.fetch("project_id")),
|
|
222
221
|
"head" => selected.fetch("head"), "candidate_generation" => selected.fetch("candidate_generation"),
|
|
223
|
-
"worker_process_binding" => origin
|
|
222
|
+
"worker_process_binding" => service_origin_fields!(origin).fetch("worker_process_binding"),
|
|
224
223
|
"caller_process_binding" => params.fetch("worker_process_binding")
|
|
225
224
|
}
|
|
226
225
|
raise AttemptErrors::Conflict, "preview canonical selection changed" unless journal.ref_value == commit
|
|
@@ -232,8 +231,8 @@ module Ace
|
|
|
232
231
|
private
|
|
233
232
|
|
|
234
233
|
def service_status(_request, params, map, peer, role)
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
with_service_scope(params: params, map: map) do |journal, _registration|
|
|
235
|
+
service_journal!(journal)
|
|
237
236
|
commit = journal.ref_value
|
|
238
237
|
read = role == :executor && journal.service_settlement_read(params.fetch("request_id"), commit: commit)
|
|
239
238
|
record = read ? read.fetch(:record) : journal.service_request(params.fetch("request_id"), commit: commit)
|
|
@@ -261,7 +260,7 @@ module Ace
|
|
|
261
260
|
review && review["reviewer_uid"] == peer["uid"] && review["head"] == params["head"] &&
|
|
262
261
|
review["candidate_generation"] == params["candidate_generation"] && @kernel.descendant?(peer, review.fetch("reviewer_process_binding"))
|
|
263
262
|
when :supervisor
|
|
264
|
-
|
|
263
|
+
service_project!(map.fetch("project_id")).fetch("supervisor_uids").include?(peer["uid"])
|
|
265
264
|
else
|
|
266
265
|
false
|
|
267
266
|
end
|
|
@@ -287,8 +286,8 @@ module Ace
|
|
|
287
286
|
authorize_service_transfer!(request, params, map, peer, role)
|
|
288
287
|
raise ArgumentError, "structured service input transfer is missing" unless transfer && transfer.count == 1
|
|
289
288
|
bytes = transfer.bytes.dup.freeze
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
with_service_scope(params: params, map: map) do |journal, _registration|
|
|
290
|
+
service_journal!(journal)
|
|
292
291
|
record = journal.service_request(params.fetch("request_id"))
|
|
293
292
|
service_executor!(peer, role, record)
|
|
294
293
|
service_ticket!(record, params, map, peer)
|
|
@@ -296,10 +295,9 @@ module Ace
|
|
|
296
295
|
raise AttemptErrors::Conflict, "authorization input binding differs"
|
|
297
296
|
end
|
|
298
297
|
events = attempt_events(journal, params)
|
|
299
|
-
|
|
300
|
-
origin =
|
|
301
|
-
unless record
|
|
302
|
-
record["launch_ticket"] == origin.fetch("launch_ticket") && record["reservation_generation"] == origin.fetch("reservation_generation")
|
|
298
|
+
service_scope_open!(events: events, params: params, map: map)
|
|
299
|
+
origin = service_origin!(events, params)
|
|
300
|
+
unless service_origin_matches!(record, origin)
|
|
303
301
|
raise AttemptErrors::UnauthorizedIdentity, "authorization launch origin differs"
|
|
304
302
|
end
|
|
305
303
|
current = exact_candidate!(candidate(events), params)
|
|
@@ -307,7 +305,7 @@ module Ace
|
|
|
307
305
|
raise AttemptErrors::Conflict, "authorization candidate changed"
|
|
308
306
|
end
|
|
309
307
|
service_receiver!(peer, role, map, record.fetch("service_id"))
|
|
310
|
-
|
|
308
|
+
service_review!(journal, events, params, map, current, service_operation: record.fetch("operation")) unless %w[create update].include?(record.fetch("operation"))
|
|
311
309
|
prepared = service_policy!.prepare!(record, input_bytes: bytes)
|
|
312
310
|
data = params.slice("request_id", "claim_binding", "head", "candidate_generation").merge(
|
|
313
311
|
"policy_digest" => prepared.fetch(:policy_digest), "operation_digest" => prepared.fetch(:operation_digest))
|
|
@@ -324,8 +322,8 @@ module Ace
|
|
|
324
322
|
raise ArgumentError, "structured service input transfer is missing" unless transfer && transfer.count == 1
|
|
325
323
|
transfer.bytes.dup.freeze
|
|
326
324
|
end
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
with_service_scope(params: params, map: map) do |journal, _registration|
|
|
326
|
+
service_journal!(journal)
|
|
329
327
|
unless request.fetch("operation") == "complete_service"
|
|
330
328
|
input_record = request.fetch("operation") == "request_service" ? params : journal.service_request(params.fetch("request_id"))
|
|
331
329
|
# Replay bypasses the mutation callback, never input validation.
|
|
@@ -348,8 +346,8 @@ module Ace
|
|
|
348
346
|
service_ticket!(record, params, map, peer)
|
|
349
347
|
completion_binding!(record, params, admitted) if request.fetch("operation") == "complete_service"
|
|
350
348
|
elsif !retained
|
|
351
|
-
origin =
|
|
352
|
-
|
|
349
|
+
origin = service_origin!(attempt_events(journal, params), params)
|
|
350
|
+
service_worker!(params.fetch("worker_process_binding"), :worker, map, origin)
|
|
353
351
|
service_receiver!(peer, role, map, params.fetch("service_id"))
|
|
354
352
|
end
|
|
355
353
|
result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
@@ -433,7 +431,7 @@ module Ace
|
|
|
433
431
|
end
|
|
434
432
|
|
|
435
433
|
def service_receiver!(peer, role, map, service_id)
|
|
436
|
-
project =
|
|
434
|
+
project = service_project!(map.fetch("project_id"))
|
|
437
435
|
receiver = project.fetch("service_receivers").fetch(service_id)
|
|
438
436
|
unless role == :executor && peer["uid"] == receiver.fetch("executor_uid") &&
|
|
439
437
|
project.fetch("service_executor_uids").include?(peer["uid"]) &&
|
|
@@ -447,18 +445,17 @@ module Ace
|
|
|
447
445
|
end
|
|
448
446
|
|
|
449
447
|
def service_claim_plan(journal, events, params, map, peer, role, input_bytes)
|
|
450
|
-
origin =
|
|
448
|
+
origin = service_origin!(events, params)
|
|
451
449
|
worker = params.fetch("worker_process_binding")
|
|
452
|
-
|
|
450
|
+
service_worker!(worker, :worker, map, origin)
|
|
453
451
|
current = exact_candidate!(candidate(events), params)
|
|
454
|
-
|
|
452
|
+
service_review!(journal, events, params, map, current, service_operation: params.fetch("operation")) unless %w[create update].include?(params.fetch("operation"))
|
|
455
453
|
receiver = service_receiver!(peer, role, map, params.fetch("service_id"))
|
|
456
454
|
binding = params.slice("request_id", "assignment_id", "attempt_id", "operation", "input_digest", "target",
|
|
457
455
|
"authorization", "service_id").merge("project_id" => map.fetch("project_id"),
|
|
458
456
|
"caller_uid" => map.fetch("worker_uid"), "mapping_id" => params.fetch("mapping_id"), "candidate_head" => current.fetch("head"),
|
|
459
|
-
"candidate_generation" => current.fetch("candidate_generation")
|
|
460
|
-
|
|
461
|
-
"launch_ticket" => origin.fetch("launch_ticket"), "reservation_generation" => origin.fetch("reservation_generation"))
|
|
457
|
+
"candidate_generation" => current.fetch("candidate_generation"))
|
|
458
|
+
.merge(service_origin_fields!(origin))
|
|
462
459
|
prepared = service_policy!.prepare!(binding, input_bytes: input_bytes)
|
|
463
460
|
binding = prepared.fetch(:binding).merge("policy_digest" => prepared.fetch(:policy_digest))
|
|
464
461
|
unless binding["executor_uid"] == receiver.fetch("executor_uid")
|
|
@@ -488,16 +485,15 @@ module Ace
|
|
|
488
485
|
# A repeated begin never grants a second invocation, including after
|
|
489
486
|
# lease expiry or terminality. It reports only retained ticket truth.
|
|
490
487
|
return {data: service_projection(record).merge("invocation" => "already_started")} unless record["dispatch_phase"] == "issued"
|
|
491
|
-
origin =
|
|
492
|
-
unless record
|
|
493
|
-
record["launch_ticket"] == origin.fetch("launch_ticket") && record["reservation_generation"] == origin.fetch("reservation_generation")
|
|
488
|
+
origin = service_origin!(events, params)
|
|
489
|
+
unless service_origin_matches!(record, origin)
|
|
494
490
|
raise AttemptErrors::UnauthorizedIdentity, "dispatch launch origin differs"
|
|
495
491
|
end
|
|
496
492
|
current = exact_candidate!(candidate(events), params)
|
|
497
493
|
unless record["candidate_head"] == current["head"] && record["candidate_generation"] == current["candidate_generation"]
|
|
498
494
|
raise AttemptErrors::Conflict, "dispatch candidate changed"
|
|
499
495
|
end
|
|
500
|
-
|
|
496
|
+
service_review!(journal, events, params, map, current, service_operation: record.fetch("operation")) unless %w[create update].include?(record.fetch("operation"))
|
|
501
497
|
service_policy!.prepare!(record, input_bytes: input_bytes)
|
|
502
498
|
replacement = record.merge("dispatch_phase" => "dispatch_started")
|
|
503
499
|
if record.fetch("operation") == "prune-preserved-workspace"
|
|
@@ -28,6 +28,7 @@ module Ace
|
|
|
28
28
|
end
|
|
29
29
|
registered = registration(params.fetch("assignment_id"), commit: @journal.ref_value)
|
|
30
30
|
raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
|
|
31
|
+
verify_result_owner!(request, peer, role, deadline, registered)
|
|
31
32
|
digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
|
|
32
33
|
@exclusion.with_shared_multi([@exclusion.task_key(registered.fetch("task_id"))], deadline: deadline) do
|
|
33
34
|
@exclusion.with_exclusive(@exclusion.assignment_key(params.fetch("assignment_id")), deadline: deadline) do
|
|
@@ -36,8 +37,8 @@ module Ace
|
|
|
36
37
|
@journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
37
38
|
mutation_id: request.fetch("mutation_id"), operation: "finish", parameters_digest: digest,
|
|
38
39
|
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
39
|
-
replay_authorizer: -> {
|
|
40
|
-
|
|
40
|
+
replay_authorizer: -> { verify_result_owner!(request, peer, role, deadline, registered) }) do |events, prefix, _generation|
|
|
41
|
+
verify_result_owner!(request, peer, role, deadline, registered)
|
|
41
42
|
unless Molecules::CanonicalAttemptState.derive(events) == "running"
|
|
42
43
|
raise AttemptErrors::Conflict, "ordinary completion requires a running attempt"
|
|
43
44
|
end
|
|
@@ -61,7 +62,7 @@ module Ace
|
|
|
61
62
|
raise AttemptErrors::Conflict, "ordinary result scope has unfinished progress" unless complete
|
|
62
63
|
settlement = OrdinaryEffectSettlement.new(journal: @journal).verify!(project: @project,
|
|
63
64
|
assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), events: events, commit: prefix)
|
|
64
|
-
|
|
65
|
+
verify_result_owner!(request, peer, role, deadline, registered)
|
|
65
66
|
plan = Organisms::AttemptCoordinator.finished_transition_plan(events: events, receipt: result.fetch("receipt"), authority: :ordinary)
|
|
66
67
|
plan.merge(data: params.slice("assignment_id", "attempt_id", "result_id", "head", "receipt_digest").merge(
|
|
67
68
|
"authority" => "ordinary", "scope" => scope, "state" => verdict, "settlement" => settlement, "inbox_deliveries" => delivery))
|
|
@@ -73,6 +74,15 @@ module Ace
|
|
|
73
74
|
raise AttemptErrors::EvidenceUnavailable, "ordinary transport settlement is unavailable: #{error.message}"
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def verify_result_owner!(request, peer, role, deadline, registered)
|
|
78
|
+
authorize!(request, peer, role, deadline)
|
|
79
|
+
supervisor = @admission.identity!(peer: peer, pane: request.fetch("params").fetch("pane"), deadline: deadline)
|
|
80
|
+
unless supervisor.role == "coordinator" &&
|
|
81
|
+
supervisor.runtime_binding == supervisor_selection(registered, @journal.ref_value).fetch("binding")
|
|
82
|
+
raise AttemptErrors::UnauthorizedIdentity, "ordinary completion belongs to its original supervisor"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
76
86
|
def with_result_transport(request, deadline)
|
|
77
87
|
if @journal.mutation_result(request.fetch("mutation_id"), commit: @journal.ref_value)
|
|
78
88
|
# A terminal reply is immutable. Current peer authority is still
|
|
@@ -8,6 +8,10 @@ require_relative "ordinary_attempt_start"
|
|
|
8
8
|
require_relative "ordinary_progress"
|
|
9
9
|
require_relative "ordinary_results"
|
|
10
10
|
require_relative "ordinary_completion"
|
|
11
|
+
require_relative "ordinary_supervisor"
|
|
12
|
+
require_relative "ordinary_reviews"
|
|
13
|
+
require_relative "ordinary_merge_evidence"
|
|
14
|
+
require_relative "ordinary_release_artifacts"
|
|
11
15
|
require_relative "../molecules/lifecycle_exclusion"
|
|
12
16
|
|
|
13
17
|
module Ace
|
|
@@ -21,22 +25,37 @@ module Ace
|
|
|
21
25
|
include OrdinaryProgress
|
|
22
26
|
include OrdinaryResults
|
|
23
27
|
include OrdinaryCompletion
|
|
24
|
-
|
|
28
|
+
include OrdinarySupervisor
|
|
29
|
+
include OrdinaryReviews
|
|
30
|
+
include OrdinaryMergeEvidence
|
|
31
|
+
include OrdinaryReleaseArtifacts
|
|
32
|
+
OPERATIONS = (%w[register_assignment registration_status start_attempt finish merge_evidence] + OrdinaryProgress::PROGRESS_OPERATIONS + OrdinaryResults::RESULT_OPERATIONS + OrdinarySupervisor::SUPERVISOR_OPERATIONS + OrdinaryReviews::REVIEW_OPERATIONS + OrdinaryReleaseArtifacts::RELEASE_OPERATIONS).freeze
|
|
25
33
|
TRANSFER_OPERATIONS = {"register_assignment" => {direction: :upload, purpose: :candidate, roles: [:supervisor]}}
|
|
26
|
-
.merge(OrdinaryProgress::PROGRESS_TRANSFERS).merge(OrdinaryResults::RESULT_TRANSFERS).freeze
|
|
34
|
+
.merge(OrdinaryProgress::PROGRESS_TRANSFERS).merge(OrdinaryResults::RESULT_TRANSFERS).merge(OrdinaryReviews::REVIEW_TRANSFERS).merge(OrdinaryReleaseArtifacts::RELEASE_TRANSFERS).freeze
|
|
27
35
|
REGISTER_KEYS = %w[assignment_id definition_digest prepared_head prepared_tree manifest_sha256 expected_generation pane transfer].freeze
|
|
28
36
|
|
|
29
|
-
def initialize(project:, admission:, journal:, exclusion:, input_root:, inbox: nil)
|
|
37
|
+
def initialize(project:, admission:, journal:, exclusion:, input_root:, inbox: nil, command_evidence: nil, release_authorizer: nil)
|
|
30
38
|
@project, @admission, @journal, @exclusion = project, admission, journal, exclusion
|
|
31
39
|
@input_root = PrivateDirectory.verify!(input_root)
|
|
32
40
|
unless inbox.nil? || inbox.is_a?(Ace::Herdr::Organisms::Inbox)
|
|
33
41
|
raise ArgumentError, "ordinary completion requires the installed Inbox owner"
|
|
34
42
|
end
|
|
35
|
-
|
|
43
|
+
unless release_authorizer.nil? || release_authorizer.respond_to?(:call)
|
|
44
|
+
raise ArgumentError, "release artifact authorization requires its installed policy owner"
|
|
45
|
+
end
|
|
46
|
+
@inbox, @command_evidence, @release_authorizer = inbox, command_evidence, release_authorizer
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
def authorize_transfer!(request:, peer:, role:, deadline:)
|
|
39
50
|
authorize!(request, peer, role, deadline)
|
|
51
|
+
if OrdinaryReleaseArtifacts::RELEASE_TRANSFERS.key?(request.fetch("operation"))
|
|
52
|
+
authorize_ordinary_release!(request, peer, role, deadline)
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
if OrdinaryReviews::REVIEW_TRANSFERS.key?(request.fetch("operation"))
|
|
56
|
+
authorize_ordinary_review!(request, peer, role, deadline)
|
|
57
|
+
return
|
|
58
|
+
end
|
|
40
59
|
if OrdinaryResults::RESULT_TRANSFERS.key?(request.fetch("operation"))
|
|
41
60
|
validate_result!(request)
|
|
42
61
|
authorize_start_replay!(request, peer, role, deadline) if request.fetch("operation") == "submit_result"
|
|
@@ -54,7 +73,16 @@ module Ace
|
|
|
54
73
|
|
|
55
74
|
def dispatch(request:, peer:, role:, deadline:, transfer: nil)
|
|
56
75
|
authorize!(request, peer, role, deadline)
|
|
76
|
+
return ordinary_review_operation(request, peer, role, deadline, transfer) if OrdinaryReviews::REVIEW_OPERATIONS.include?(request.fetch("operation"))
|
|
57
77
|
case request.fetch("operation")
|
|
78
|
+
when "retain_release_artifact"
|
|
79
|
+
retain_release_artifact(request, peer, role, deadline, transfer)
|
|
80
|
+
when "merge_evidence"
|
|
81
|
+
raise ArgumentError, "merge evidence read forbids transfer" if transfer
|
|
82
|
+
ordinary_merge_evidence(request, peer, role, deadline)
|
|
83
|
+
when "supervisor_status", "adopt_supervisor"
|
|
84
|
+
raise ArgumentError, "ordinary supervisor operation forbids transfer" if transfer
|
|
85
|
+
supervisor_operation(request, peer, role, deadline)
|
|
58
86
|
when "registration_status"
|
|
59
87
|
strict!(request.fetch("params"), %w[assignment_id])
|
|
60
88
|
raise ArgumentError, "status mutation must be null" unless request.fetch("mutation_id").nil?
|
|
@@ -134,6 +162,7 @@ module Ace
|
|
|
134
162
|
{events: [], blobs: {definition_path => definition, bundle_path => bytes, queue_path => queue.bytes}, data: {
|
|
135
163
|
"authority" => "ordinary", "assignment_id" => assignment.id, "project_id" => @project,
|
|
136
164
|
"task_id" => assignment.task_id, "registered_by" => principal, "phase" => "registered",
|
|
165
|
+
"supervisor_binding" => identity.runtime_binding,
|
|
137
166
|
"definition_ref" => definition_path, "definition_digest" => params.fetch("definition_digest"),
|
|
138
167
|
"queue_ref" => queue_path, "queue_sha256" => queue_sha,
|
|
139
168
|
"prepared_work" => work.reference(head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree")),
|
|
@@ -165,6 +194,13 @@ module Ace
|
|
|
165
194
|
unless request.fetch("project_id") == @project && @admission.authorize!(request: request, peer: peer, deadline: deadline) == role
|
|
166
195
|
raise AttemptErrors::UnauthorizedIdentity, "ordinary lifecycle principal differs"
|
|
167
196
|
end
|
|
197
|
+
# Project commands share the canonical journal but never the managed
|
|
198
|
+
# assignment namespace. Reserve their prefix on every ordinary route,
|
|
199
|
+
# including transfer admission, status and registration replay.
|
|
200
|
+
assignment = request.fetch("params")["assignment_id"]
|
|
201
|
+
if assignment.is_a?(String) && assignment.start_with?("command-")
|
|
202
|
+
raise ArgumentError, "ordinary assignment uses the reserved project-command namespace"
|
|
203
|
+
end
|
|
168
204
|
end
|
|
169
205
|
|
|
170
206
|
def validate_registration!(request)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
# The installed fixed merge consumes this same lifecycle, never a local
|
|
7
|
+
# receipt file. Locks precede journal CAS; no callback acquires an inverted
|
|
8
|
+
# lifecycle lock while holding the journal writer lock.
|
|
9
|
+
module OrdinaryMergeEvidence
|
|
10
|
+
def with_merge_scope(record:, deadline:, exclusive: false)
|
|
11
|
+
assignment, attempt = record.values_at("assignment_id", "attempt_id")
|
|
12
|
+
token!(assignment)
|
|
13
|
+
token!(attempt)
|
|
14
|
+
registered = registration(assignment, commit: @journal.ref_value)
|
|
15
|
+
raise AttemptErrors::EvidenceUnavailable, "merge assignment is unregistered" unless registered
|
|
16
|
+
@exclusion.with_shared_multi([@exclusion.task_key(registered.fetch("task_id"))], deadline: deadline) do
|
|
17
|
+
key = @exclusion.assignment_key(assignment)
|
|
18
|
+
if exclusive
|
|
19
|
+
@exclusion.with_exclusive(key, deadline: deadline) { yield }
|
|
20
|
+
else
|
|
21
|
+
@exclusion.with_shared_multi([key], deadline: deadline) { yield }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def verify_merge_evidence_at!(record:, commit:, deadline:)
|
|
27
|
+
unless record.fetch("project_id") == @project
|
|
28
|
+
raise AttemptErrors::ReceiptRejected, "merge evidence project differs"
|
|
29
|
+
end
|
|
30
|
+
request = record.fetch("request")
|
|
31
|
+
events = result_events(record, commit)
|
|
32
|
+
current = ordinary_candidate_event(events)&.dig("payload", "data")
|
|
33
|
+
raise AttemptErrors::ReceiptRejected, "merge candidate is unavailable" unless current
|
|
34
|
+
projection = merge_evidence_at!(events: events, commit: commit, head: request.fetch("candidate_head"),
|
|
35
|
+
generation: current.fetch("candidate_generation"), execution_ids: request.fetch("test_evidence").map { |reference| reference.fetch("reference") }, deadline: deadline)
|
|
36
|
+
unless request.fetch("review_evidence") == projection.fetch("review_evidence") && request.fetch("test_evidence") == projection.fetch("test_evidence")
|
|
37
|
+
raise AttemptErrors::ReceiptRejected, "merge requires exact canonical review and executed test references"
|
|
38
|
+
end
|
|
39
|
+
origin = origin_at!(events: events, commit: commit)
|
|
40
|
+
observed = @admission.recorded_execution!(binding: origin.fetch("runtime_binding"), head: request.fetch("candidate_head"), deadline: deadline)
|
|
41
|
+
unless observed.fetch(:runtime_binding) == origin.fetch("runtime_binding")
|
|
42
|
+
raise AttemptErrors::UnauthorizedIdentity, "merge producer native identity changed"
|
|
43
|
+
end
|
|
44
|
+
true
|
|
45
|
+
rescue KeyError, TypeError
|
|
46
|
+
raise AttemptErrors::ReceiptRejected, "canonical merge evidence is incomplete"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def merge_evidence_at!(events:, commit:, head:, generation:, execution_ids:, deadline:)
|
|
50
|
+
unless @command_evidence && @command_evidence.journal.equal?(@journal) && execution_ids.is_a?(Array) &&
|
|
51
|
+
execution_ids.size.between?(1, 16) && execution_ids.uniq == execution_ids
|
|
52
|
+
raise AttemptErrors::EvidenceUnavailable, "merge requires the installed canonical executed-test owner"
|
|
53
|
+
end
|
|
54
|
+
candidate = candidate_at!(events: events, commit: commit, head: head, generation: generation)
|
|
55
|
+
review = review_at!(events: events, commit: commit, head: head, generation: generation)
|
|
56
|
+
tests = execution_ids.map { |id| @command_evidence.test_evidence_at!(execution_id: id, commit: commit,
|
|
57
|
+
head: head, tree: candidate.fetch("tree"), deadline: deadline) }
|
|
58
|
+
{"review_evidence" => {"schema" => "ace.review.acceptance-reference/v1", "reference" => review.fetch("review_id"),
|
|
59
|
+
"reviewer" => review.fetch("reviewer_actor"), "verdict" => "approved", "reviewed_head" => head,
|
|
60
|
+
"recorded_at" => review.fetch("recorded_at"), "record_sha256" => review.fetch("acceptance_event_digest")},
|
|
61
|
+
"test_evidence" => tests}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def ordinary_merge_evidence(request, peer, role, deadline)
|
|
67
|
+
params = request.fetch("params")
|
|
68
|
+
strict!(params, %w[assignment_id attempt_id head candidate_generation test_execution_ids])
|
|
69
|
+
raise ArgumentError, "merge evidence read mutation must be null" unless request.fetch("mutation_id").nil?
|
|
70
|
+
with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
|
|
71
|
+
prefix = @journal.ref_value
|
|
72
|
+
projection = merge_evidence_at!(events: result_events(params, prefix), commit: prefix, head: params.fetch("head"),
|
|
73
|
+
generation: params.fetch("candidate_generation"), execution_ids: params.fetch("test_execution_ids"), deadline: deadline)
|
|
74
|
+
authorize!(request, peer, role, deadline)
|
|
75
|
+
{data: projection.merge("journal_commit" => prefix), replayed: false}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "release_artifact_manifest"
|
|
4
|
+
require_relative "candidate_transfer"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Assign
|
|
8
|
+
module Authority
|
|
9
|
+
# Retention is a canonical input, never a publication permit or result.
|
|
10
|
+
# Native/candidate/review and release policy are rechecked at every CAS.
|
|
11
|
+
module OrdinaryReleaseArtifacts
|
|
12
|
+
RELEASE_OPERATIONS = %w[retain_release_artifact].freeze
|
|
13
|
+
RELEASE_TRANSFERS = {"retain_release_artifact" =>
|
|
14
|
+
{direction: :upload, purpose: :release_artifact, roles: %i[worker supervisor]}}.freeze
|
|
15
|
+
RELEASE_PARAMETERS = %w[assignment_id attempt_id head candidate_generation pane expected_generation
|
|
16
|
+
authorization manifest transfer].freeze
|
|
17
|
+
|
|
18
|
+
# Read an accepted import at the current immutable canonical prefix only.
|
|
19
|
+
# Historical commit selection is not supported by the native origin reader.
|
|
20
|
+
# The later effect consumer must independently admit its fresh scope,
|
|
21
|
+
# candidate, review, authorization and fixed executor before dispatch.
|
|
22
|
+
def release_artifact_at!(events:, commit:, artifact_id:)
|
|
23
|
+
token!(artifact_id)
|
|
24
|
+
unless commit && Models::EvidenceEvent.chain_valid?(events)
|
|
25
|
+
raise AttemptErrors::EvidenceUnavailable, "release artifact history is unavailable"
|
|
26
|
+
end
|
|
27
|
+
accepted = events.select { |event| event["type"] == "authority_mutation" &&
|
|
28
|
+
event.dig("payload", "operation") == "retain_release_artifact" && event.dig("payload", "mutation_id") == artifact_id }
|
|
29
|
+
data = accepted.one? && accepted.first.dig("payload", "data")
|
|
30
|
+
unless data && data["artifact_id"] == artifact_id && data["input_digest"] == Atoms::EvidenceDigest.digest(data.fetch("input"))
|
|
31
|
+
raise AttemptErrors::EvidenceUnavailable, "release artifact acceptance is unavailable"
|
|
32
|
+
end
|
|
33
|
+
origin = data.fetch("producer_origin")
|
|
34
|
+
retained = @journal.read_events(origin.fetch("assignment_id"), commit: commit)
|
|
35
|
+
.select { |event| event["attempt_id"] == origin.fetch("attempt_id") }
|
|
36
|
+
unless retained == events && ordinary_origin_at!(events, commit, running: false) == origin
|
|
37
|
+
raise AttemptErrors::EvidenceUnavailable, "release artifact original scope differs"
|
|
38
|
+
end
|
|
39
|
+
context = release_evidence_context(data.fetch("input"), origin, data.fetch("authorization"), artifact_id)
|
|
40
|
+
bytes = Molecules::CanonicalEvidence.new(journal: @journal).read(data.fetch("artifact"), **context, commit: commit)
|
|
41
|
+
unless bytes.bytesize == data.dig("input", "publication", "artifact_bytes") &&
|
|
42
|
+
Digest::SHA256.hexdigest(bytes) == data.dig("input", "target", "artifact_digest")
|
|
43
|
+
raise AttemptErrors::EvidenceUnavailable, "release artifact retained bytes differ"
|
|
44
|
+
end
|
|
45
|
+
data.merge("bytes" => bytes)
|
|
46
|
+
rescue KeyError, TypeError
|
|
47
|
+
raise AttemptErrors::EvidenceUnavailable, "release artifact acceptance is incomplete"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def validate_ordinary_release!(request)
|
|
53
|
+
params = request.fetch("params")
|
|
54
|
+
strict!(params, RELEASE_PARAMETERS)
|
|
55
|
+
%w[assignment_id attempt_id authorization].each { |key| token!(params.fetch(key)) }
|
|
56
|
+
token!(request.fetch("mutation_id"))
|
|
57
|
+
unless params["head"].is_a?(String) && CandidateTransfer::SHA.match?(params["head"]) &&
|
|
58
|
+
%w[candidate_generation expected_generation].all? { |key| params[key].is_a?(Integer) && params[key].positive? }
|
|
59
|
+
raise ArgumentError, "invalid release artifact candidate selection"
|
|
60
|
+
end
|
|
61
|
+
ReleaseArtifactManifest.validate!(params.fetch("manifest"))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def authorize_ordinary_release!(request, peer, role, deadline)
|
|
65
|
+
validate_ordinary_release!(request)
|
|
66
|
+
params = request.fetch("params")
|
|
67
|
+
with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
|
|
68
|
+
prefix = @journal.ref_value
|
|
69
|
+
release_context_at!(request, peer, role, deadline, result_events(params, prefix), prefix)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Caller holds lifecycle exclusion before entering the journal writer.
|
|
74
|
+
# This method never acquires another lifecycle lock or trusts a path.
|
|
75
|
+
def release_context_at!(request, peer, role, deadline, events, commit)
|
|
76
|
+
authorize!(request, peer, role, deadline)
|
|
77
|
+
unless %i[worker supervisor].include?(role) && @release_authorizer
|
|
78
|
+
raise AttemptErrors::UnauthorizedIdentity, "installed release artifact authorization is unavailable"
|
|
79
|
+
end
|
|
80
|
+
params = request.fetch("params")
|
|
81
|
+
origin = origin_at!(events: events, commit: commit)
|
|
82
|
+
current = candidate_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
|
|
83
|
+
reference = candidate_reference_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
|
|
84
|
+
observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: current.fetch("head"), deadline: deadline)
|
|
85
|
+
unless observed.fetch(:identity).runtime_binding == origin.fetch("runtime_binding")
|
|
86
|
+
raise AttemptErrors::UnauthorizedIdentity, "release artifact producer native identity changed"
|
|
87
|
+
end
|
|
88
|
+
review_at!(events: events, commit: commit, head: current.fetch("head"), generation: current.fetch("candidate_generation"))
|
|
89
|
+
input = ReleaseArtifactManifest.publication_input(manifest: params.fetch("manifest"), candidate: current, reference: reference)
|
|
90
|
+
binding = params.slice("assignment_id", "attempt_id", "authorization").merge(
|
|
91
|
+
"operation" => "publish", "project_id" => @project, "candidate_head" => current.fetch("head"),
|
|
92
|
+
"caller_uid" => origin.dig("runtime_binding", "process_identity", "uid"),
|
|
93
|
+
"input_digest" => Atoms::EvidenceDigest.digest(input), "target" => input.fetch("target"))
|
|
94
|
+
unless @release_authorizer.call(binding)
|
|
95
|
+
raise AttemptErrors::UnauthorizedIdentity, "release artifact manifest is not authorized"
|
|
96
|
+
end
|
|
97
|
+
{input: input, binding: binding, origin: origin}
|
|
98
|
+
rescue SecurityError
|
|
99
|
+
raise AttemptErrors::UnauthorizedIdentity, "release artifact manifest authorization is unavailable"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def retain_release_artifact(request, peer, role, deadline, transfer)
|
|
103
|
+
validate_ordinary_release!(request)
|
|
104
|
+
unless transfer && transfer.count == 1
|
|
105
|
+
raise AttemptErrors::MalformedTransfer, "release artifact requires its one bounded binary transfer"
|
|
106
|
+
end
|
|
107
|
+
params = request.fetch("params")
|
|
108
|
+
with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
|
|
109
|
+
prefix = @journal.ref_value
|
|
110
|
+
release_context_at!(request, peer, role, deadline, result_events(params, prefix), prefix)
|
|
111
|
+
bytes = transfer.bytes.dup.freeze
|
|
112
|
+
ReleaseArtifactManifest.verify!(bytes, params.fetch("manifest"))
|
|
113
|
+
digest = Atoms::EvidenceDigest.digest("project_id" => @project, "params" => params)
|
|
114
|
+
recheck = lambda do
|
|
115
|
+
current = @journal.ref_value
|
|
116
|
+
release_context_at!(request, peer, role, deadline, result_events(params, current), current)
|
|
117
|
+
end
|
|
118
|
+
@journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
119
|
+
mutation_id: request.fetch("mutation_id"), operation: "retain_release_artifact", parameters_digest: digest,
|
|
120
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
121
|
+
replay_authorizer: recheck) do |events, commit, _generation|
|
|
122
|
+
selected = release_context_at!(request, peer, role, deadline, events, commit)
|
|
123
|
+
artifact_id = request.fetch("mutation_id")
|
|
124
|
+
context = release_evidence_context(selected.fetch(:input), selected.fetch(:origin), params.fetch("authorization"), artifact_id)
|
|
125
|
+
plan = Molecules::CanonicalEvidence.new(journal: @journal).import_plan(**context,
|
|
126
|
+
artifacts: [bytes], admitted_after_event_digest: events.last.fetch("digest"))
|
|
127
|
+
plan.merge(data: {"artifact_id" => artifact_id, "input" => selected.fetch(:input),
|
|
128
|
+
"input_digest" => selected.fetch(:binding).fetch("input_digest"), "artifact" => plan.fetch(:references).first,
|
|
129
|
+
"authorization" => params.fetch("authorization"), "producer_origin" => selected.fetch(:origin)})
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def release_evidence_context(input, origin, authorization, artifact_id)
|
|
135
|
+
{kind: "release-artifact", project_id: @project, assignment_id: origin.fetch("assignment_id"),
|
|
136
|
+
attempt_id: origin.fetch("attempt_id"), peer_uid: origin.dig("runtime_binding", "process_identity", "uid"),
|
|
137
|
+
binding: {"input" => input, "producer_origin" => origin, "authorization" => authorization},
|
|
138
|
+
request_id_or_event_id: artifact_id, generation: input.fetch("publication").fetch("candidate_reference").fetch("candidate_generation")}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|