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
|
@@ -12,7 +12,18 @@ module Ace
|
|
|
12
12
|
# exclusion; the immutable commit is the current CAS input, never a
|
|
13
13
|
# caller-selected alternate evidence ref.
|
|
14
14
|
def service_settlement_complete!(journal:, events:, params:, map:, commit:)
|
|
15
|
+
service_settlement_evidence!(journal: journal, events: events, params: params, map: map, commit: commit)
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def service_settlement_evidence!(journal:, events:, params:, map:, commit:)
|
|
15
20
|
protected_journal!(journal)
|
|
21
|
+
journal.verify_commit!(commit)
|
|
22
|
+
retained = journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
23
|
+
.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
24
|
+
unless retained == events && Models::EvidenceEvent.chain_valid?(events)
|
|
25
|
+
raise AttemptErrors::EvidenceUnavailable, "service settlement requires exact canonical commit"
|
|
26
|
+
end
|
|
16
27
|
references = events.select { |event| %w[service_claim service_transition].include?(event["type"]) }
|
|
17
28
|
.map { |event| event.fetch("payload").fetch("request_id") }.uniq.sort
|
|
18
29
|
records = journal.service_request_records(commit: commit).select do |record|
|
|
@@ -21,17 +32,48 @@ module Ace
|
|
|
21
32
|
unless records.map { |record| record.fetch("request_id") }.uniq.sort == references
|
|
22
33
|
raise AttemptErrors::EvidenceUnavailable, "canonical service request set is incomplete"
|
|
23
34
|
end
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
pending = false
|
|
36
|
+
projected = records.sort_by { |record| record.fetch("request_id") }.map do |record|
|
|
37
|
+
unless record["project_id"] == map.fetch("project_id") && record["mapping_id"] == params.fetch("mapping_id")
|
|
27
38
|
raise AttemptErrors::EvidenceUnavailable, "canonical service request is not settled for this scope"
|
|
28
39
|
end
|
|
29
40
|
# This existing reader verifies the accepted record digest and all
|
|
30
41
|
# imported terminal/no-effect evidence against this exact commit.
|
|
31
|
-
journal.service_request(record.fetch("request_id"), commit: commit)
|
|
42
|
+
verified = journal.service_request(record.fetch("request_id"), commit: commit)
|
|
43
|
+
unless verified == record && %w[accepted uncertain rejected succeeded failed failed-settled].include?(verified.fetch("state"))
|
|
44
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical service record differs"
|
|
45
|
+
end
|
|
46
|
+
if %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| verified.key?(key) }
|
|
47
|
+
ServiceEvidence.new(journal: journal).challenge!(verified, pending: {commit: commit}, current: false)
|
|
48
|
+
end
|
|
49
|
+
unless %w[succeeded failed-settled].include?(verified.fetch("state"))
|
|
50
|
+
pending = true
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
transition = events.reverse.find { |event| %w[service_claim service_transition].include?(event["type"]) &&
|
|
54
|
+
event.dig("payload", "request_id") == record.fetch("request_id") }
|
|
55
|
+
receipt = verified.fetch("receipt")
|
|
56
|
+
unless transition && transition["type"] == "service_transition" &&
|
|
57
|
+
transition.dig("payload", "state") == verified.fetch("state") &&
|
|
58
|
+
transition.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(verified) &&
|
|
59
|
+
transition.dig("payload", "receipt_digest") == Atoms::EvidenceDigest.digest(receipt)
|
|
60
|
+
raise AttemptErrors::EvidenceUnavailable, "accepted service terminal transition differs"
|
|
61
|
+
end
|
|
62
|
+
{"request_id" => verified.fetch("request_id"), "state" => verified.fetch("state"),
|
|
63
|
+
"event_digest" => transition.fetch("digest"), "record_digest" => transition.dig("payload", "record_digest"),
|
|
64
|
+
"receipt_digest" => transition.dig("payload", "receipt_digest"),
|
|
65
|
+
"evidence_refs" => receipt.fetch("evidence").sort_by { |ref| ref.values_at("ref", "sha256") }}
|
|
32
66
|
end
|
|
33
|
-
|
|
34
|
-
|
|
67
|
+
projected.compact!
|
|
68
|
+
unless projected.empty?
|
|
69
|
+
journal.event_commits!(assignment_id: params.fetch("assignment_id"),
|
|
70
|
+
event_digests: projected.map { |row| row.fetch("event_digest") }, commit: commit)
|
|
71
|
+
end
|
|
72
|
+
if pending
|
|
73
|
+
raise AttemptErrors::ServiceSettlementPending, "authenticated canonical service requests remain unsettled"
|
|
74
|
+
end
|
|
75
|
+
settlement_immutable({"commit" => commit, "services" => projected})
|
|
76
|
+
rescue KeyError, TypeError, ArgumentError, NoMethodError, AttemptErrors::ReceiptRejected
|
|
35
77
|
raise AttemptErrors::EvidenceUnavailable, "canonical service settlement is unverifiable"
|
|
36
78
|
end
|
|
37
79
|
|
|
@@ -39,8 +81,30 @@ module Ace
|
|
|
39
81
|
# service writers. It never reacquires lifecycle/authority locks.
|
|
40
82
|
def authorize_service_update!(journal:, existing:, replacement:, pending:)
|
|
41
83
|
protected_journal!(journal)
|
|
84
|
+
if existing && existing.key?("executor_process_binding") &&
|
|
85
|
+
Atoms::EvidenceDigest.digest(existing.fetch("executor_process_binding")) != Atoms::EvidenceDigest.digest(replacement["executor_process_binding"])
|
|
86
|
+
raise AttemptErrors::InvalidState, "original cleanup executor cannot change"
|
|
87
|
+
end
|
|
88
|
+
if existing && existing.key?("operation_owner_binding") &&
|
|
89
|
+
Atoms::EvidenceDigest.digest(existing.fetch("operation_owner_binding")) != Atoms::EvidenceDigest.digest(replacement["operation_owner_binding"])
|
|
90
|
+
raise AttemptErrors::InvalidState, "original cleanup dispatch owner cannot change"
|
|
91
|
+
end
|
|
92
|
+
if existing && %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| existing[key] != replacement[key] }
|
|
93
|
+
unless pending && pending[:operation] == "claim_service_settlement" &&
|
|
94
|
+
existing["state"] == replacement["state"] && existing["receipt"] == replacement["receipt"] &&
|
|
95
|
+
existing["dispatch_phase"] == replacement["dispatch_phase"]
|
|
96
|
+
raise AttemptErrors::InvalidState, "no-effect challenge requires its fixed source owner"
|
|
97
|
+
end
|
|
98
|
+
ServiceEvidence.new(journal: journal).challenge!(replacement, pending: pending)
|
|
99
|
+
end
|
|
42
100
|
initial = existing.nil? && %w[accepted uncertain].include?(replacement["state"])
|
|
101
|
+
if initial && (replacement.key?("operation_owner_binding") || replacement.key?("executor_process_binding"))
|
|
102
|
+
raise AttemptErrors::InvalidState, "cleanup owner identity requires fresh dispatch admission"
|
|
103
|
+
end
|
|
43
104
|
begin_dispatch = existing && existing["dispatch_phase"] == "issued" && replacement["dispatch_phase"] == "dispatch_started"
|
|
105
|
+
if existing && existing["operation"] == "publish" && pending && %w[publication_challenge publication_continue].include?(pending[:operation])
|
|
106
|
+
return authorize_publication_update!(journal: journal, existing: existing, replacement: replacement, pending: pending)
|
|
107
|
+
end
|
|
44
108
|
if existing && existing["dispatch_phase"] != replacement["dispatch_phase"] && !begin_dispatch
|
|
45
109
|
raise AttemptErrors::InvalidState, "invalid protected dispatch phase change"
|
|
46
110
|
end
|
|
@@ -65,8 +129,22 @@ module Ace
|
|
|
65
129
|
receiver = @deployment.project(map.fetch("project_id")).fetch("service_receivers").fetch(replacement.fetch("service_id"))
|
|
66
130
|
raise AttemptErrors::UnauthorizedIdentity, "protected service executor differs" unless receiver.fetch("executor_uid") == replacement.fetch("executor_uid")
|
|
67
131
|
current = exact_candidate!(candidate(events), params)
|
|
68
|
-
approved_review!(journal, events, params, map, current)
|
|
132
|
+
approved_review!(journal, events, params, map, current, service_operation: replacement.fetch("operation")) unless %w[create update].include?(replacement.fetch("operation"))
|
|
69
133
|
service_policy!.prepare!(replacement, input_bytes: bytes)
|
|
134
|
+
if begin_dispatch && %w[prune-preserved-workspace publish].include?(replacement.fetch("operation"))
|
|
135
|
+
executor = replacement.fetch("executor_process_binding")
|
|
136
|
+
boot = replacement.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
|
|
137
|
+
Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
|
|
138
|
+
credentials = @deployment.project(map.fetch("project_id")).fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
|
|
139
|
+
unless executor.slice("uid", "gid", "groups") == credentials.slice("gid", "groups").merge("uid" => receiver.fetch("executor_uid"))
|
|
140
|
+
raise AttemptErrors::UnauthorizedIdentity, "original cleanup executor credentials differ"
|
|
141
|
+
end
|
|
142
|
+
@kernel.live!(executor)
|
|
143
|
+
unless replacement["operation"] != "prune-preserved-workspace" || Atoms::EvidenceDigest.digest(service_policy!.dispatch_owner_binding!(replacement)) ==
|
|
144
|
+
Atoms::EvidenceDigest.digest(replacement.fetch("operation_owner_binding"))
|
|
145
|
+
raise AttemptErrors::UnauthorizedIdentity, "fixed cleanup owner changed before dispatch acceptance"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
70
148
|
true
|
|
71
149
|
rescue KeyError
|
|
72
150
|
raise AttemptErrors::UnauthorizedIdentity, "protected service admission context is incomplete"
|
|
@@ -74,6 +152,15 @@ module Ace
|
|
|
74
152
|
|
|
75
153
|
private
|
|
76
154
|
|
|
155
|
+
def settlement_immutable(value)
|
|
156
|
+
case value
|
|
157
|
+
when Hash then value.each_with_object({}) { |(key, item), result| result[key.dup.freeze] = settlement_immutable(item) }.freeze
|
|
158
|
+
when Array then value.map { |item| settlement_immutable(item) }.freeze
|
|
159
|
+
when String then value.dup.freeze
|
|
160
|
+
else value.freeze
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
77
164
|
def authorize_service_transfer!(request, params, map, peer, role)
|
|
78
165
|
service_policy!
|
|
79
166
|
service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid")) unless request.fetch("operation") == "complete_service"
|
|
@@ -100,20 +187,71 @@ module Ace
|
|
|
100
187
|
true
|
|
101
188
|
end
|
|
102
189
|
|
|
190
|
+
# Receiver identity and forwarded worker identity are independent checks.
|
|
191
|
+
# This immutable observation grants no service claim or dispatch permit.
|
|
192
|
+
def workspace_prune_preview_context(_request, params, map, peer, role, transfer)
|
|
193
|
+
raise ArgumentError, "preview context forbids upload" unless transfer.nil?
|
|
194
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
195
|
+
{data: workspace_prune_preview_context_at!(journal: journal, params: params, map: map, peer: peer, role: role), replayed: false}
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Only source composition supplies this fixed journal/snapshot owner.
|
|
200
|
+
# Both ordinary admission and the root read-only view use the same
|
|
201
|
+
# original registration, process and candidate proof implementation.
|
|
202
|
+
public
|
|
203
|
+
|
|
204
|
+
def workspace_prune_preview_context_at!(journal:, params:, map:, peer:, role:)
|
|
205
|
+
service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid"))
|
|
206
|
+
protected_journal!(journal)
|
|
207
|
+
commit = journal.ref_value
|
|
208
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: map)
|
|
209
|
+
origin = active_origin(events, params)
|
|
210
|
+
worker_or_launcher!(params.fetch("worker_process_binding"), :worker, map, origin)
|
|
211
|
+
receiver = service_receiver!(peer, role, map, params.fetch("service_id"))
|
|
212
|
+
service_policy!.workspace_prune_receiver!(project: map.fetch("project_id"), uid: map.fetch("worker_uid"),
|
|
213
|
+
service_id: params.fetch("service_id"), executor_uid: receiver.fetch("executor_uid"))
|
|
214
|
+
selected = candidate(events)
|
|
215
|
+
unless selected && selected["head"].is_a?(String) && selected["head"].match?(CandidateTransfer::SHA) &&
|
|
216
|
+
selected["candidate_generation"].is_a?(Integer) && selected["candidate_generation"].positive?
|
|
217
|
+
raise AttemptErrors::EvidenceUnavailable, "preview candidate is unavailable"
|
|
218
|
+
end
|
|
219
|
+
data = {
|
|
220
|
+
"journal_commit" => commit,
|
|
221
|
+
"maintenance" => params.slice("mapping_id", "assignment_id", "attempt_id").merge("project_id" => map.fetch("project_id")),
|
|
222
|
+
"head" => selected.fetch("head"), "candidate_generation" => selected.fetch("candidate_generation"),
|
|
223
|
+
"worker_process_binding" => origin.fetch("process_binding").fetch("process_identity"),
|
|
224
|
+
"caller_process_binding" => params.fetch("worker_process_binding")
|
|
225
|
+
}
|
|
226
|
+
raise AttemptErrors::Conflict, "preview canonical selection changed" unless journal.ref_value == commit
|
|
227
|
+
raise AttemptErrors::EvidenceUnavailable, "preview context exceeds bound" if JSON.generate(data).bytesize > 16_384
|
|
228
|
+
Atoms::EvidenceDigest.digest(data) # Reject unsupported transport values before copying.
|
|
229
|
+
JSON.parse(JSON.generate(data))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
103
234
|
def service_status(_request, params, map, peer, role)
|
|
104
235
|
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
105
236
|
protected_journal!(journal)
|
|
106
|
-
|
|
237
|
+
commit = journal.ref_value
|
|
238
|
+
read = role == :executor && journal.service_settlement_read(params.fetch("request_id"), commit: commit)
|
|
239
|
+
record = read ? read.fetch(:record) : journal.service_request(params.fetch("request_id"), commit: commit)
|
|
107
240
|
unless record && record["assignment_id"] == params["assignment_id"] && record["attempt_id"] == params["attempt_id"] &&
|
|
108
241
|
record["project_id"] == map["project_id"] && record["candidate_head"] == params["head"] &&
|
|
109
242
|
record["candidate_generation"] == params["candidate_generation"]
|
|
110
243
|
raise AttemptErrors::UnauthorizedIdentity, "service status binding differs"
|
|
111
244
|
end
|
|
112
|
-
|
|
245
|
+
if role != :executor && %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| record.key?(key) }
|
|
246
|
+
ServiceEvidence.new(journal: journal).challenge!(record, pending: {commit: commit}, current: false)
|
|
247
|
+
end
|
|
248
|
+
selected = read ? read.fetch(:read_view).inventory.fetch("events").fetch(params.fetch("assignment_id")) :
|
|
249
|
+
journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
250
|
+
events = selected.select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
|
|
113
251
|
@kernel.live!(peer)
|
|
114
252
|
service_policy!.visible!(project: map.fetch("project_id"), uid: peer.fetch("uid"))
|
|
115
253
|
if role == :executor
|
|
116
|
-
|
|
254
|
+
record = service_settlement_record!(journal, params, map, peer, role, record: record)
|
|
117
255
|
else
|
|
118
256
|
authorized = case role
|
|
119
257
|
when :worker
|
|
@@ -129,8 +267,19 @@ module Ace
|
|
|
129
267
|
end
|
|
130
268
|
raise AttemptErrors::UnauthorizedIdentity, "service status purpose differs" unless authorized
|
|
131
269
|
end
|
|
132
|
-
|
|
133
|
-
"journal_commit" =>
|
|
270
|
+
data = service_projection(record).merge("generation" => journal.authority_generation(events),
|
|
271
|
+
"journal_commit" => commit)
|
|
272
|
+
data["input_digest"] = record.fetch("input_digest") if %w[create update ready merge].include?(record["operation"])
|
|
273
|
+
if %w[create update ready merge].include?(record["operation"]) && record["state"] == "succeeded"
|
|
274
|
+
event = ServiceDeliveryEvidence.new(journal: journal).verified(record: record, events: events, commit: commit)
|
|
275
|
+
data.merge!(record.slice("input_digest", "authorization", "executor_uid", "completion_digest", "receipt"))
|
|
276
|
+
data["delivery_event"] = event
|
|
277
|
+
end
|
|
278
|
+
if role == :executor
|
|
279
|
+
data["settlement_context"] = read.fetch(:settlement_context) ||
|
|
280
|
+
ServiceEvidence.new(journal: journal).settlement_context(record, commit: commit, read_view: read.fetch(:read_view))
|
|
281
|
+
end
|
|
282
|
+
{data: data, replayed: false}
|
|
134
283
|
end
|
|
135
284
|
end
|
|
136
285
|
|
|
@@ -158,7 +307,7 @@ module Ace
|
|
|
158
307
|
raise AttemptErrors::Conflict, "authorization candidate changed"
|
|
159
308
|
end
|
|
160
309
|
service_receiver!(peer, role, map, record.fetch("service_id"))
|
|
161
|
-
approved_review!(journal, events, params, map, current)
|
|
310
|
+
approved_review!(journal, events, params, map, current, service_operation: record.fetch("operation")) unless %w[create update].include?(record.fetch("operation"))
|
|
162
311
|
prepared = service_policy!.prepare!(record, input_bytes: bytes)
|
|
163
312
|
data = params.slice("request_id", "claim_binding", "head", "candidate_generation").merge(
|
|
164
313
|
"policy_digest" => prepared.fetch(:policy_digest), "operation_digest" => prepared.fetch(:operation_digest))
|
|
@@ -183,7 +332,7 @@ module Ace
|
|
|
183
332
|
# Only body binding is checked here; current effect policy stays
|
|
184
333
|
# inside each fresh CAS callback.
|
|
185
334
|
service_policy!.input_binding(admitted, expected_digest: input_record.fetch("input_digest"),
|
|
186
|
-
expected_target: input_record.fetch("target"))
|
|
335
|
+
expected_target: input_record.fetch("target"), operation: input_record.fetch("operation"))
|
|
187
336
|
end
|
|
188
337
|
# Admission also runs outside mutate: an exact mutation replay
|
|
189
338
|
# skips its callback and cannot bypass the authenticated ticket.
|
|
@@ -234,10 +383,40 @@ module Ace
|
|
|
234
383
|
data["claim"] = retained || result.fetch(:replayed) || result.dig(:data, "claim") == "retained" ? "retained" : "created"
|
|
235
384
|
result = result.merge(data: data)
|
|
236
385
|
end
|
|
386
|
+
if %w[request_service begin_dispatch].include?(request.fetch("operation")) &&
|
|
387
|
+
%w[prune-preserved-workspace publish].include?(result.fetch(:data).fetch("operation"))
|
|
388
|
+
selector = request.fetch("operation") == "request_service" ? "request_event_digest" : "dispatch_event_digest"
|
|
389
|
+
digest = cleanup_service_mutation_event!(journal, request, params, result.fetch(:data).fetch("journal_commit"))
|
|
390
|
+
result = result.merge(data: result.fetch(:data).merge(selector => digest))
|
|
391
|
+
end
|
|
237
392
|
result
|
|
238
393
|
end
|
|
239
394
|
end
|
|
240
395
|
|
|
396
|
+
# Selectors refer to the actual accepted mutation, never a caller's
|
|
397
|
+
# pathname or the latest service record. They are derived after CAS,
|
|
398
|
+
# avoiding a self-reference in the persisted mutation reply.
|
|
399
|
+
def cleanup_service_mutation_event!(journal, request, params, commit)
|
|
400
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
401
|
+
.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
402
|
+
unless Models::EvidenceEvent.chain_valid?(events)
|
|
403
|
+
raise AttemptErrors::EvidenceUnavailable, "cleanup service mutation chain is unavailable"
|
|
404
|
+
end
|
|
405
|
+
selected = events.select do |event|
|
|
406
|
+
event["type"] == "authority_mutation" && event.dig("payload", "mutation_id") == request.fetch("mutation_id")
|
|
407
|
+
end
|
|
408
|
+
event = selected.one? && selected.first
|
|
409
|
+
unless event && event.dig("payload", "operation") == request.fetch("operation") &&
|
|
410
|
+
event.dig("payload", "parameters_digest") == Atoms::EvidenceDigest.digest(params) &&
|
|
411
|
+
event.dig("payload", "assignment_id") == params.fetch("assignment_id") &&
|
|
412
|
+
event.dig("payload", "attempt_id") == params.fetch("attempt_id") &&
|
|
413
|
+
event.dig("payload", "data", "request_id") == params.fetch("request_id") &&
|
|
414
|
+
journal.event_commit!(assignment_id: params.fetch("assignment_id"), event_digest: event.fetch("digest"), commit: commit) == commit
|
|
415
|
+
raise AttemptErrors::EvidenceUnavailable, "cleanup service mutation introduction is unavailable"
|
|
416
|
+
end
|
|
417
|
+
event.fetch("digest")
|
|
418
|
+
end
|
|
419
|
+
|
|
241
420
|
def service_policy!
|
|
242
421
|
@service_policy || raise(ArgumentError, "full-service authority requires its installed policy owner")
|
|
243
422
|
end
|
|
@@ -246,6 +425,9 @@ module Ace
|
|
|
246
425
|
unless record && role == :executor && peer["uid"] == record.fetch("executor_uid")
|
|
247
426
|
raise AttemptErrors::UnauthorizedIdentity, "completion requires the recorded executor"
|
|
248
427
|
end
|
|
428
|
+
if record["operation"] == "publish" && record["dispatch_phase"] != "issued" && peer != record["executor_process_binding"]
|
|
429
|
+
raise AttemptErrors::UnauthorizedIdentity, "publication requires its original receiver birth"
|
|
430
|
+
end
|
|
249
431
|
@kernel.live!(peer)
|
|
250
432
|
true
|
|
251
433
|
end
|
|
@@ -269,7 +451,7 @@ module Ace
|
|
|
269
451
|
worker = params.fetch("worker_process_binding")
|
|
270
452
|
worker_or_launcher!(worker, :worker, map, origin)
|
|
271
453
|
current = exact_candidate!(candidate(events), params)
|
|
272
|
-
approved_review!(journal, events, params, map, current)
|
|
454
|
+
approved_review!(journal, events, params, map, current, service_operation: params.fetch("operation")) unless %w[create update].include?(params.fetch("operation"))
|
|
273
455
|
receiver = service_receiver!(peer, role, map, params.fetch("service_id"))
|
|
274
456
|
binding = params.slice("request_id", "assignment_id", "attempt_id", "operation", "input_digest", "target",
|
|
275
457
|
"authorization", "service_id").merge("project_id" => map.fetch("project_id"),
|
|
@@ -315,9 +497,15 @@ module Ace
|
|
|
315
497
|
unless record["candidate_head"] == current["head"] && record["candidate_generation"] == current["candidate_generation"]
|
|
316
498
|
raise AttemptErrors::Conflict, "dispatch candidate changed"
|
|
317
499
|
end
|
|
318
|
-
approved_review!(journal, events, params, map, current)
|
|
500
|
+
approved_review!(journal, events, params, map, current, service_operation: record.fetch("operation")) unless %w[create update].include?(record.fetch("operation"))
|
|
319
501
|
service_policy!.prepare!(record, input_bytes: input_bytes)
|
|
320
502
|
replacement = record.merge("dispatch_phase" => "dispatch_started")
|
|
503
|
+
if record.fetch("operation") == "prune-preserved-workspace"
|
|
504
|
+
replacement["operation_owner_binding"] = service_policy!.dispatch_owner_binding!(record)
|
|
505
|
+
end
|
|
506
|
+
if %w[prune-preserved-workspace publish].include?(record.fetch("operation"))
|
|
507
|
+
replacement["executor_process_binding"] = JSON.parse(JSON.generate(peer))
|
|
508
|
+
end
|
|
321
509
|
{data: service_projection(replacement).merge("invocation" => "permitted"),
|
|
322
510
|
service_inputs: {record.fetch("request_id") => input_bytes},
|
|
323
511
|
service_updates: [{request_id: record.fetch("request_id"), expected: record,
|
|
@@ -355,6 +543,12 @@ module Ace
|
|
|
355
543
|
return {data: service_projection(record)}
|
|
356
544
|
end
|
|
357
545
|
receipt = admitted.fetch(:receipt)
|
|
546
|
+
if record["operation"] == "publish"
|
|
547
|
+
unless admitted.fetch(:artifacts).size == 1
|
|
548
|
+
raise AttemptErrors::ReceiptRejected, "publication requires its one exact result"
|
|
549
|
+
end
|
|
550
|
+
ServiceEvidence.new(journal: journal).publication_result!(admitted.fetch(:artifacts).first, record, state: receipt.fetch("outcome"))
|
|
551
|
+
end
|
|
358
552
|
owner = ServiceEvidence.new(journal: journal)
|
|
359
553
|
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
360
554
|
plan = canonical.import_plan(**owner.context(record), artifacts: admitted.fetch(:artifacts),
|
|
@@ -362,6 +556,11 @@ module Ace
|
|
|
362
556
|
normalized = JSON.parse(JSON.generate(receipt)).merge("evidence" => plan.fetch(:references))
|
|
363
557
|
replacement = record.merge("state" => receipt.fetch("outcome"), "receipt" => normalized, "completion_digest" => digest)
|
|
364
558
|
replacement["failed_at"] = Time.now.utc.iso8601(9) if receipt["outcome"] == "failed"
|
|
559
|
+
if %w[create update ready merge].include?(replacement["operation"]) && replacement["state"] == "succeeded"
|
|
560
|
+
event = ServiceDeliveryEvidence.new(journal: journal).completion_event(record: replacement,
|
|
561
|
+
receipt: normalized, completion_digest: digest, artifacts: admitted.fetch(:artifacts))
|
|
562
|
+
plan = plan.merge(events: plan.fetch(:events) + [event])
|
|
563
|
+
end
|
|
365
564
|
plan.merge(data: service_projection(replacement), service_updates: [{request_id: record.fetch("request_id"),
|
|
366
565
|
expected: record, replacement: replacement, event_type: "service_transition"}])
|
|
367
566
|
end
|
|
@@ -379,7 +578,7 @@ module Ace
|
|
|
379
578
|
unless receipt.keys.sort == Molecules::EvidenceJournal::TERMINAL_RECEIPT_FIELDS.sort &&
|
|
380
579
|
Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS.all? { |field| receipt[field] == record[field] } &&
|
|
381
580
|
%w[succeeded failed].include?(receipt["outcome"]) &&
|
|
382
|
-
(receipt["outcome"] != "succeeded" || record["dispatch_phase"] == "dispatch_started")
|
|
581
|
+
(receipt["outcome"] != "succeeded" || record["dispatch_phase"] == "dispatch_started" || (record["operation"] == "publish" && record["dispatch_phase"] == "issuing"))
|
|
383
582
|
raise AttemptErrors::ReceiptRejected, "completion must attest the exact dispatched request"
|
|
384
583
|
end
|
|
385
584
|
digest
|
|
@@ -387,7 +586,11 @@ module Ace
|
|
|
387
586
|
|
|
388
587
|
def service_projection(record)
|
|
389
588
|
record.slice("request_id", "assignment_id", "attempt_id", "project_id", "operation", "service_id", "target",
|
|
390
|
-
"candidate_head", "candidate_generation", "state", "dispatch_ticket_id", "claim_binding", "claim_generation", "dispatch_phase", "policy_digest")
|
|
589
|
+
"candidate_head", "candidate_generation", "state", "dispatch_ticket_id", "claim_binding", "claim_generation", "dispatch_phase", "policy_digest", "operation_owner_binding", "executor_process_binding", *ServicePublicationEvidence::PUBLICATION_FIELDS).tap do |projection|
|
|
590
|
+
if record["operation"] == "publish" && %w[succeeded failed].include?(record["state"])
|
|
591
|
+
projection.merge!(record.slice("receipt", "completion_digest", "authorization", "input_digest"))
|
|
592
|
+
end
|
|
593
|
+
end
|
|
391
594
|
end
|
|
392
595
|
end
|
|
393
596
|
end
|