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,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "deployment"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Authority
|
|
8
|
+
# Immutable installer configuration, indexed by canonical content identity.
|
|
9
|
+
# This object is never a transport selector or an execution ledger.
|
|
10
|
+
class DeploymentHistory
|
|
11
|
+
PATH = "/etc/ace/assignment-deployment-history.json"
|
|
12
|
+
attr_reader :original, :candidate, :artifact_reference
|
|
13
|
+
|
|
14
|
+
def self.load = load_protected(nil)
|
|
15
|
+
|
|
16
|
+
def self.load_artifact(reference)
|
|
17
|
+
validate_reference!(reference)
|
|
18
|
+
load_protected(reference)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.load_protected(reference)
|
|
22
|
+
Ace::Runtime::Molecules::ProtectedArtifactSet.new.with do |artifacts|
|
|
23
|
+
bytes = if reference
|
|
24
|
+
artifacts.read!(reference)
|
|
25
|
+
else
|
|
26
|
+
held, selected = artifacts.read_path!(PATH, limit: 65_536)
|
|
27
|
+
reference = selected
|
|
28
|
+
held
|
|
29
|
+
end
|
|
30
|
+
raw = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
31
|
+
raise ArgumentError, "history manifest is not UTF-8" unless raw.valid_encoding?
|
|
32
|
+
value = JSON.parse(raw, create_additions: false, max_nesting: 32,
|
|
33
|
+
allow_duplicate_key: false, allow_comments: false)
|
|
34
|
+
unless value.is_a?(Hash) && value.keys.sort ==
|
|
35
|
+
%w[candidate_descriptor descriptors original_descriptor public_keys schema] &&
|
|
36
|
+
value["schema"] == "ace.assign.deployment-history/v1"
|
|
37
|
+
raise ArgumentError, "invalid deployment history manifest"
|
|
38
|
+
end
|
|
39
|
+
descriptors = value.fetch("descriptors")
|
|
40
|
+
keys = value.fetch("public_keys")
|
|
41
|
+
unless descriptors.is_a?(Array) && descriptors.size.between?(1, 128) &&
|
|
42
|
+
keys.is_a?(Array) && keys.size <= 128 && descriptors.size + keys.size <= 255
|
|
43
|
+
raise ArgumentError, "deployment history inventory exceeds bounds"
|
|
44
|
+
end
|
|
45
|
+
index = {}
|
|
46
|
+
descriptors.each do |selected|
|
|
47
|
+
validate_reference!(selected)
|
|
48
|
+
digest = selected.fetch("sha256")
|
|
49
|
+
raise ArgumentError, "duplicate historical descriptor selector" if index.key?(digest)
|
|
50
|
+
index[digest] = Deployment.send(:from_verified_bytes, artifacts.read!(selected), immutable(selected))
|
|
51
|
+
end
|
|
52
|
+
selections = %w[original_descriptor candidate_descriptor].map do |name|
|
|
53
|
+
selected = value.fetch(name)
|
|
54
|
+
validate_reference!(selected)
|
|
55
|
+
descriptor = index.fetch(selected.fetch("sha256")) do
|
|
56
|
+
raise ArgumentError, "transaction descriptor is not retained"
|
|
57
|
+
end
|
|
58
|
+
unless descriptor.artifact_reference == selected
|
|
59
|
+
raise ArgumentError, "transaction descriptor reference conflicts"
|
|
60
|
+
end
|
|
61
|
+
descriptor
|
|
62
|
+
end
|
|
63
|
+
public_keys = {}
|
|
64
|
+
keys.each do |entry|
|
|
65
|
+
unless entry.is_a?(Hash) && entry.keys.sort == %w[public_key_sha256 ref] &&
|
|
66
|
+
digest?(entry["public_key_sha256"])
|
|
67
|
+
raise ArgumentError, "invalid historical public key entry"
|
|
68
|
+
end
|
|
69
|
+
validate_reference!(entry.fetch("ref"))
|
|
70
|
+
digest = entry.fetch("public_key_sha256")
|
|
71
|
+
raise ArgumentError, "duplicate historical public key selector" if public_keys.key?(digest)
|
|
72
|
+
key = OpenSSL::PKey.read(artifacts.read!(entry.fetch("ref")))
|
|
73
|
+
unless key.is_a?(OpenSSL::PKey::RSA) && !key.private? &&
|
|
74
|
+
Digest::SHA256.hexdigest(key.public_key.to_der) == digest
|
|
75
|
+
raise ArgumentError, "historical public key identity differs"
|
|
76
|
+
end
|
|
77
|
+
public_keys[digest] = key.freeze
|
|
78
|
+
end
|
|
79
|
+
selections.first.maintenance_inventory(selections.last)
|
|
80
|
+
history = new(selections, index, public_keys, immutable(reference))
|
|
81
|
+
artifacts.verify_unchanged!
|
|
82
|
+
history.freeze
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def initialize(selections, descriptors, keys, reference)
|
|
87
|
+
@original, @candidate = selections
|
|
88
|
+
@descriptors, @keys = descriptors.freeze, keys.freeze
|
|
89
|
+
@artifact_reference = reference
|
|
90
|
+
end
|
|
91
|
+
private_class_method :new
|
|
92
|
+
|
|
93
|
+
def descriptor!(sha256:)
|
|
94
|
+
@descriptors.fetch(sha256) do
|
|
95
|
+
raise AttemptErrors::EvidenceUnavailable, "original protected descriptor is not retained"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def public_key!(sha256:)
|
|
100
|
+
@keys.fetch(sha256) do
|
|
101
|
+
raise AttemptErrors::EvidenceUnavailable, "original protected public key is not retained"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def descriptors = @descriptors.values.freeze
|
|
106
|
+
|
|
107
|
+
# Publication moves verified bytes to the fixed current descriptor path.
|
|
108
|
+
# Retention paths stay immutable; canonical content, not pathname, joins
|
|
109
|
+
# the installed selection to that exact independently verified artifact.
|
|
110
|
+
def selects?(deployment, selection: nil)
|
|
111
|
+
return false unless deployment.is_a?(Deployment) && deployment.frozen? && deployment.artifact_reference
|
|
112
|
+
choices = case selection
|
|
113
|
+
when :original then [original]
|
|
114
|
+
when :candidate then [candidate]
|
|
115
|
+
when nil then [original, candidate]
|
|
116
|
+
else raise ArgumentError, "invalid history transaction selection"
|
|
117
|
+
end
|
|
118
|
+
choices.any? do |selected|
|
|
119
|
+
selected.artifact_reference.values_at("sha256", "bytes") == deployment.artifact_reference.values_at("sha256", "bytes")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.digest?(value) = value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/)
|
|
124
|
+
|
|
125
|
+
def self.validate_reference!(value)
|
|
126
|
+
unless value.is_a?(Hash) && value.keys.sort == %w[bytes path sha256] &&
|
|
127
|
+
value["path"].is_a?(String) && value["path"].start_with?("/") &&
|
|
128
|
+
!value["path"].include?("\0") && File.expand_path(value["path"]) == value["path"] &&
|
|
129
|
+
digest?(value["sha256"]) && value["bytes"].is_a?(Integer) && value["bytes"].between?(1, 65_536)
|
|
130
|
+
raise ArgumentError, "invalid protected history artifact reference"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.immutable(value)
|
|
135
|
+
case value
|
|
136
|
+
when Hash then value.to_h { |key, item| [key.dup.freeze, immutable(item)] }.freeze
|
|
137
|
+
when Array then value.map { |item| immutable(item) }.freeze
|
|
138
|
+
when String then value.dup.freeze
|
|
139
|
+
else value.freeze
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
private_class_method :digest?, :validate_reference!, :immutable, :load_protected
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require_relative "candidate_transfer"
|
|
7
|
+
require_relative "receipt_transfer"
|
|
8
|
+
require_relative "service_evidence"
|
|
9
|
+
require_relative "service_delivery_evidence"
|
|
10
|
+
require_relative "../molecules/canonical_evidence"
|
|
11
|
+
|
|
12
|
+
module Ace
|
|
13
|
+
module Assign
|
|
14
|
+
module Authority
|
|
15
|
+
# Business admission shares the launch origin, lifecycle exclusion and
|
|
16
|
+
# journal CAS. Network transfer is completed outside those locks.
|
|
17
|
+
class Endcap
|
|
18
|
+
OPERATIONS = %w[submit_candidate export_candidate request_review review_status assign_review cancel_review accept_review request_service begin_dispatch complete_service publication_challenge publication_continue claim_service_settlement complete_no_effect service_authorization service_status workspace_prune_preview_context submit_result evidence_fetch bind_inbox finish recover campaign_record_round campaign_export_result].freeze
|
|
19
|
+
TRANSFER_OPERATIONS = {
|
|
20
|
+
"campaign_export_result" => {direction: :download, purpose: :artifacts, roles: %i[worker launcher supervisor]},
|
|
21
|
+
"campaign_record_round" => {direction: :upload, purpose: :receipt_artifacts, roles: %i[launcher supervisor]},
|
|
22
|
+
"submit_result" => {direction: :upload, purpose: :receipt_artifacts, roles: [:worker]},
|
|
23
|
+
"evidence_fetch" => {direction: :download, purpose: :artifacts, roles: %i[worker reviewer executor launcher supervisor signer]},
|
|
24
|
+
"submit_candidate" => {direction: :upload, purpose: :candidate, roles: %i[worker launcher]},
|
|
25
|
+
"export_candidate" => {direction: :download, purpose: :candidate, roles: %i[reviewer executor]},
|
|
26
|
+
"accept_review" => {direction: :upload, purpose: :receipt_artifacts, roles: [:reviewer]},
|
|
27
|
+
"request_service" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
28
|
+
"begin_dispatch" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
29
|
+
"service_authorization" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
30
|
+
"complete_service" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]},
|
|
31
|
+
"publication_challenge" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]},
|
|
32
|
+
"complete_no_effect" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]}
|
|
33
|
+
}.freeze
|
|
34
|
+
PARAMETERS = {
|
|
35
|
+
"campaign_export_result" => %w[mapping_id assignment_id attempt_id candidate_generation head],
|
|
36
|
+
"campaign_record_round" => %w[mapping_id assignment_id attempt_id candidate_generation head input_sha256 transfer],
|
|
37
|
+
"submit_result" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head receipt_sha256 transfer],
|
|
38
|
+
"evidence_fetch" => %w[mapping_id assignment_id attempt_id kind purpose_id artifact_id],
|
|
39
|
+
"submit_candidate" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head transfer],
|
|
40
|
+
"export_candidate" => %w[mapping_id assignment_id attempt_id candidate_generation head purpose_id],
|
|
41
|
+
"request_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head],
|
|
42
|
+
"assign_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head reviewer_uid reviewer_process_binding],
|
|
43
|
+
"cancel_review" => %w[mapping_id assignment_id attempt_id head candidate_generation review_event_id expected_generation],
|
|
44
|
+
"accept_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head purpose_id receipt_sha256 transfer],
|
|
45
|
+
"request_service" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head request_id operation input_digest target authorization service_id worker_process_binding transfer],
|
|
46
|
+
"begin_dispatch" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head request_id claim_binding transfer],
|
|
47
|
+
"workspace_prune_preview_context" => %w[mapping_id assignment_id attempt_id service_id worker_process_binding],
|
|
48
|
+
"service_status" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id],
|
|
49
|
+
"service_authorization" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest transfer],
|
|
50
|
+
"complete_service" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding receipt_sha256 transfer],
|
|
51
|
+
"claim_service_settlement" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id expected_generation],
|
|
52
|
+
"publication_challenge" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest expected_generation receipt_sha256 transfer],
|
|
53
|
+
"publication_continue" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest expected_generation challenge_digest],
|
|
54
|
+
"complete_no_effect" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding reconciliation_challenge receipt_sha256 transfer]
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
def initialize(deployment:, launch:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, service_policy: nil, inbox_context_clients: nil, deployment_history: nil)
|
|
58
|
+
@deployment, @launch, @kernel, @service_policy = deployment, launch, kernel, service_policy
|
|
59
|
+
@inbox_context_clients = inbox_context_clients
|
|
60
|
+
@deployment_history = deployment_history
|
|
61
|
+
launch.attach_result_owner(self) if launch.respond_to?(:attach_result_owner)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Internal registration consumer only: caller holds the original parent
|
|
65
|
+
# lifecycle exclusion. Read one authenticated prefix; never accept a
|
|
66
|
+
# child's revisions as evidence of the parent's current candidate.
|
|
67
|
+
def campaign_parent_candidate!(journal:, commit:, params:, map:)
|
|
68
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: map)
|
|
69
|
+
original = active_origin(events, params)
|
|
70
|
+
current = candidate(events)
|
|
71
|
+
unless current && original.fetch("scope") == params.fetch("scope") &&
|
|
72
|
+
original.fetch("base_head") == params.fetch("base") &&
|
|
73
|
+
current.fetch("head") == params.fetch("head") &&
|
|
74
|
+
current.fetch("candidate_generation") == params.fetch("candidate_generation") && journal.ref_value == commit
|
|
75
|
+
raise AttemptErrors::Conflict, "campaign parent candidate changed"
|
|
76
|
+
end
|
|
77
|
+
projection = params.slice("assignment_id", "attempt_id").merge(
|
|
78
|
+
"scope" => original.fetch("scope"), "base" => original.fetch("base_head"),
|
|
79
|
+
"head" => current.fetch("head"), "candidate_generation" => current.fetch("candidate_generation"))
|
|
80
|
+
projection.transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze
|
|
81
|
+
rescue KeyError, TypeError
|
|
82
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent candidate is unavailable"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def authorize_transfer!(request:, peer:, role:)
|
|
86
|
+
return dispatch_campaign_export(request: request, peer: peer, role: role, body: false) if request.fetch("operation") == "campaign_export_result"
|
|
87
|
+
return authorize_campaign_round!(request: request, peer: peer, role: role) if request.fetch("operation") == "campaign_record_round"
|
|
88
|
+
return dispatch_prepared_fetch(request: request, peer: peer, role: role, body: false) if prepared_fetch?(request)
|
|
89
|
+
return authorize_result_transfer!(request: request, peer: peer, role: role) if %w[submit_result evidence_fetch].include?(request.fetch("operation"))
|
|
90
|
+
params, map = validate_request(request)
|
|
91
|
+
return authorize_service_settlement!(request, params, map, peer, role) if request.fetch("operation") == "complete_no_effect"
|
|
92
|
+
return authorize_publication_transfer!(request, params, map, peer, role) if request.fetch("operation") == "publication_challenge"
|
|
93
|
+
return authorize_service_transfer!(request, params, map, peer, role) if %w[request_service begin_dispatch complete_service service_authorization].include?(request.fetch("operation"))
|
|
94
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
95
|
+
protected_journal!(journal)
|
|
96
|
+
events = attempt_events(journal, params)
|
|
97
|
+
origin = active_origin(events, params)
|
|
98
|
+
if request.fetch("operation") == "submit_candidate"
|
|
99
|
+
worker_or_launcher!(peer, role, map, origin)
|
|
100
|
+
candidate_generation!(candidate(events), params) unless candidate_submission_replay!(journal, events, request, params)
|
|
101
|
+
else
|
|
102
|
+
export_admission!(journal, events, params, map, origin, peer, role)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def dispatch(request:, peer:, role:, transfer: nil)
|
|
109
|
+
return dispatch_campaign_export(request: request, peer: peer, role: role) if request.fetch("operation") == "campaign_export_result"
|
|
110
|
+
return dispatch_campaign_round(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "campaign_record_round"
|
|
111
|
+
if prepared_fetch?(request)
|
|
112
|
+
raise ArgumentError, "prepared fetch forbids upload" unless transfer.nil?
|
|
113
|
+
return dispatch_prepared_fetch(request: request, peer: peer, role: role)
|
|
114
|
+
end
|
|
115
|
+
return dispatch_bind_inbox(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "bind_inbox"
|
|
116
|
+
return dispatch_finish(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "finish"
|
|
117
|
+
return dispatch_recover(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "recover"
|
|
118
|
+
return dispatch_result(request: request, peer: peer, role: role, transfer: transfer) if %w[submit_result evidence_fetch].include?(request.fetch("operation"))
|
|
119
|
+
return review_status(request, peer, role, transfer) if request.fetch("operation") == "review_status"
|
|
120
|
+
params, map = validate_request(request)
|
|
121
|
+
return request_review(request, params, map, peer, role, transfer) if request.fetch("operation") == "request_review"
|
|
122
|
+
return cancel_review(request, params, map, peer, role, transfer) if request.fetch("operation") == "cancel_review"
|
|
123
|
+
return dispatch_publication(request, params, map, peer, role, transfer) if %w[publication_challenge publication_continue].include?(request.fetch("operation"))
|
|
124
|
+
return dispatch_service_settlement(request, params, map, peer, role, transfer) if %w[claim_service_settlement complete_no_effect].include?(request.fetch("operation"))
|
|
125
|
+
return workspace_prune_preview_context(request, params, map, peer, role, transfer) if request.fetch("operation") == "workspace_prune_preview_context"
|
|
126
|
+
return service_status(request, params, map, peer, role) if request.fetch("operation") == "service_status"
|
|
127
|
+
return dispatch_service(request, params, map, peer, role, transfer) if %w[request_service begin_dispatch complete_service service_authorization].include?(request.fetch("operation"))
|
|
128
|
+
admitted = nil
|
|
129
|
+
if request.fetch("operation") == "submit_candidate"
|
|
130
|
+
authorize_transfer!(request: request, peer: peer, role: role)
|
|
131
|
+
raise ArgumentError, "candidate transfer is missing" unless transfer && transfer.count == 1
|
|
132
|
+
bytes = transfer.bytes
|
|
133
|
+
descriptor = params.fetch("transfer")
|
|
134
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
135
|
+
admitted = CandidateTransfer.new(root: project.fetch("candidate_root")).admit(
|
|
136
|
+
bytes: bytes, sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), head: params.fetch("head"))
|
|
137
|
+
elsif request.fetch("operation") == "accept_review"
|
|
138
|
+
authorize_transfer!(request: request, peer: peer, role: role)
|
|
139
|
+
admitted = ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
|
|
140
|
+
artifact_field: "artifacts", reference_key: "path")
|
|
141
|
+
end
|
|
142
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
143
|
+
protected_journal!(journal)
|
|
144
|
+
events = attempt_events(journal, params)
|
|
145
|
+
origin = active_origin(events, params)
|
|
146
|
+
if request.fetch("operation") == "export_candidate"
|
|
147
|
+
current = export_admission!(journal, events, params, map, origin, peer, role)
|
|
148
|
+
bytes = journal.blob(current.fetch("bundle_ref"))
|
|
149
|
+
unless bytes.bytesize == current.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == current.fetch("sha256")
|
|
150
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical candidate bundle differs"
|
|
151
|
+
end
|
|
152
|
+
return {data: current.slice("head", "tree", "candidate_generation", "sha256", "bytes"),
|
|
153
|
+
replayed: false, transfer_parts: [bytes]}
|
|
154
|
+
end
|
|
155
|
+
# Journal replay skips its callback. Authenticate the caller here
|
|
156
|
+
# as well as on every fresh/CAS-retried mutation below.
|
|
157
|
+
if request.fetch("operation") == "accept_review"
|
|
158
|
+
export_admission!(journal, events, params, map, origin, peer, role)
|
|
159
|
+
else
|
|
160
|
+
worker_or_launcher!(peer, role, map, origin,
|
|
161
|
+
launcher_only: request.fetch("operation") == "assign_review")
|
|
162
|
+
end
|
|
163
|
+
delegated_review_request!(journal, events, request, params, peer, role) if request.fetch("operation") == "assign_review"
|
|
164
|
+
candidate_submission_replay!(journal, events, request, params, admitted: admitted) if request.fetch("operation") == "submit_candidate"
|
|
165
|
+
result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
166
|
+
mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
|
|
167
|
+
parameters_digest: Atoms::EvidenceDigest.digest(params), expected_generation: params.fetch("expected_generation"),
|
|
168
|
+
with_replay: true) do |events, _commit, _generation|
|
|
169
|
+
origin = active_origin(events, params)
|
|
170
|
+
case request.fetch("operation")
|
|
171
|
+
when "submit_candidate"
|
|
172
|
+
worker_or_launcher!(peer, role, map, origin)
|
|
173
|
+
current = candidate(events)
|
|
174
|
+
candidate_generation!(current, params)
|
|
175
|
+
path = "candidates/bundles/#{SecureRandom.hex(16)}"
|
|
176
|
+
data = admitted.except("bundle").merge("bundle_ref" => path,
|
|
177
|
+
"candidate_generation" => params.fetch("candidate_generation") + 1,
|
|
178
|
+
"worker_uid" => map.fetch("worker_uid"))
|
|
179
|
+
{data: data, blobs: {path => admitted.fetch("bundle")}}
|
|
180
|
+
when "assign_review"
|
|
181
|
+
worker_or_launcher!(peer, role, map, origin, launcher_only: true)
|
|
182
|
+
current = exact_candidate!(candidate(events), params)
|
|
183
|
+
delegated = delegated_review_request!(journal, events, request, params, peer, role)
|
|
184
|
+
reservation = active_review_reservation(events, current)
|
|
185
|
+
if delegated
|
|
186
|
+
unless reservation && reservation.fetch("digest") == delegated.fetch("digest") && !active_review_event(events, current)
|
|
187
|
+
raise AttemptErrors::Conflict, "delegated review reservation is no longer assignable"
|
|
188
|
+
end
|
|
189
|
+
elsif reservation
|
|
190
|
+
raise AttemptErrors::Conflict, "candidate review reservation is active"
|
|
191
|
+
end
|
|
192
|
+
reviewer = params.fetch("reviewer_process_binding")
|
|
193
|
+
uid = params.fetch("reviewer_uid")
|
|
194
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
195
|
+
unless uid.is_a?(Integer) && project.fetch("reviewer_uids").include?(uid) && uid != map.fetch("worker_uid") &&
|
|
196
|
+
reviewer.is_a?(Hash) && reviewer["uid"] == uid
|
|
197
|
+
raise AttemptErrors::UnauthorizedIdentity, "reviewer must be independently mapped"
|
|
198
|
+
end
|
|
199
|
+
@kernel.live!(reviewer)
|
|
200
|
+
{data: current.slice("head", "candidate_generation").merge(
|
|
201
|
+
"review_id" => SecureRandom.hex(16), "reviewer_uid" => uid, "reviewer_actor" => "uid-#{uid}",
|
|
202
|
+
"reviewer_process_binding" => reviewer)}
|
|
203
|
+
when "accept_review"
|
|
204
|
+
current = export_admission!(journal, events, params, map, origin, peer, role)
|
|
205
|
+
review = assigned_review(events)
|
|
206
|
+
accept_review_plan(journal, events, params, map, current, review, admitted)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
if request.fetch("operation") == "assign_review"
|
|
210
|
+
review_event_reply(journal, result, request, params, "assignment_event_id")
|
|
211
|
+
else
|
|
212
|
+
result
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def protected_journal!(journal)
|
|
220
|
+
unless journal.evidence_mode == :protected
|
|
221
|
+
raise ArgumentError, "full-service Endcap requires canonical protected evidence composition"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def validate_request(request)
|
|
226
|
+
operation = request.fetch("operation")
|
|
227
|
+
params = request.fetch("params")
|
|
228
|
+
unless params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch(operation).sort
|
|
229
|
+
raise ArgumentError, "invalid Endcap parameters"
|
|
230
|
+
end
|
|
231
|
+
%w[mapping_id assignment_id attempt_id].each do |key|
|
|
232
|
+
raise ArgumentError, "invalid Endcap identifier" unless params[key].is_a?(String) && params[key].match?(Molecules::JournalMutation::ID)
|
|
233
|
+
end
|
|
234
|
+
unless operation == "workspace_prune_preview_context" || (params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
235
|
+
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"] >= 0)
|
|
236
|
+
raise ArgumentError, "invalid candidate binding"
|
|
237
|
+
end
|
|
238
|
+
if %w[service_authorization service_status workspace_prune_preview_context].include?(operation) && !request.fetch("mutation_id").nil?
|
|
239
|
+
raise ArgumentError, "authorization read must not supply a mutation ID"
|
|
240
|
+
end
|
|
241
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
242
|
+
raise AttemptErrors::UnauthorizedIdentity, "project mapping differs" unless map.fetch("project_id") == request.fetch("project_id")
|
|
243
|
+
[params, map]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def attempt_events(journal, params)
|
|
247
|
+
journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def active_origin(events, params)
|
|
251
|
+
unless Models::EvidenceEvent.chain_valid?(events) &&
|
|
252
|
+
!events.any? { |event| %w[receipt_accepted transition reconciliation].include?(event["type"]) &&
|
|
253
|
+
%w[succeeded failed stopped].include?(event.dig("payload", "to") || event.dig("payload", "resolution") || event.dig("payload", "receipt", "verdict")) }
|
|
254
|
+
raise AttemptErrors::InvalidState, "attempt is not active"
|
|
255
|
+
end
|
|
256
|
+
origin = @launch.origin(events, assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
257
|
+
mapping_id: params.fetch("mapping_id"))
|
|
258
|
+
raise AttemptErrors::InvalidState, "exact bound launch is required" unless %w[bound issued].include?(origin["phase"])
|
|
259
|
+
@kernel.live!(origin.fetch("process_binding").fetch("process_identity"))
|
|
260
|
+
origin
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def worker_or_launcher!(peer, role, map, origin, launcher_only: false)
|
|
264
|
+
anchor = if role == :worker && !launcher_only && peer["uid"] == map.fetch("worker_uid")
|
|
265
|
+
origin.fetch("process_binding").fetch("process_identity")
|
|
266
|
+
elsif role == :launcher && peer["uid"] == map.fetch("launcher_uid")
|
|
267
|
+
origin.fetch("launcher_identity")
|
|
268
|
+
else
|
|
269
|
+
raise AttemptErrors::UnauthorizedIdentity, "operation requires owning worker or launcher"
|
|
270
|
+
end
|
|
271
|
+
unless @kernel.descendant?(peer, anchor)
|
|
272
|
+
raise AttemptErrors::UnauthorizedIdentity, "peer does not descend from exact launch binding"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def candidate(events)
|
|
277
|
+
event = events.reverse.find { |entry| entry["type"] == "authority_mutation" && entry.dig("payload", "operation") == "submit_candidate" }
|
|
278
|
+
event&.dig("payload", "data")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Header replay admission still requires the live original caller above.
|
|
282
|
+
# TransferCodec and CandidateTransfer verify the exact body before dispatch;
|
|
283
|
+
# the descriptor is part of the original immutable parameter digest.
|
|
284
|
+
def candidate_submission_replay!(journal, events, request, params, admitted: nil)
|
|
285
|
+
replay = journal.mutation_result(request.fetch("mutation_id"))
|
|
286
|
+
return false unless replay
|
|
287
|
+
unless replay["operation"] == "submit_candidate" && replay["assignment_id"] == params.fetch("assignment_id") &&
|
|
288
|
+
replay["attempt_id"] == params.fetch("attempt_id") && replay["parameters_digest"] == Atoms::EvidenceDigest.digest(params)
|
|
289
|
+
raise AttemptErrors::Conflict, "candidate replay input differs"
|
|
290
|
+
end
|
|
291
|
+
data = replay.fetch("data")
|
|
292
|
+
retained = retained_candidate(events, data.fetch("candidate_generation"))
|
|
293
|
+
unless retained == data && data.fetch("candidate_generation") == params.fetch("candidate_generation") + 1 &&
|
|
294
|
+
data.fetch("head") == params.fetch("head")
|
|
295
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay provenance differs"
|
|
296
|
+
end
|
|
297
|
+
bytes = journal.bounded_blob(data.fetch("bundle_ref"), commit: replay.fetch("journal_commit"), max_bytes: 64 * 1024 * 1024)
|
|
298
|
+
unless bytes.bytesize == data.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == data.fetch("sha256")
|
|
299
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay bundle differs"
|
|
300
|
+
end
|
|
301
|
+
if admitted && admitted.values_at("head", "tree", "sha256", "bytes") != data.values_at("head", "tree", "sha256", "bytes")
|
|
302
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay normalized bundle differs"
|
|
303
|
+
end
|
|
304
|
+
true
|
|
305
|
+
rescue KeyError, TypeError
|
|
306
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay evidence is malformed"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def candidate_generation!(current, params)
|
|
310
|
+
unless (current && current.fetch("candidate_generation") || 0) == params.fetch("candidate_generation")
|
|
311
|
+
raise AttemptErrors::Conflict, "candidate generation changed"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def exact_candidate!(current, params)
|
|
316
|
+
candidate_generation!(current, params)
|
|
317
|
+
raise AttemptErrors::Conflict, "candidate head differs" unless current && current.fetch("head") == params.fetch("head")
|
|
318
|
+
current
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def export_admission!(journal, events, params, map, _origin, peer, role)
|
|
322
|
+
current = exact_candidate!(candidate(events), params)
|
|
323
|
+
@kernel.live!(peer)
|
|
324
|
+
if role == :reviewer
|
|
325
|
+
assignment = active_review_event(events, current)&.dig("payload", "data")
|
|
326
|
+
unless assignment && assignment["review_id"] == params["purpose_id"] && assignment["head"] == current["head"] &&
|
|
327
|
+
assignment["candidate_generation"] == current["candidate_generation"] && assignment["reviewer_uid"] == peer["uid"] &&
|
|
328
|
+
@kernel.descendant?(peer, assignment.fetch("reviewer_process_binding"))
|
|
329
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate export requires assigned independent reviewer"
|
|
330
|
+
end
|
|
331
|
+
elsif role == :executor
|
|
332
|
+
record = journal.service_request(params.fetch("purpose_id"))
|
|
333
|
+
unless record && record["assignment_id"] == params["assignment_id"] && record["attempt_id"] == params["attempt_id"] &&
|
|
334
|
+
record["executor_uid"] == peer["uid"] && record["candidate_head"] == current["head"] &&
|
|
335
|
+
record["candidate_generation"] == current["candidate_generation"] && record["dispatch_phase"] == "issued"
|
|
336
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate export requires current executor ticket"
|
|
337
|
+
end
|
|
338
|
+
else
|
|
339
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate export purpose is unauthorized"
|
|
340
|
+
end
|
|
341
|
+
current
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def assigned_review(events)
|
|
345
|
+
events.reverse.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "assign_review" }&.dig("payload", "data")
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# A retained approval grants permission only while its original imported
|
|
349
|
+
# bytes, independent reviewer and exact candidate still verify. An old
|
|
350
|
+
# successful mutation reply is not a substitute for canonical evidence.
|
|
351
|
+
def approved_review!(journal, events, params, map, current, commit: journal.ref_value, service_operation: nil)
|
|
352
|
+
accepted = events.reverse.find { |event| event["type"] == "authority_mutation" &&
|
|
353
|
+
event.dig("payload", "operation") == "accept_review" }&.dig("payload", "data")
|
|
354
|
+
review = active_review_event(events, current)&.dig("payload", "data")
|
|
355
|
+
unless accepted && review && accepted["head"] == current["head"] &&
|
|
356
|
+
accepted["candidate_generation"] == current["candidate_generation"] &&
|
|
357
|
+
accepted["review_id"] == review["review_id"] && accepted["reviewer_uid"] == review["reviewer_uid"] &&
|
|
358
|
+
accepted["review_binding"] == review.slice("review_id", "head", "candidate_generation", "reviewer_uid", "reviewer_process_binding") &&
|
|
359
|
+
review["reviewer_uid"] != map.fetch("worker_uid")
|
|
360
|
+
raise AttemptErrors::ReceiptRejected, "current candidate requires its exact independent review"
|
|
361
|
+
end
|
|
362
|
+
context = {kind: "review", project_id: map.fetch("project_id"), assignment_id: params.fetch("assignment_id"),
|
|
363
|
+
attempt_id: params.fetch("attempt_id"), peer_uid: review.fetch("reviewer_uid"),
|
|
364
|
+
binding: accepted.fetch("review_binding"), request_id_or_event_id: review.fetch("review_id"),
|
|
365
|
+
generation: current.fetch("candidate_generation")}
|
|
366
|
+
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
367
|
+
reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
368
|
+
**context, commit: commit) }
|
|
369
|
+
intent = events.find { |event| event["type"] == "intent" }
|
|
370
|
+
attempt = journal.send(:build_attempt, params.fetch("assignment_id"), params.fetch("attempt_id"),
|
|
371
|
+
intent.fetch("payload"), events, "running")
|
|
372
|
+
identity = Molecules::ExecutionIdentityResolver::Identity.new(actor: "protected-authority", role: "service", runtime: "unix")
|
|
373
|
+
receipt = Molecules::ReceiptVerifier.new(artifact_reader: reader).verify!(accepted.fetch("review_receipt"),
|
|
374
|
+
attempt: attempt, identity: identity, live_head: current.fetch("head"), repo_root: journal.repo_root)
|
|
375
|
+
unless receipt.digest == accepted.fetch("receipt_digest") && receipt.operation == "review" &&
|
|
376
|
+
receipt.to_h.dig("review", "reviewer", "actor") == review.fetch("reviewer_actor")
|
|
377
|
+
raise AttemptErrors::ReceiptRejected, "canonical review receipt differs"
|
|
378
|
+
end
|
|
379
|
+
if %w[ready merge].include?(service_operation)
|
|
380
|
+
campaign_service_checks!(journal: journal, commit: commit, events: events, params: params, map: map, current: current)
|
|
381
|
+
end
|
|
382
|
+
accepted
|
|
383
|
+
rescue KeyError
|
|
384
|
+
raise AttemptErrors::ReceiptRejected, "canonical review binding is incomplete"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def accept_review_plan(journal, events, params, map, current, review, admitted)
|
|
388
|
+
receipt = admitted.fetch(:receipt)
|
|
389
|
+
raise AttemptErrors::EvidenceUnavailable, "protected campaigns require their canonical owner" if receipt["campaign"]
|
|
390
|
+
unless receipt["operation"] == "review" && receipt["verdict"] == "succeeded" &&
|
|
391
|
+
receipt["producer"] == {"actor" => map.fetch("worker_actor"), "role" => "worker", "runtime" => "herdr"} &&
|
|
392
|
+
receipt.dig("review", "reviewer", "actor") == review.fetch("reviewer_actor")
|
|
393
|
+
raise AttemptErrors::ReceiptRejected, "review attribution must match authenticated candidate and reviewer"
|
|
394
|
+
end
|
|
395
|
+
original = Models::ExecutionReceipt.from_h(receipt)
|
|
396
|
+
unless original.digest == Atoms::EvidenceDigest.digest(original.digest_payload)
|
|
397
|
+
raise AttemptErrors::ReceiptRejected, "uploaded review receipt digest differs"
|
|
398
|
+
end
|
|
399
|
+
context = {kind: "review", project_id: map.fetch("project_id"), assignment_id: params.fetch("assignment_id"),
|
|
400
|
+
attempt_id: params.fetch("attempt_id"), peer_uid: review.fetch("reviewer_uid"),
|
|
401
|
+
binding: review.slice("review_id", "head", "candidate_generation", "reviewer_uid", "reviewer_process_binding"),
|
|
402
|
+
request_id_or_event_id: review.fetch("review_id"), generation: current.fetch("candidate_generation")}
|
|
403
|
+
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
404
|
+
plan = canonical.import_plan(**context, artifacts: admitted.fetch(:artifacts),
|
|
405
|
+
admitted_after_event_digest: events.last.fetch("digest"))
|
|
406
|
+
original_refs = receipt.fetch("artifacts")
|
|
407
|
+
normalized = JSON.parse(JSON.generate(receipt)).except("digest", "recorded_at")
|
|
408
|
+
normalized["artifacts"] = plan.fetch(:references).map { |reference| {"path" => reference.fetch("ref"), "sha256" => reference.fetch("sha256")} }
|
|
409
|
+
if normalized["campaign"]
|
|
410
|
+
index = original_refs.index(normalized.fetch("campaign").fetch("result"))
|
|
411
|
+
raise AttemptErrors::ReceiptRejected, "campaign result must name an uploaded artifact" unless index
|
|
412
|
+
normalized["campaign"]["result"] = normalized.fetch("artifacts").fetch(index)
|
|
413
|
+
end
|
|
414
|
+
pending = journal.send(:chain_mutation_events, params.fetch("attempt_id"), events.last.fetch("digest"), plan.fetch(:events))
|
|
415
|
+
reader = lambda do |_data, artifact|
|
|
416
|
+
canonical.read_pending({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
417
|
+
**context, current_events: events, pending_events: pending, blobs: plan.fetch(:blobs), commit: journal.ref_value)
|
|
418
|
+
end
|
|
419
|
+
intent = events.find { |event| event["type"] == "intent" }
|
|
420
|
+
attempt = journal.send(:build_attempt, params.fetch("assignment_id"), params.fetch("attempt_id"),
|
|
421
|
+
intent.fetch("payload"), events, "running")
|
|
422
|
+
identity = Molecules::ExecutionIdentityResolver::Identity.new(actor: "protected-authority", role: "service", runtime: "unix")
|
|
423
|
+
verified = Molecules::ReceiptVerifier.new(artifact_reader: reader).verify!(normalized,
|
|
424
|
+
attempt: attempt, identity: identity, live_head: current.fetch("head"), repo_root: journal.repo_root)
|
|
425
|
+
plan.merge(data: {"head" => current.fetch("head"), "candidate_generation" => current.fetch("candidate_generation"),
|
|
426
|
+
"review_id" => review.fetch("review_id"), "reviewer_uid" => review.fetch("reviewer_uid"),
|
|
427
|
+
"review_binding" => context.fetch(:binding), "review_receipt" => verified.to_h,
|
|
428
|
+
"receipt_digest" => verified.digest, "uploaded_receipt_sha256" => admitted.fetch(:receipt_sha256)})
|
|
429
|
+
rescue KeyError
|
|
430
|
+
raise AttemptErrors::ReceiptRejected, "review receipt binding is incomplete"
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
require_relative "endcap_services"
|
|
438
|
+
require_relative "endcap_service_settlement"
|
|
439
|
+
|
|
440
|
+
require_relative "endcap_campaign_results"
|
|
441
|
+
require_relative "endcap_campaign_rounds"
|
|
442
|
+
require_relative "endcap_results"
|
|
443
|
+
|
|
444
|
+
require_relative "endcap_inboxes"
|
|
445
|
+
|
|
446
|
+
require_relative "endcap_contexts"
|
|
447
|
+
require_relative "endcap_prepared_work"
|
|
448
|
+
|
|
449
|
+
require_relative "endcap_reviews"
|
|
450
|
+
|
|
451
|
+
require_relative "endcap_review_requests"
|
|
452
|
+
|
|
453
|
+
require_relative "endcap_attempts"
|
|
454
|
+
|
|
455
|
+
require_relative "endcap_publication"
|