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,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../atoms/evidence_digest"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Assign
|
|
6
|
+
module Authority
|
|
7
|
+
class LaunchLifecycle
|
|
8
|
+
# One authenticated immutable prefix; never the latest registration.
|
|
9
|
+
def original_prepared_registration!(journal:, params:, commit:)
|
|
10
|
+
original_prepared_registration_for_phase!(journal: journal, params: params, commit: commit, phase: :issued) do |map, state|
|
|
11
|
+
yield(map, state) if block_given?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def original_bound_prepared_registration!(journal:, params:, commit:)
|
|
18
|
+
original_prepared_registration_for_phase!(journal: journal, params: params, commit: commit, phase: :bound)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def original_prepared_registration_for_phase!(journal:, params:, commit:, phase:)
|
|
22
|
+
inventory = journal.canonical_event_inventory!(commit: commit)
|
|
23
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared canonical snapshot differs" unless inventory.fetch("commit") == commit
|
|
24
|
+
assignment = params.fetch("assignment_id")
|
|
25
|
+
events = inventory.fetch("events").fetch(assignment)
|
|
26
|
+
chain = events.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
27
|
+
state = origin(chain, **params.slice("assignment_id", "attempt_id", "mapping_id").transform_keys(&:to_sym))
|
|
28
|
+
reserves = chain.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
|
|
29
|
+
releases = chain.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "release_launch" }
|
|
30
|
+
provisioning = chain.select { |event| event["type"] == "scope_provisioning" }
|
|
31
|
+
admitted_phase = if phase == :issued
|
|
32
|
+
releases.one? && releases.first.dig("payload", "data", "phase") == "issued"
|
|
33
|
+
else
|
|
34
|
+
releases.empty? && state.fetch("phase") == "bound"
|
|
35
|
+
end
|
|
36
|
+
unless reserves.one? && admitted_phase && provisioning.one? &&
|
|
37
|
+
!terminal_events?(chain) && !chain.any? { |event| %w[scope_sealed input_inhibited attempt_stopped].include?(event["type"]) }
|
|
38
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared attempt is not #{phase} and open"
|
|
39
|
+
end
|
|
40
|
+
identity = provisioning.first.fetch("payload")
|
|
41
|
+
sha = identity.fetch("descriptor_sha256")
|
|
42
|
+
selected = if @deployment.artifact_reference.fetch("sha256") == sha
|
|
43
|
+
@deployment
|
|
44
|
+
else
|
|
45
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared deployment is unavailable" unless @deployment_history
|
|
46
|
+
@deployment_history.descriptor!(sha256: sha)
|
|
47
|
+
end
|
|
48
|
+
map = selected.mapping(params.fetch("mapping_id"))
|
|
49
|
+
original_project = selected.project(map.fetch("project_id"))
|
|
50
|
+
unless identity.fetch("deployment_digest") == Digest::SHA256.hexdigest(JSON.generate(canonical(map))) &&
|
|
51
|
+
identity.fetch("slot_id") == map.fetch("execution_scope").fetch("slot_id") && state.fetch("project_id") == map.fetch("project_id") &&
|
|
52
|
+
selected.project(map.fetch("project_id")).values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
|
|
53
|
+
[journal.repo_root, journal.ref, journal.checkout_root]
|
|
54
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared deployment association differs"
|
|
55
|
+
end
|
|
56
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: chain, project_id: state.fetch("project_id"),
|
|
57
|
+
assignment_id: assignment, attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
58
|
+
unless lineage.require_launch_bound! == state.fetch("process_binding")
|
|
59
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared child binding differs"
|
|
60
|
+
end
|
|
61
|
+
original = original_prompt_record!(chain, state, params: params)
|
|
62
|
+
# Authenticate the actual original worker/descendant before reading artifacts.
|
|
63
|
+
yield(map, state) if block_given?
|
|
64
|
+
introductions = inventory.fetch("introductions").fetch(assignment)
|
|
65
|
+
reservation_commit = introductions.fetch(reserves.first.fetch("digest"))
|
|
66
|
+
registration = definition(journal, assignment, commit: reservation_commit)
|
|
67
|
+
original_events = journal.read_events(assignment, commit: reservation_commit)
|
|
68
|
+
authenticate_inventory_registration!(journal, assignment, original_events, registration, params.fetch("mapping_id"), map, reservation_commit, introductions)
|
|
69
|
+
registered = original_events.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "register_assignment" && event.dig("payload", "data") == registration }
|
|
70
|
+
registration_commit = introductions.fetch(registered.fetch("digest"))
|
|
71
|
+
TaskContextEntry.validate!(registration.fetch("task_context_entry"))
|
|
72
|
+
unless registration.fetch("task_context_entry") == map.fetch("task_context_entry")
|
|
73
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared task context entry differs"
|
|
74
|
+
end
|
|
75
|
+
prepared = registration.fetch("prepared_work")
|
|
76
|
+
unless prepared.fetch("scope") == state.fetch("scope") && prepared.fetch("task_id") == state.fetch("task_id")
|
|
77
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared selection differs"
|
|
78
|
+
end
|
|
79
|
+
bytes = journal.bounded_blob(registration.fetch("definition_ref"), commit: registration_commit, max_bytes: PreparedWork::MAX_MANIFEST)
|
|
80
|
+
unless bytes.is_a?(String) && bytes.bytesize.between?(1, PreparedWork::MAX_MANIFEST) &&
|
|
81
|
+
Digest::SHA256.hexdigest(bytes) == registration.fetch("definition_digest") && JSON.parse(bytes).fetch("prepared_work") == prepared
|
|
82
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared definition differs"
|
|
83
|
+
end
|
|
84
|
+
projection = immutable_maintenance_projection(registration: registration, registration_commit: registration_commit, state: state, map: map, events: chain,
|
|
85
|
+
workspace_exclusion: maintenance_scope_observer_for(selected, params.fetch("mapping_id")).workspace_exclusion_projection!(lineage),
|
|
86
|
+
original_worker_scratch_root: original_project.fetch("peer_credentials").fetch(map.fetch("worker_uid").to_s).fetch("scratch_root"),
|
|
87
|
+
original_binding_digest: original.fetch("binding_digest"), commit: commit)
|
|
88
|
+
pin = compact_prepared_input(projection)
|
|
89
|
+
if phase == :issued
|
|
90
|
+
unless Atoms::EvidenceDigest.digest(releases.first.dig("payload", "data", "prepared_input")) == Atoms::EvidenceDigest.digest(pin)
|
|
91
|
+
raise AttemptErrors::EvidenceUnavailable, "accepted prepared release differs"
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
bundle = journal.bounded_blob(registration.fetch("prepared_bundle_ref"), commit: registration_commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
95
|
+
unless bundle.is_a?(String) && bundle.bytesize == registration.fetch("prepared_bundle_bytes") &&
|
|
96
|
+
bundle.bytesize.between?(1, CandidateTransfer::MAX_BYTES) && Digest::SHA256.hexdigest(bundle) == registration.fetch("prepared_bundle_sha256")
|
|
97
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared bundle differs"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
projection
|
|
101
|
+
rescue KeyError, TypeError, NoMethodError, JSON::ParserError
|
|
102
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared registration is unavailable"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def compact_prepared_input(projection)
|
|
106
|
+
registration = projection.fetch(:registration)
|
|
107
|
+
immutable_maintenance_projection(
|
|
108
|
+
"registration_generation" => registration.fetch("generation"),
|
|
109
|
+
"registration_commit" => projection.fetch(:registration_commit),
|
|
110
|
+
"definition_digest" => registration.fetch("definition_digest"),
|
|
111
|
+
"original_binding_digest" => projection.fetch(:original_binding_digest),
|
|
112
|
+
"prepared_work" => registration.fetch("prepared_work"),
|
|
113
|
+
"worker_entry" => Ace::Runtime::Molecules::ProtectedWorkerEntry.validate!(projection.fetch(:map).fetch("worker_entry")),
|
|
114
|
+
"workspace_exclusion" => projection.fetch(:workspace_exclusion),
|
|
115
|
+
"bundle_ref" => registration.fetch("prepared_bundle_ref"),
|
|
116
|
+
"bundle_bytes" => registration.fetch("prepared_bundle_bytes"),
|
|
117
|
+
"bundle_sha256" => registration.fetch("prepared_bundle_sha256"))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class LaunchLifecycle
|
|
7
|
+
# Called by Endcap only while holding the existing assignment/slot
|
|
8
|
+
# exclusion and shared authority mutex. Returned channel capacity is
|
|
9
|
+
# released after the caller leaves those locks and waits on the stream.
|
|
10
|
+
def reserve_review_dispatch!(events:, params:, map:)
|
|
11
|
+
state = origin(events, **params.slice("mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
|
|
12
|
+
unless state["phase"] == "issued" && !terminal_events?(events)
|
|
13
|
+
raise AttemptErrors::Conflict, "review delegation requires an issued original launcher"
|
|
14
|
+
end
|
|
15
|
+
scope_open_for_effect!(events: events, params: params, map: map)
|
|
16
|
+
original = original_prompt_record!(events, state, params: params)
|
|
17
|
+
@kernel.live!(state.fetch("launcher_identity"))
|
|
18
|
+
channel = @control_channels[steering_key(params, map)]
|
|
19
|
+
raise AttemptErrors::EvidenceUnavailable, "original review channel is unavailable" unless channel
|
|
20
|
+
[channel, channel.reserve_dispatch!, original]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def review_original_reference!(events:, params:)
|
|
24
|
+
state = origin(events, **params.slice("mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
|
|
25
|
+
original_prompt_record!(events, state, params: params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read-only original-launcher projection at the exact request commit.
|
|
29
|
+
# Permission to assign still belongs to Endcap's fresh/replay checks.
|
|
30
|
+
def launch_review_intent!(request:, peer:, role:)
|
|
31
|
+
params = request.fetch("params")
|
|
32
|
+
strict!(params, %w[mapping_id assignment_id attempt_id mutation_id request_event_id journal_commit original_binding_digest])
|
|
33
|
+
raise ArgumentError, "Review intent lookup is read-only" unless request.fetch("mutation_id").nil?
|
|
34
|
+
raise AttemptErrors::UnauthorizedIdentity, "Review intent requires original launcher" unless role == :launcher
|
|
35
|
+
%w[mapping_id assignment_id attempt_id mutation_id].each { |key| token!(params.fetch(key)) }
|
|
36
|
+
unless %w[request_event_id original_binding_digest].all? { |key| params[key].is_a?(String) && params[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
37
|
+
params["journal_commit"].is_a?(String) && params["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
38
|
+
raise ArgumentError, "Review intent selectors differ"
|
|
39
|
+
end
|
|
40
|
+
map = steering_principal!(params, peer, role)
|
|
41
|
+
raise AttemptErrors::UnauthorizedIdentity, "Review project differs" unless request.fetch("project_id") == map.fetch("project_id")
|
|
42
|
+
journal = journal_for(map)
|
|
43
|
+
commit = params.fetch("journal_commit")
|
|
44
|
+
journal.verify_canonical_prefix!(commit: commit)
|
|
45
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
46
|
+
state = origin(events, **params.slice("mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
|
|
47
|
+
unless @kernel.same?(state.fetch("launcher_identity"), peer)
|
|
48
|
+
raise AttemptErrors::UnauthorizedIdentity, "Review intent requires exact original launcher birth"
|
|
49
|
+
end
|
|
50
|
+
original = original_prompt_record!(events, state, params: params)
|
|
51
|
+
event = events.find { |item| item["digest"] == params.fetch("request_event_id") }
|
|
52
|
+
accepted = journal.mutation_result(params.fetch("mutation_id"), commit: commit)
|
|
53
|
+
unless event && event["type"] == "authority_mutation" && event.dig("payload", "operation") == "request_review" &&
|
|
54
|
+
event.dig("payload", "mutation_id") == params.fetch("mutation_id") && accepted &&
|
|
55
|
+
accepted == event.fetch("payload").merge("journal_commit" => commit) &&
|
|
56
|
+
original.fetch("binding_digest") == params.fetch("original_binding_digest")
|
|
57
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review request introduction differs"
|
|
58
|
+
end
|
|
59
|
+
data = event.fetch("payload").fetch("data")
|
|
60
|
+
fields = %w[candidate_generation generation head original_binding_digest requester reviewer_process_binding reviewer_uid]
|
|
61
|
+
unless data.is_a?(Hash) && data.keys.sort == fields && data["original_binding_digest"] == original.fetch("binding_digest") &&
|
|
62
|
+
data["head"].is_a?(String) && data["head"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
63
|
+
%w[candidate_generation generation reviewer_uid].all? { |key| data[key].is_a?(Integer) && data[key].positive? }
|
|
64
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review request data differs"
|
|
65
|
+
end
|
|
66
|
+
identity = data.fetch("reviewer_process_binding")
|
|
67
|
+
boot = identity.is_a?(Hash) && identity["started_at"].is_a?(String) && identity["started_at"].split(":", -1)[1]
|
|
68
|
+
Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: boot)
|
|
69
|
+
requester = data["requester"]
|
|
70
|
+
unless requester.is_a?(Hash) && requester.keys.sort == %w[gid groups role uid] &&
|
|
71
|
+
%w[uid gid].all? { |key| requester[key].is_a?(Integer) } && requester["groups"].is_a?(Array) &&
|
|
72
|
+
requester["groups"].all? { |group| group.is_a?(Integer) } &&
|
|
73
|
+
requester == identity.slice("uid", "gid", "groups").merge("role" => "reviewer") &&
|
|
74
|
+
data["reviewer_uid"] == identity["uid"] && identity["uid"] != map.fetch("worker_uid")
|
|
75
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical reviewer attribution differs"
|
|
76
|
+
end
|
|
77
|
+
candidate = events.reverse.find { |item| item["type"] == "authority_mutation" && item.dig("payload", "operation") == "submit_candidate" }&.dig("payload", "data")
|
|
78
|
+
unless candidate && candidate.values_at("head", "candidate_generation") == data.values_at("head", "candidate_generation")
|
|
79
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical request candidate differs"
|
|
80
|
+
end
|
|
81
|
+
assignment = params.slice("mapping_id", "assignment_id", "attempt_id").merge(
|
|
82
|
+
data.slice("head", "candidate_generation", "reviewer_uid", "reviewer_process_binding"),
|
|
83
|
+
"expected_generation" => data.fetch("generation"))
|
|
84
|
+
{data: params.slice("request_event_id", "original_binding_digest").merge("assignment_params" => assignment), replayed: false}
|
|
85
|
+
rescue KeyError, TypeError
|
|
86
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review intent is incomplete"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class LaunchLifecycle
|
|
7
|
+
SCOPE_ABORT_FIELDS = %w[kind scope_generation scope_binding_event_id seal_event_id proof_id].freeze
|
|
8
|
+
|
|
9
|
+
# Also used by the shared final release-before-reuse predicate. Calls
|
|
10
|
+
# existing owners at the same immutable CAS input, without impersonating
|
|
11
|
+
# a public evidence-fetch peer or inventing an alternate verifier.
|
|
12
|
+
def scope_settlement_complete!(journal:, events:, params:, map:, commit:)
|
|
13
|
+
if @result_owner
|
|
14
|
+
@result_owner.service_settlement_complete!(journal: journal, events: events, params: params, map: map, commit: commit)
|
|
15
|
+
else
|
|
16
|
+
service_events = events.any? { |event| %w[service_claim service_transition].include?(event["type"]) }
|
|
17
|
+
service_records = journal.service_request_records(commit: commit).any? do |record|
|
|
18
|
+
record["assignment_id"] == params.fetch("assignment_id") && record["attempt_id"] == params.fetch("attempt_id")
|
|
19
|
+
end
|
|
20
|
+
if service_events || service_records
|
|
21
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical service settlement owner is unavailable"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def scope_abort_plan(params, map, events, peer, journal:, commit:, supervisor:)
|
|
30
|
+
bytes = params.fetch("failure_evidence").dup.force_encoding(Encoding::UTF_8)
|
|
31
|
+
raise ArgumentError, "scope failure must be UTF-8 JSON" unless bytes.valid_encoding?
|
|
32
|
+
failure = JSON.parse(bytes, create_additions: false, max_nesting: 8, allow_duplicate_key: false, allow_comments: false)
|
|
33
|
+
strict!(failure, SCOPE_ABORT_FIELDS)
|
|
34
|
+
unless failure["kind"] == "protected_scope_before_release" && failure["scope_generation"].is_a?(Integer) && failure["scope_generation"].positive?
|
|
35
|
+
raise ArgumentError, "scope failure selectors differ"
|
|
36
|
+
end
|
|
37
|
+
%w[scope_binding_event_id seal_event_id proof_id].each do |key|
|
|
38
|
+
value = failure.fetch(key)
|
|
39
|
+
raise ArgumentError, "invalid scope failure selector" unless value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/)
|
|
40
|
+
end
|
|
41
|
+
state = states(events)[params.fetch("attempt_id")]
|
|
42
|
+
unless state && state["mapping_id"] == params.fetch("mapping_id") && state["launch_ticket"] == params.fetch("launch_ticket") &&
|
|
43
|
+
(supervisor || @kernel.same?(state.fetch("launcher_identity"), peer))
|
|
44
|
+
raise AttemptErrors::UnauthorizedIdentity, "scope failure is not owned by this launcher"
|
|
45
|
+
end
|
|
46
|
+
chain = events.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
47
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: chain, project_id: map.fetch("project_id"),
|
|
48
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
49
|
+
lineage.require_positive!(**failure.reject { |key, _| key == "kind" }.transform_keys(&:to_sym))
|
|
50
|
+
scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
|
|
51
|
+
if chain.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "release_launch" } ||
|
|
52
|
+
state["phase"] == "issued" || state["execution"] == "potentially_executed"
|
|
53
|
+
raise AttemptErrors::Conflict, "scope failure cannot undo issued launch permission"
|
|
54
|
+
end
|
|
55
|
+
scope_settlement_complete!(journal: journal, events: chain, params: params, map: map, commit: commit)
|
|
56
|
+
current = journal.canonical_attempt_state(chain)
|
|
57
|
+
unless %w[reserved running uncertain].include?(current)
|
|
58
|
+
raise AttemptErrors::Conflict, "scope failure cannot change a terminal attempt"
|
|
59
|
+
end
|
|
60
|
+
proof = failure.merge("release" => "not_issued")
|
|
61
|
+
reason = "protected_scope_closed_before_release"
|
|
62
|
+
event = if current == "uncertain"
|
|
63
|
+
{type: "reconciliation", payload: {"resolution" => "failed", "reason" => reason,
|
|
64
|
+
"failure_digest" => params.fetch("failure_digest"), "abort_observation" => proof}}
|
|
65
|
+
else
|
|
66
|
+
# This is the sole reserved -> failed admission. The generic state
|
|
67
|
+
# machine remains unchanged; no synthetic process_start is written.
|
|
68
|
+
{type: "transition", payload: {"from" => current, "to" => "failed", "reason" => reason,
|
|
69
|
+
"abort_observation" => proof}}
|
|
70
|
+
end
|
|
71
|
+
reference = "evidence/imports/launch-failure-#{params.fetch('attempt_id')}-#{params.fetch('failure_digest')}"
|
|
72
|
+
{events: [event], blobs: {reference => params.fetch("failure_evidence")}, data: state.merge(
|
|
73
|
+
"phase" => "failed", "failure_digest" => params.fetch("failure_digest"), "failure_ref" => reference,
|
|
74
|
+
"abort_observation" => proof, "required_action" => nil)}
|
|
75
|
+
rescue JSON::ParserError
|
|
76
|
+
raise ArgumentError, "scope failure JSON is invalid"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class LaunchLifecycle
|
|
7
|
+
# Internal owner call, deliberately absent from the public operation
|
|
8
|
+
# allowlist. The durable winner alone may call the fixed manager. A
|
|
9
|
+
# lost StartUnit reply retains admission uncertainty; replay never starts.
|
|
10
|
+
def admit_native_service!(params:, peer:, role:)
|
|
11
|
+
strict!(params, %w[mapping_id assignment_id attempt_id launch_ticket mutation_id expected_generation])
|
|
12
|
+
unless %i[launcher supervisor].include?(role)
|
|
13
|
+
raise AttemptErrors::UnauthorizedIdentity, "native admission requires an exact launch owner"
|
|
14
|
+
end
|
|
15
|
+
%w[assignment_id attempt_id launch_ticket mutation_id].each { |key| token!(params.fetch(key)) }
|
|
16
|
+
generation!(params.fetch("expected_generation"))
|
|
17
|
+
@kernel.live!(peer)
|
|
18
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
19
|
+
journal = journal_for(map)
|
|
20
|
+
input = params.reject { |key, _| key == "mutation_id" }
|
|
21
|
+
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(input)))
|
|
22
|
+
result = with_exclusion(params, map, journal) do
|
|
23
|
+
admit_native_service_held!(params, map, journal, peer, role, digest)
|
|
24
|
+
end
|
|
25
|
+
complete_native_start!(params, map, journal, peer, role, result) unless result.fetch(:replayed)
|
|
26
|
+
result
|
|
27
|
+
ensure
|
|
28
|
+
settle_native_issuer!(params, map) if map
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Private transport owner. Exact manager hook attribution precedes
|
|
32
|
+
# challenge issuance; this never enters the ordinary worker dispatcher.
|
|
33
|
+
def native_readiness!(mapping_id:, peer:, socket:, codec:, deadline:)
|
|
34
|
+
map = @deployment.mapping(mapping_id)
|
|
35
|
+
journal = journal_for(map)
|
|
36
|
+
pending, key = @mutex.synchronize do
|
|
37
|
+
selected = @native_issuers.select { |identity, _| identity[0] == map.fetch("project_id") && identity[1] == mapping_id }
|
|
38
|
+
raise AttemptErrors::EvidenceUnavailable, "no unique live admitted issuer" unless selected.size == 1
|
|
39
|
+
identity, entry = selected.first
|
|
40
|
+
raise AttemptErrors::Conflict, "readiness already supplied" if entry[:report] || entry[:challenge]
|
|
41
|
+
entry[:challenge] = SecureRandom.hex(32)
|
|
42
|
+
[entry, identity]
|
|
43
|
+
end
|
|
44
|
+
params = pending.fetch(:params)
|
|
45
|
+
observer = scope_observer_for(mapping_id)
|
|
46
|
+
challenge = with_exclusion(params, map, journal) do
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
events = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
49
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
50
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: mapping_id)
|
|
51
|
+
lineage.require_open!
|
|
52
|
+
server = observer.readiness_peer!(lineage, peer)
|
|
53
|
+
{"version" => 1, "challenge_id" => pending.fetch(:challenge), "server_identity" => server}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
wire.write(socket, challenge, deadline: deadline, limit: 16_384)
|
|
57
|
+
envelope = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
58
|
+
unless envelope.is_a?(Hash) && envelope.keys.sort == %w[challenge_id transfer version] &&
|
|
59
|
+
envelope["version"] == 1 && envelope["challenge_id"] == challenge.fetch("challenge_id")
|
|
60
|
+
raise ArgumentError, "private readiness upload envelope differs"
|
|
61
|
+
end
|
|
62
|
+
codec.receive(socket, descriptor: envelope.fetch("transfer"), purpose: :scope_boundary_observation,
|
|
63
|
+
deadline: deadline) do |input|
|
|
64
|
+
bytes = input.bytes.force_encoding(Encoding::UTF_8)
|
|
65
|
+
raise ArgumentError, "readiness observation is not UTF-8" unless bytes.valid_encoding?
|
|
66
|
+
report = JSON.parse(bytes, create_additions: false, max_nesting: 12, allow_duplicate_key: false, allow_comments: false)
|
|
67
|
+
with_exclusion(params, map, journal) do
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
unless @native_issuers[key].equal?(pending) && !pending[:report]
|
|
70
|
+
raise AttemptErrors::EvidenceUnavailable, "readiness issuer lifetime ended"
|
|
71
|
+
end
|
|
72
|
+
events = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
73
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
74
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: mapping_id)
|
|
75
|
+
lineage.require_open!
|
|
76
|
+
pending[:report] = observer.verify_readiness_report!(lineage, peer, report, challenge: challenge)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
wire.write(socket, {"version" => 1, "challenge_id" => challenge.fetch("challenge_id"), "status" => "ready"},
|
|
81
|
+
deadline: deadline, limit: 16_384)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def admit_native_service_held!(params, map, journal, peer, role, digest)
|
|
87
|
+
result = @mutex.synchronize do
|
|
88
|
+
# Visibility is current even on a historical mutation replay.
|
|
89
|
+
current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
90
|
+
scope_admission_owner!(params, map, current, peer, role)
|
|
91
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
92
|
+
mutation_id: params.fetch("mutation_id"), operation: "scope_service_admission", parameters_digest: digest,
|
|
93
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true) do |events, _commit, generation|
|
|
94
|
+
state, lineage = scope_admission_owner!(params, map, events, peer, role)
|
|
95
|
+
lineage.require_open!
|
|
96
|
+
unless state["phase"] == "reserved" && !lineage.native_event && !lineage.child_event &&
|
|
97
|
+
events.none? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_service_admission" }
|
|
98
|
+
raise AttemptErrors::Conflict, "native activation was already admitted or launch advanced"
|
|
99
|
+
end
|
|
100
|
+
observer = scope_observer_for(params.fetch("mapping_id"))
|
|
101
|
+
installation = observer.native_admission_ready!(lineage)
|
|
102
|
+
binding = lineage.binding
|
|
103
|
+
data = state.merge("scope_generation" => binding.fetch("scope_generation"),
|
|
104
|
+
"scope_binding_event_id" => lineage.binding_event.fetch("digest"),
|
|
105
|
+
"service_unit" => map.fetch("execution_scope").fetch("service_unit"), "native_admission" => "issued_uncertain",
|
|
106
|
+
"network_installation" => installation)
|
|
107
|
+
unless installation.is_a?(Hash) && installation["namespace_path"] == map.fetch("execution_scope").fetch("network_namespace_path")
|
|
108
|
+
raise AttemptErrors::EvidenceUnavailable, "network verifier output names another fixed namespace path"
|
|
109
|
+
end
|
|
110
|
+
candidate = Models::EvidenceEvent.build(type: "authority_mutation", attempt_id: params.fetch("attempt_id"),
|
|
111
|
+
payload: {"operation" => "scope_service_admission", "data" => data.merge("generation" => generation + 1)},
|
|
112
|
+
previous_digest: events.last&.fetch("digest"))
|
|
113
|
+
Molecules::ExecutionScopeLineage.new(events: events + [candidate], project_id: map.fetch("project_id"),
|
|
114
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
115
|
+
{events: [], blobs: {}, data: data}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
unless result.fetch(:replayed)
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
raise AttemptErrors::Conflict, "native issuer already active" if @native_issuers.key?(native_issuer_key(params, map))
|
|
121
|
+
@native_issuers[native_issuer_key(params, map)] = {params: params.dup.freeze, report: nil, owner: Thread.current}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
result
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def complete_native_start!(params, map, journal, peer, role, result)
|
|
128
|
+
attempt_id = params.fetch("attempt_id")
|
|
129
|
+
observer = scope_observer_for(params.fetch("mapping_id"))
|
|
130
|
+
issue = with_exclusion(params, map, journal) do
|
|
131
|
+
@mutex.synchronize do
|
|
132
|
+
events = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == attempt_id }
|
|
133
|
+
_, lineage = scope_admission_owner!(params, map, events, peer, role)
|
|
134
|
+
!lineage.sealed?
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
observer.start_admitted_service! if issue
|
|
138
|
+
# The pending issuer lifetime remains live through this final check
|
|
139
|
+
# and cleanup. A seal cannot publish proof then permit a late start.
|
|
140
|
+
with_exclusion(params, map, journal) do
|
|
141
|
+
events, lineage = @mutex.synchronize do
|
|
142
|
+
current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == attempt_id }
|
|
143
|
+
_, retained = scope_admission_owner!(params, map, current, peer, role)
|
|
144
|
+
[current, retained]
|
|
145
|
+
end
|
|
146
|
+
if lineage.sealed?
|
|
147
|
+
observer.stop_sealed_service!
|
|
148
|
+
elsif issue
|
|
149
|
+
payload = observer.complete_native_readiness!(lineage,
|
|
150
|
+
@mutex.synchronize { @native_issuers.fetch(native_issuer_key(params, map)).fetch(:report) })
|
|
151
|
+
@mutex.synchronize { commit_ready_native!(params, map, journal, events, lineage, payload) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
rescue StandardError
|
|
155
|
+
# Lost command completion is uncertain. Cleanup of the fixed service
|
|
156
|
+
# must finish before the live issuer can retire its pending lifetime.
|
|
157
|
+
observer.stop_sealed_service! if issue
|
|
158
|
+
raise
|
|
159
|
+
ensure
|
|
160
|
+
settle_native_issuer!(params, map) if attempt_id
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def commit_ready_native!(params, map, journal, events, lineage, payload)
|
|
164
|
+
context = params.slice("mapping_id", "assignment_id", "attempt_id").merge(
|
|
165
|
+
"scope_binding_event_id" => lineage.binding_event.fetch("digest"))
|
|
166
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
167
|
+
mutation_id: "scope-native-#{Digest::SHA256.hexdigest(lineage.binding_event.fetch('digest'))[0, 48]}",
|
|
168
|
+
operation: "scope_native_binding", parameters_digest: Digest::SHA256.hexdigest(JSON.generate(canonical(context))),
|
|
169
|
+
expected_generation: journal.authority_generation(events), with_replay: true) do |current, _commit, _generation|
|
|
170
|
+
fresh = Molecules::ExecutionScopeLineage.new(events: current, project_id: map.fetch("project_id"),
|
|
171
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
172
|
+
fresh.require_open!
|
|
173
|
+
raise AttemptErrors::Conflict, "native binding already exists" if fresh.native_event
|
|
174
|
+
at = Time.now.utc
|
|
175
|
+
event = Models::EvidenceEvent.build(type: "scope_native_bound", attempt_id: params.fetch("attempt_id"),
|
|
176
|
+
payload: payload, previous_digest: current.last&.fetch("digest"), recorded_at: at)
|
|
177
|
+
Molecules::ExecutionScopeLineage.new(events: current + [event], project_id: map.fetch("project_id"),
|
|
178
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
179
|
+
{events: [{type: "scope_native_bound", payload: payload, recorded_at: at}], blobs: {}, data: context}
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def native_issuer_key(params, map)
|
|
184
|
+
[map.fetch("project_id"), params.fetch("mapping_id"), params.fetch("assignment_id"), params.fetch("attempt_id")].freeze
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def settle_native_issuer!(params, map)
|
|
188
|
+
@mutex.synchronize do
|
|
189
|
+
key = native_issuer_key(params, map)
|
|
190
|
+
pending = @native_issuers[key]
|
|
191
|
+
@native_issuers.delete(key) if pending && pending[:owner].equal?(Thread.current)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def native_issuer_pending!(params, map)
|
|
196
|
+
if @native_issuers.key?(native_issuer_key(params, map))
|
|
197
|
+
raise AttemptErrors::EvidenceUnavailable, "native issuance is still pending"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def scope_admission_owner!(params, map, events, peer, role)
|
|
202
|
+
state = states(events)[params.fetch("attempt_id")]
|
|
203
|
+
unless state && state["mapping_id"] == params.fetch("mapping_id") &&
|
|
204
|
+
state["launch_ticket"] == params.fetch("launch_ticket") &&
|
|
205
|
+
(role == :supervisor || @kernel.same?(state.fetch("launcher_identity"), peer))
|
|
206
|
+
raise AttemptErrors::UnauthorizedIdentity, "native admission launch origin differs"
|
|
207
|
+
end
|
|
208
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
209
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
210
|
+
[state, lineage]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|