ace-assign 0.63.1 → 0.66.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/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- 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 +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- 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 +37 -0
- 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 +398 -0
- 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 +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- 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 +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- 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 +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- 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 +59 -0
- 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/private_directory.rb +33 -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/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- 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 +286 -0
- 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 +187 -0
- 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 +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- 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/terminate.rb +29 -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 +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- 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 +38 -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 +56 -4
- 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 +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- 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 +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -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 +68 -32
- 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 +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- 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 +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Historical assignment lookup stays separate: revocation removes
|
|
10
|
+
# permission, never the original receipt or response.
|
|
11
|
+
def active_review_reservation(events, current)
|
|
12
|
+
reservation = events.reverse.find do |event|
|
|
13
|
+
event["type"] == "authority_mutation" &&
|
|
14
|
+
%w[request_review assign_review].include?(event.dig("payload", "operation")) &&
|
|
15
|
+
!event.dig("payload", "mutation_id").to_s.start_with?("review-delegate.") &&
|
|
16
|
+
event.dig("payload", "data", "head") == current.fetch("head") &&
|
|
17
|
+
event.dig("payload", "data", "candidate_generation") == current.fetch("candidate_generation")
|
|
18
|
+
end
|
|
19
|
+
return unless reservation
|
|
20
|
+
cancelled = events.any? do |event|
|
|
21
|
+
event["type"] == "authority_mutation" && event.dig("payload", "operation") == "cancel_review" &&
|
|
22
|
+
event.dig("payload", "data", "review_event_id") == reservation.fetch("digest")
|
|
23
|
+
end
|
|
24
|
+
reservation unless cancelled
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def active_review_event(events, current)
|
|
28
|
+
reservation = active_review_reservation(events, current)
|
|
29
|
+
return unless reservation
|
|
30
|
+
return reservation if reservation.dig("payload", "operation") == "assign_review"
|
|
31
|
+
events.find do |event|
|
|
32
|
+
event["type"] == "authority_mutation" && event.dig("payload", "operation") == "assign_review" &&
|
|
33
|
+
event.dig("payload", "mutation_id") == "review-delegate.#{reservation.fetch('digest')}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Reserved spelling is not authorization. Reconstruct the immutable
|
|
38
|
+
# request through the original-launcher reader before historical replay,
|
|
39
|
+
# then separately check the active reservation for a fresh mutation.
|
|
40
|
+
def delegated_review_request!(journal, events, request, params, peer, role)
|
|
41
|
+
mutation = request.fetch("mutation_id")
|
|
42
|
+
return unless mutation.is_a?(String) && mutation.start_with?("review-delegate.")
|
|
43
|
+
digest = mutation.delete_prefix("review-delegate.")
|
|
44
|
+
original = events.find { |event| event["digest"] == digest && event["type"] == "authority_mutation" &&
|
|
45
|
+
event.dig("payload", "operation") == "request_review" }
|
|
46
|
+
raise AttemptErrors::UnauthorizedIdentity, "delegation requires a canonical request" unless original
|
|
47
|
+
accepted = journal.mutation_result(original.dig("payload", "mutation_id"))
|
|
48
|
+
selectors = params.slice("mapping_id", "assignment_id", "attempt_id").merge(
|
|
49
|
+
"mutation_id" => original.dig("payload", "mutation_id"), "request_event_id" => digest,
|
|
50
|
+
"journal_commit" => accepted.fetch("journal_commit"),
|
|
51
|
+
"original_binding_digest" => original.dig("payload", "data", "original_binding_digest"))
|
|
52
|
+
intent = @launch.launch_review_intent!(request: request.merge("operation" => "launch_review_intent",
|
|
53
|
+
"mutation_id" => nil, "params" => selectors), peer: peer, role: role)
|
|
54
|
+
unless Atoms::EvidenceDigest.digest(intent.dig(:data, "assignment_params")) == Atoms::EvidenceDigest.digest(params)
|
|
55
|
+
raise AttemptErrors::UnauthorizedIdentity, "delegation differs from exact canonical request"
|
|
56
|
+
end
|
|
57
|
+
original
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cancel_review(request, params, map, peer, role, transfer)
|
|
61
|
+
raise ArgumentError, "review cancellation forbids upload" unless transfer.nil?
|
|
62
|
+
unless params["review_event_id"].is_a?(String) && params["review_event_id"].match?(/\A[0-9a-f]{64}\z/)
|
|
63
|
+
raise ArgumentError, "review cancellation selector differs"
|
|
64
|
+
end
|
|
65
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
66
|
+
protected_journal!(journal)
|
|
67
|
+
events = attempt_events(journal, params)
|
|
68
|
+
authenticate_review_cancellation!(events, params, map, peer, role)
|
|
69
|
+
result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
70
|
+
mutation_id: request.fetch("mutation_id"), operation: "cancel_review",
|
|
71
|
+
parameters_digest: Atoms::EvidenceDigest.digest(params), expected_generation: params.fetch("expected_generation"),
|
|
72
|
+
with_replay: true) do |current_events, _commit, _generation|
|
|
73
|
+
authenticate_review_cancellation!(current_events, params, map, peer, role)
|
|
74
|
+
current = exact_candidate!(candidate(current_events), params)
|
|
75
|
+
reservation = active_review_reservation(current_events, current)
|
|
76
|
+
unless reservation && reservation.fetch("digest") == params.fetch("review_event_id")
|
|
77
|
+
raise AttemptErrors::Conflict, "review reservation is no longer active"
|
|
78
|
+
end
|
|
79
|
+
{data: current.slice("head", "candidate_generation").merge(
|
|
80
|
+
"review_event_id" => reservation.fetch("digest"),
|
|
81
|
+
"review_id" => active_review_event(current_events, current)&.dig("payload", "data", "review_id"), "state" => "cancelled")}
|
|
82
|
+
end
|
|
83
|
+
review_event_reply(journal, result, request, params, "cancellation_event_id")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def review_event_reply(journal, result, request, params, field)
|
|
88
|
+
accepted = journal.read_events(params.fetch("assignment_id"), commit: result.fetch(:data).fetch("journal_commit")).find do |event|
|
|
89
|
+
event["attempt_id"] == params.fetch("attempt_id") && event["type"] == "authority_mutation" &&
|
|
90
|
+
event.dig("payload", "operation") == request.fetch("operation") &&
|
|
91
|
+
event.dig("payload", "mutation_id") == request.fetch("mutation_id")
|
|
92
|
+
end
|
|
93
|
+
raise AttemptErrors::EvidenceUnavailable, "accepted review event is unavailable" unless accepted
|
|
94
|
+
result.merge(data: result.fetch(:data).merge(field => accepted.fetch("digest")))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def authenticate_review_cancellation!(events, params, map, peer, role)
|
|
98
|
+
origin = active_origin(events, params)
|
|
99
|
+
@kernel.live!(peer)
|
|
100
|
+
original = events.find { |event| event["digest"] == params.fetch("review_event_id") }
|
|
101
|
+
unless original && original["type"] == "authority_mutation" &&
|
|
102
|
+
%w[request_review assign_review].include?(original.dig("payload", "operation")) &&
|
|
103
|
+
!original.dig("payload", "mutation_id").to_s.start_with?("review-delegate.") &&
|
|
104
|
+
original.dig("payload", "data", "head") == params.fetch("head") &&
|
|
105
|
+
original.dig("payload", "data", "candidate_generation") == params.fetch("candidate_generation")
|
|
106
|
+
raise AttemptErrors::UnauthorizedIdentity, "review cancellation requires the exact reservation root"
|
|
107
|
+
end
|
|
108
|
+
if role == :launcher
|
|
109
|
+
unless @kernel.same?(origin.fetch("launcher_identity"), peer)
|
|
110
|
+
raise AttemptErrors::UnauthorizedIdentity, "cancellation requires original launcher birth"
|
|
111
|
+
end
|
|
112
|
+
worker_or_launcher!(peer, role, map, origin, launcher_only: true)
|
|
113
|
+
elsif role == :reviewer && original.dig("payload", "operation") == "request_review"
|
|
114
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
115
|
+
credentials = project.fetch("peer_credentials")[peer.fetch("uid").to_s]
|
|
116
|
+
principal = peer.slice("uid", "gid", "groups").merge("role" => "reviewer")
|
|
117
|
+
unless project.fetch("reviewer_uids").include?(peer.fetch("uid")) && peer.fetch("uid") != map.fetch("worker_uid") &&
|
|
118
|
+
credentials && peer.values_at("gid", "groups") == credentials.values_at("gid", "groups") &&
|
|
119
|
+
Atoms::EvidenceDigest.digest(principal) == Atoms::EvidenceDigest.digest(original.dig("payload", "data", "requester"))
|
|
120
|
+
raise AttemptErrors::UnauthorizedIdentity, "cancellation requires the original mapped requester"
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
raise AttemptErrors::UnauthorizedIdentity, "review reservation cancellation is unauthorized"
|
|
124
|
+
end
|
|
125
|
+
original
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def authorize_service_settlement!(request, params, map, peer, role)
|
|
10
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
11
|
+
protected_journal!(journal)
|
|
12
|
+
service_settlement_record!(journal, params, map, peer, role)
|
|
13
|
+
end
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def service_settlement_record!(journal, params, map, peer, role, record: nil)
|
|
18
|
+
record ||= journal.service_request(params.fetch("request_id"))
|
|
19
|
+
raise AttemptErrors::NotFound, "canonical service request is missing" unless record
|
|
20
|
+
service_executor!(peer, role, record)
|
|
21
|
+
receiver = service_receiver!(peer, role, map, record.fetch("service_id"))
|
|
22
|
+
credentials = @deployment.project(map.fetch("project_id")).fetch("peer_credentials").fetch(peer.fetch("uid").to_s)
|
|
23
|
+
unless peer["gid"] == credentials.fetch("gid") && peer["groups"] == credentials.fetch("groups") &&
|
|
24
|
+
receiver.fetch("executor_uid") == record.fetch("executor_uid") &&
|
|
25
|
+
%w[assignment_id attempt_id mapping_id].all? { |key| record[key] == params[key] } &&
|
|
26
|
+
record["project_id"] == map.fetch("project_id") && record["candidate_head"] == params.fetch("head") &&
|
|
27
|
+
record["candidate_generation"] == params.fetch("candidate_generation") &&
|
|
28
|
+
%w[issued dispatch_started].include?(record["dispatch_phase"])
|
|
29
|
+
raise AttemptErrors::UnauthorizedIdentity, "original service settlement identity differs"
|
|
30
|
+
end
|
|
31
|
+
# The normal protected binding validates original ticket/claim, even
|
|
32
|
+
# while effect policy, worker liveness or current candidate changed.
|
|
33
|
+
ServiceEvidence.new(journal: journal).context(record)
|
|
34
|
+
record
|
|
35
|
+
rescue KeyError
|
|
36
|
+
raise AttemptErrors::EvidenceUnavailable, "original service settlement binding is incomplete"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dispatch_service_settlement(request, params, map, peer, role, transfer)
|
|
40
|
+
authorize_service_settlement!(request, params, map, peer, role)
|
|
41
|
+
completion = request.fetch("operation") == "complete_no_effect"
|
|
42
|
+
admitted = completion && ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
|
|
43
|
+
artifact_field: "evidence", reference_key: "ref")
|
|
44
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
45
|
+
record = service_settlement_record!(journal, params, map, peer, role)
|
|
46
|
+
no_effect_completion_binding!(record, params, admitted) if completion
|
|
47
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
48
|
+
mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
|
|
49
|
+
parameters_digest: Atoms::EvidenceDigest.digest(params),
|
|
50
|
+
expected_generation: completion ? nil : params.fetch("expected_generation"),
|
|
51
|
+
generation_mode: completion ? :recorded_completion : :expected, with_replay: true) do |events, commit, generation|
|
|
52
|
+
current = service_settlement_record!(journal, params, map, peer, role)
|
|
53
|
+
if completion
|
|
54
|
+
no_effect_completion_plan(journal, events, current, params, admitted)
|
|
55
|
+
else
|
|
56
|
+
service_challenge_plan(journal, events, current, params, map, generation, commit)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def service_challenge_projection(record)
|
|
63
|
+
record.slice("request_id", "state", "dispatch_ticket_id", "claim_binding", "dispatch_phase").merge(
|
|
64
|
+
"reconciliation_challenge" => %w[succeeded failed-settled].include?(record.fetch("state")) ? nil :
|
|
65
|
+
record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest"))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def service_challenge_plan(journal, events, record, params, map, generation, commit)
|
|
69
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
70
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
71
|
+
unless lineage.sealed? && lineage.binding && record["reservation_generation"] == lineage.binding.fetch("reservation_generation")
|
|
72
|
+
raise AttemptErrors::Conflict, "original service generation is not sealed"
|
|
73
|
+
end
|
|
74
|
+
return {data: service_challenge_projection(record)} if %w[succeeded failed-settled].include?(record.fetch("state"))
|
|
75
|
+
unless %w[uncertain failed].include?(record.fetch("state"))
|
|
76
|
+
raise AttemptErrors::EvidenceUnavailable, "original protected request has no uncertainty outcome"
|
|
77
|
+
end
|
|
78
|
+
if record["challenge_event_digest"]
|
|
79
|
+
begin
|
|
80
|
+
ServiceEvidence.new(journal: journal).challenge!(record, pending: {commit: commit})
|
|
81
|
+
return {data: service_challenge_projection(record)}
|
|
82
|
+
rescue AttemptErrors::EvidenceUnavailable
|
|
83
|
+
# Only a positively authenticated later failure may supersede a
|
|
84
|
+
# challenge. Corrupt/missing old history never becomes freshness.
|
|
85
|
+
old = ServiceEvidence.new(journal: journal).challenge!(record, pending: {commit: commit}, current: false)
|
|
86
|
+
raise unless events.drop_while { |event| event["digest"] != old.fetch("digest") }.drop(1).any? do |event|
|
|
87
|
+
%w[service_claim service_transition].include?(event["type"]) && event.dig("payload", "request_id") == record.fetch("request_id") &&
|
|
88
|
+
%w[uncertain failed].include?(event.dig("payload", "state"))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
failure = events.reverse.find { |event| %w[service_claim service_transition].include?(event["type"]) &&
|
|
93
|
+
event.dig("payload", "request_id") == record.fetch("request_id") && %w[uncertain failed].include?(event.dig("payload", "state")) }
|
|
94
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical service failure is missing" unless failure
|
|
95
|
+
failure_prefix = events.take_while { |event| event["digest"] != failure.fetch("digest") }
|
|
96
|
+
payload = record.slice("request_id", "input_digest", "claim_binding", "dispatch_ticket_id").merge(
|
|
97
|
+
"version" => 1, "no_effect_challenge" => SecureRandom.hex(16), "challenge_generation" => generation + 1,
|
|
98
|
+
"failure_event_digest" => failure.fetch("digest"), "failure_generation" => journal.authority_generation(failure_prefix) + 1)
|
|
99
|
+
recorded_at = Time.now.utc
|
|
100
|
+
challenge = Models::EvidenceEvent.build(type: "service_no_effect_challenge", attempt_id: params.fetch("attempt_id"),
|
|
101
|
+
payload: payload, previous_digest: events.last&.fetch("digest"), recorded_at: recorded_at)
|
|
102
|
+
replacement = record.merge(payload.slice("no_effect_challenge", "challenge_generation"), "challenge_event_digest" => challenge.fetch("digest"))
|
|
103
|
+
{events: [{type: "service_no_effect_challenge", payload: payload, recorded_at: recorded_at}],
|
|
104
|
+
data: service_challenge_projection(replacement), service_updates: [{request_id: record.fetch("request_id"),
|
|
105
|
+
expected: record, replacement: replacement, event_type: "service_challenge"}]}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def no_effect_completion_binding!(record, params, admitted)
|
|
109
|
+
selector = params.fetch("reconciliation_challenge")
|
|
110
|
+
unless selector.is_a?(Hash) && selector.keys.sort == %w[no_effect_challenge challenge_generation challenge_event_digest].sort &&
|
|
111
|
+
selector["no_effect_challenge"].is_a?(String) && selector["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
112
|
+
selector["challenge_generation"].is_a?(Integer) && selector["challenge_generation"].positive? &&
|
|
113
|
+
selector["challenge_event_digest"].is_a?(String) && selector["challenge_event_digest"].match?(/\A[0-9a-f]{64}\z/)
|
|
114
|
+
raise ArgumentError, "no-effect challenge selector is malformed"
|
|
115
|
+
end
|
|
116
|
+
unless selector.all? { |key, value| record[key] == value } && record["claim_binding"] == params.fetch("claim_binding")
|
|
117
|
+
raise AttemptErrors::Conflict, "no-effect challenge or claim differs"
|
|
118
|
+
end
|
|
119
|
+
receipt = admitted.fetch(:receipt)
|
|
120
|
+
unless receipt.keys.sort == Molecules::EvidenceJournal::TERMINAL_RECEIPT_FIELDS.sort && receipt["outcome"] == "failed" &&
|
|
121
|
+
Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS.all? { |field| receipt[field] == record[field] }
|
|
122
|
+
raise AttemptErrors::ReceiptRejected, "no-effect receipt original binding differs"
|
|
123
|
+
end
|
|
124
|
+
digest = Atoms::EvidenceDigest.digest("challenge" => selector, "receipt_sha256" => admitted.fetch(:receipt_sha256),
|
|
125
|
+
"artifacts" => admitted.fetch(:artifacts).map { |bytes| Digest::SHA256.hexdigest(bytes) })
|
|
126
|
+
if record["state"] == "succeeded" || record["no_effect_completion_digest"] && record["no_effect_completion_digest"] != digest
|
|
127
|
+
raise AttemptErrors::Conflict, "no-effect completion contradicts retained outcome"
|
|
128
|
+
end
|
|
129
|
+
digest
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def no_effect_completion_plan(journal, events, record, params, admitted)
|
|
133
|
+
digest = no_effect_completion_binding!(record, params, admitted)
|
|
134
|
+
return {data: service_projection(record)} if record["no_effect_completion_digest"]
|
|
135
|
+
owner = ServiceEvidence.new(journal: journal)
|
|
136
|
+
challenge = owner.challenge!(record)
|
|
137
|
+
if record["operation"] == "prune-preserved-workspace"
|
|
138
|
+
owner.cleanup_inspection_inputs!(admitted.fetch(:artifacts), record, challenge)
|
|
139
|
+
else
|
|
140
|
+
admitted.fetch(:artifacts).each { |bytes| owner.inspection!(bytes, record, challenge) }
|
|
141
|
+
end
|
|
142
|
+
plan = Molecules::CanonicalEvidence.new(journal: journal).import_plan(**owner.context(record, no_effect: true),
|
|
143
|
+
artifacts: admitted.fetch(:artifacts), admitted_after_event_digest: challenge.fetch("digest"))
|
|
144
|
+
receipt = admitted.fetch(:receipt).merge("evidence" => plan.fetch(:references))
|
|
145
|
+
replacement = record.merge("state" => "failed-settled", "receipt" => receipt, "no_effect_completion_digest" => digest)
|
|
146
|
+
plan.merge(data: service_projection(replacement), service_updates: [{request_id: record.fetch("request_id"),
|
|
147
|
+
expected: record, replacement: replacement, event_type: "service_transition"}])
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|