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,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "original_inbox_selection"
|
|
4
|
+
require_relative "../molecules/original_launch_binding"
|
|
5
|
+
require_relative "../molecules/execution_scope_lineage"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Assign
|
|
9
|
+
module Authority
|
|
10
|
+
# Canonical original identity only. No native thread, effect permission,
|
|
11
|
+
# live process observation, context lock or journal mutation is returned.
|
|
12
|
+
class InboxContextOriginal
|
|
13
|
+
OPERATIONS = ["inbox_context_original"].freeze
|
|
14
|
+
PARAMETERS = %w[assignment_id attempt_id event_id inbox_context_id mapping_id payload_sha256 purpose].freeze
|
|
15
|
+
NATIVE_FIELDS = %w[scope_generation scope_binding_event_id service_invocation_id server_identity socket_identity workspace_id].freeze
|
|
16
|
+
TOKEN = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/
|
|
17
|
+
SHA = /\A[0-9a-f]{64}\z/
|
|
18
|
+
|
|
19
|
+
def initialize(deployment:, history:, authority_id:, journals:,
|
|
20
|
+
kernel: Ace::Runtime::Molecules::ProtectedLinux.new)
|
|
21
|
+
@deployment, @history, @authority_id, @journals, @kernel = deployment, history, authority_id, journals, kernel
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dispatch(request:, peer:, role:)
|
|
25
|
+
unless request.is_a?(Hash) && request.keys.sort == %w[mutation_id operation params project_id version] &&
|
|
26
|
+
request["version"].is_a?(Integer) && request["version"] == 1 && request["operation"] == OPERATIONS.first &&
|
|
27
|
+
request["mutation_id"].nil? && role == :context_owner && request["params"].is_a?(Hash) &&
|
|
28
|
+
request["params"].keys.sort == PARAMETERS
|
|
29
|
+
raise AttemptErrors::UnauthorizedIdentity, "original inbox query differs"
|
|
30
|
+
end
|
|
31
|
+
params = request.fetch("params")
|
|
32
|
+
unless %w[assignment_id attempt_id event_id inbox_context_id mapping_id].all? { |key| params[key].is_a?(String) && TOKEN.match?(params[key]) } &&
|
|
33
|
+
%w[payload_sha256].all? { |key| params[key].is_a?(String) && SHA.match?(params[key]) } &&
|
|
34
|
+
%w[enqueue deliver].include?(params["purpose"])
|
|
35
|
+
raise AttemptErrors::UnauthorizedIdentity, "original inbox selectors differ"
|
|
36
|
+
end
|
|
37
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
38
|
+
context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
39
|
+
unless map.values_at("authority_id", "project_id") == [@authority_id, request.fetch("project_id")] &&
|
|
40
|
+
context.fetch("owner_credentials").values_at("uid", "gid", "groups") == peer.values_at("uid", "gid", "groups")
|
|
41
|
+
raise AttemptErrors::UnauthorizedIdentity, "original inbox query requires its fixed owner"
|
|
42
|
+
end
|
|
43
|
+
@kernel.live!(peer)
|
|
44
|
+
journal = @journals.fetch(map.fetch("project_id"))
|
|
45
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
46
|
+
unless journal.evidence_mode == :protected && project.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
|
|
47
|
+
[journal.repo_root, journal.ref, journal.checkout_root]
|
|
48
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox journal association differs"
|
|
49
|
+
end
|
|
50
|
+
commit = journal.ref_value
|
|
51
|
+
journal.verify_commit!(commit)
|
|
52
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
53
|
+
original, original_map = OriginalInboxSelection.new(history: @history, authority_id: @authority_id)
|
|
54
|
+
.load!(events: events, params: params, map: map, journal: journal, commit: commit)
|
|
55
|
+
original_context = original.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
56
|
+
unless original_context.fetch("native_mapping_id") == params.fetch("mapping_id")
|
|
57
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox native mapping differs"
|
|
58
|
+
end
|
|
59
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: original_map.fetch("project_id"),
|
|
60
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
61
|
+
child = lineage.child_event&.dig("payload", "original_process_binding")
|
|
62
|
+
records = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "record_launch" }
|
|
63
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox launch missing" unless records.one? && child
|
|
64
|
+
state = records.first.fetch("payload").fetch("data")
|
|
65
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox child differs" unless state.fetch("process_binding") == child
|
|
66
|
+
guarded = Molecules::OriginalLaunchBinding.verify!(events: events, state: state, params: params)
|
|
67
|
+
registration = {"event_id" => params.fetch("event_id"), "attempt_id" => params.fetch("attempt_id"),
|
|
68
|
+
"payload_sha256" => params.fetch("payload_sha256")}
|
|
69
|
+
bindings = events.select { |event| event["type"] == "inbox_binding" && event.dig("payload", "event_id") == params.fetch("event_id") }
|
|
70
|
+
unless bindings.size <= 1 && (params.fetch("purpose") == "enqueue" || bindings.one?) && bindings.all? { |event|
|
|
71
|
+
event.fetch("payload") == {"attempt_id" => params.fetch("attempt_id"), "event_id" => params.fetch("event_id"),
|
|
72
|
+
"inbox_context_id" => params.fetch("inbox_context_id"), "registration" => registration} }
|
|
73
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox registration differs"
|
|
74
|
+
end
|
|
75
|
+
digests = [guarded.fetch("binding_digest"), lineage.native_event.fetch("digest"), lineage.child_event.fetch("digest")] + bindings.map { |event| event.fetch("digest") }
|
|
76
|
+
journal.event_commits!(assignment_id: params.fetch("assignment_id"), event_digests: digests, commit: commit)
|
|
77
|
+
@kernel.live!(peer)
|
|
78
|
+
channel = original_map.fetch("native")
|
|
79
|
+
server = lineage.native_event.fetch("payload").fetch("server_identity")
|
|
80
|
+
path = channel.fetch("socket_path")
|
|
81
|
+
unless path.is_a?(String) && path.valid_encoding? && path.bytesize.between?(1, 107) &&
|
|
82
|
+
path.start_with?("/") && !path.include?("\0") && File.expand_path(path) == path &&
|
|
83
|
+
channel.fetch("version") == "0.9.3" && channel.fetch("protocol") == 22 &&
|
|
84
|
+
channel.fetch("workspace_id") == lineage.native_event.fetch("payload").fetch("workspace_id") &&
|
|
85
|
+
server.values_at("uid", "gid", "groups") == original_map.values_at("worker_uid", "worker_gid", "worker_groups")
|
|
86
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox native channel differs"
|
|
87
|
+
end
|
|
88
|
+
data = params.slice("assignment_id", "attempt_id", "event_id", "inbox_context_id", "mapping_id", "purpose")
|
|
89
|
+
.merge("schema" => "ace.assign.inbox-context-original/v1", "project_id" => map.fetch("project_id"),
|
|
90
|
+
"commit" => commit, "registration" => registration, "registered" => bindings.one?,
|
|
91
|
+
"original_binding_digest" => guarded.fetch("binding_digest"), "process_binding" => child,
|
|
92
|
+
"guarded_origin" => guarded.fetch("origin"), "native_binding" => lineage.native_event.fetch("payload").slice(*NATIVE_FIELDS),
|
|
93
|
+
"native_channel" => channel.slice("socket_path", "version"))
|
|
94
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox projection exceeds bound" if JSON.generate(data).bytesize > 15_360
|
|
95
|
+
{data: immutable(data), replayed: false}
|
|
96
|
+
rescue KeyError, TypeError, ArgumentError, NoMethodError, Ace::Herdr::Error, Ace::Runtime::RuntimeUnavailableError
|
|
97
|
+
raise AttemptErrors::EvidenceUnavailable, "original inbox evidence is unavailable"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def immutable(value)
|
|
103
|
+
case value
|
|
104
|
+
when Hash then value.to_h { |key, item| [key.dup.freeze, immutable(item)] }.freeze
|
|
105
|
+
when Array then value.map { |item| immutable(item) }.freeze
|
|
106
|
+
when String then value.dup.freeze
|
|
107
|
+
else value.freeze
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class LaunchLifecycle
|
|
7
|
+
# Caller owns the existing slot/assignment exclusion. This verifies
|
|
8
|
+
# an already accepted closure; it never seals, stops or observes anew.
|
|
9
|
+
def completion_scope!(journal:, events:, params:, map:, commit:)
|
|
10
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
11
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
12
|
+
mapping_id: params.fetch("mapping_id"))
|
|
13
|
+
unless lineage.proof_event && !pending_prompt_issuers?(events, journal, commit)
|
|
14
|
+
raise AttemptErrors::EvidenceUnavailable, "Original scope closure is unavailable"
|
|
15
|
+
end
|
|
16
|
+
scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
|
|
17
|
+
lineage
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Reuse the complete immutable inventory/terminal/release owner for
|
|
21
|
+
# recovery and finish replay; no current process grants terminality.
|
|
22
|
+
def completion_terminal!(journal:, commit:, params:, map:)
|
|
23
|
+
entries = inventory_index!(journal, commit, params.fetch("mapping_id"), map).select do |entry|
|
|
24
|
+
entry.fetch(:selector).values_at("assignment_id", "attempt_id") == params.values_at("assignment_id", "attempt_id")
|
|
25
|
+
end
|
|
26
|
+
raise AttemptErrors::NotFound, "Canonical attempt is missing" if entries.empty?
|
|
27
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical attempt is ambiguous" unless entries.one?
|
|
28
|
+
inventory_row!(journal, commit, entries.first)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def recovery_observation!(journal:, events:, params:, map:, commit:)
|
|
32
|
+
state = origin(events, **params.slice("assignment_id", "attempt_id", "mapping_id").transform_keys(&:to_sym))
|
|
33
|
+
return {"liveness" => "unknown"} unless %w[bound issued].include?(state["phase"])
|
|
34
|
+
original, original_map = inventory_original_deployment!(journal, events, params.fetch("mapping_id"), map)
|
|
35
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: original_map.fetch("project_id"),
|
|
36
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
37
|
+
return {"liveness" => "unknown"} if lineage.sealed?
|
|
38
|
+
# Disposable local cache is not a checkpoint authority. Authenticate
|
|
39
|
+
# the retained original prepared selection with the existing owner.
|
|
40
|
+
if state.fetch("phase") == "issued"
|
|
41
|
+
original_prepared_registration!(journal: journal, params: params, commit: commit)
|
|
42
|
+
else
|
|
43
|
+
original_bound_prepared_registration!(journal: journal, params: params, commit: commit)
|
|
44
|
+
end
|
|
45
|
+
Molecules::AttemptReconciler.protected_observation(kernel: @kernel, binding: state.fetch("process_binding"),
|
|
46
|
+
observer: maintenance_scope_observer_for(original, params.fetch("mapping_id")), lineage: lineage)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "ace/review/organisms/campaign_manager"
|
|
3
|
+
require_relative "campaign_consumer_policy"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Authority
|
|
8
|
+
class LaunchLifecycle
|
|
9
|
+
# Called by the terminal owner at its selected CAS prefix. Discover
|
|
10
|
+
# children from canonical registration; no caller list can omit one.
|
|
11
|
+
def campaign_children_settled!(journal:, commit:, params:, map:)
|
|
12
|
+
parent = preview_attempt_definition!(journal: journal, commit: commit, params: params, map: map)
|
|
13
|
+
return true unless parent.review_campaign
|
|
14
|
+
inventory = journal.canonical_event_inventory!(commit: commit)
|
|
15
|
+
inventory.fetch("events").each do |assignment, events|
|
|
16
|
+
next if assignment == params.fetch("assignment_id")
|
|
17
|
+
registrations = events.select { |event| event.dig("payload", "operation") == "register_assignment" }
|
|
18
|
+
next if registrations.empty?
|
|
19
|
+
registration = registrations.last.fetch("payload").fetch("data")
|
|
20
|
+
child = inventory_definition!(journal, commit,
|
|
21
|
+
{selector: {"assignment_id" => assignment, "attempt_id" => nil}, registration: registration})
|
|
22
|
+
execution = child.campaign_execution
|
|
23
|
+
next unless execution && execution.values_at("parent_assignment_id", "parent_attempt_id") ==
|
|
24
|
+
params.values_at("assignment_id", "attempt_id")
|
|
25
|
+
original = control_original_descriptor!(registration.fetch("lifecycle_control"))
|
|
26
|
+
mapping_id = registration.fetch("mapping_id")
|
|
27
|
+
child_map = original.mapping(mapping_id)
|
|
28
|
+
authenticate_inventory_registration!(journal, assignment, events, registration, mapping_id,
|
|
29
|
+
child_map, commit, inventory.fetch("introductions").fetch(assignment))
|
|
30
|
+
rows = inventory_index!(journal, commit, mapping_id, child_map).select { |entry|
|
|
31
|
+
entry.fetch(:selector).fetch("assignment_id") == assignment }
|
|
32
|
+
unless !rows.empty? && rows.all? { |entry|
|
|
33
|
+
row = inventory_row!(journal, commit, entry)
|
|
34
|
+
%w[succeeded failed stopped].include?(row["canonical_state"]) &&
|
|
35
|
+
row["terminal_event_id"] && row["reservation_release_event_id"] }
|
|
36
|
+
raise AttemptErrors::Conflict, "campaign parent has an unsettled registered child"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
true
|
|
40
|
+
rescue KeyError, TypeError
|
|
41
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child completion provenance is unavailable"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Resolve both immutable canonical owners before entering exclusions.
|
|
47
|
+
def campaign_child_context!(params, context, map, journal)
|
|
48
|
+
value = if params.key?("definition_bytes")
|
|
49
|
+
JSON.parse(params.fetch("definition_bytes"))
|
|
50
|
+
elsif context.fetch(:registration)
|
|
51
|
+
registration = context.fetch(:registration)
|
|
52
|
+
bytes = journal.blob(registration.fetch("definition_ref"), commit: context.fetch(:commit))
|
|
53
|
+
unless bytes.is_a?(String) && bytes.bytesize <= 32_768 &&
|
|
54
|
+
Digest::SHA256.hexdigest(bytes) == registration.fetch("definition_digest")
|
|
55
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign definition content differs"
|
|
56
|
+
end
|
|
57
|
+
decoded = JSON.parse(bytes, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
|
|
58
|
+
return context unless decoded.is_a?(Hash) && decoded.key?("campaign_execution")
|
|
59
|
+
inventory_definition!(journal, context.fetch(:commit),
|
|
60
|
+
{selector: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => params["attempt_id"]},
|
|
61
|
+
registration: registration}).to_h
|
|
62
|
+
else
|
|
63
|
+
return context
|
|
64
|
+
end
|
|
65
|
+
return context unless value.key?("campaign_execution")
|
|
66
|
+
execution = value.fetch("campaign_execution")
|
|
67
|
+
unless execution.is_a?(Hash) && execution.keys.sort == CampaignExecution::FIELDS
|
|
68
|
+
raise ArgumentError, "campaign execution fields differ"
|
|
69
|
+
end
|
|
70
|
+
parent_id, attempt = execution.values_at("parent_assignment_id", "parent_attempt_id")
|
|
71
|
+
token!(parent_id); token!(attempt)
|
|
72
|
+
raise ArgumentError, "campaign child cannot be its own parent" if parent_id == params.fetch("assignment_id")
|
|
73
|
+
inventory = journal.canonical_event_inventory!(commit: context.fetch(:commit))
|
|
74
|
+
reserves = inventory.fetch("events").fetch(parent_id).select { |event|
|
|
75
|
+
event["attempt_id"] == attempt && event.dig("payload", "operation") == "reserve_attempt" }
|
|
76
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent reservation differs" unless reserves.one?
|
|
77
|
+
parent_mapping = reserves.first.fetch("payload").fetch("data").fetch("mapping_id")
|
|
78
|
+
parent_params = {"assignment_id" => parent_id, "attempt_id" => attempt, "mapping_id" => parent_mapping,
|
|
79
|
+
"scope" => execution.fetch("parent_scope"), "base" => execution.fetch("base"), "head" => execution.fetch("head"),
|
|
80
|
+
"candidate_generation" => execution.fetch("parent_candidate_generation")}.freeze
|
|
81
|
+
parent_map = @deployment.mapping(parent_mapping)
|
|
82
|
+
parent_context = control_registration_context!(parent_params, parent_map, journal, commit: context.fetch(:commit))
|
|
83
|
+
original_map = parent_context.fetch(:map)
|
|
84
|
+
parent = preview_attempt_definition!(journal: journal, commit: context.fetch(:commit), params: parent_params, map: original_map)
|
|
85
|
+
selected = CampaignExecution.validate_parent!(parent.review_campaign)
|
|
86
|
+
unless value["parent"] == parent_id && value["task_id"] == parent.task_id && value["project_id"] == parent.project_id &&
|
|
87
|
+
parent.project_id == map.fetch("project_id") &&
|
|
88
|
+
parent_context.fetch(:selection).fetch("root_identity") == context.fetch(:selection).fetch("root_identity") &&
|
|
89
|
+
original_map.fetch("authority_id") == context.fetch(:map).fetch("authority_id")
|
|
90
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent task or control owner differs"
|
|
91
|
+
end
|
|
92
|
+
CampaignExecution.validate!(execution, parent: parent_id, policy: selected.fetch("policy"))
|
|
93
|
+
context.merge(campaign: {params: parent_params, map: parent_map, context: parent_context,
|
|
94
|
+
definition: parent, selected: selected, execution: execution, child_assignment: params.fetch("assignment_id")}.freeze).freeze
|
|
95
|
+
rescue KeyError, TypeError
|
|
96
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent canonical registration is unavailable"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def recheck_campaign_parent!(context, journal, commit: journal.ref_value)
|
|
100
|
+
campaign = context.fetch(:campaign)
|
|
101
|
+
parent = campaign.fetch(:context)
|
|
102
|
+
recheck_control_registration!(parent, campaign.fetch(:params), campaign.fetch(:map), journal,
|
|
103
|
+
commit: commit, held_exclusion: context.fetch(:exclusion))
|
|
104
|
+
unless @result_owner&.respond_to?(:campaign_parent_candidate!)
|
|
105
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent candidate owner is unavailable"
|
|
106
|
+
end
|
|
107
|
+
@result_owner.campaign_parent_candidate!(journal: journal, commit: commit,
|
|
108
|
+
params: campaign.fetch(:params), map: parent.fetch(:map))
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Called inside the existing lifecycle exclusions and authority mutex,
|
|
113
|
+
# before entering the journal mutation; the store lock spans CAS retries.
|
|
114
|
+
def with_parent_campaign_registration(params, context, replay:)
|
|
115
|
+
# A stored mutation remains a historical observation after supersession.
|
|
116
|
+
# The journal still verifies its operation and exact parameters below.
|
|
117
|
+
return yield if replay
|
|
118
|
+
if context[:campaign]
|
|
119
|
+
return with_child_campaign_registration(params, context) { |guard| yield guard }
|
|
120
|
+
end
|
|
121
|
+
return yield unless params.key?("definition_bytes")
|
|
122
|
+
definition = JSON.parse(params.fetch("definition_bytes"))
|
|
123
|
+
return yield unless definition.key?("review_campaign")
|
|
124
|
+
selected = CampaignExecution.validate_parent!(definition.fetch("review_campaign"))
|
|
125
|
+
manager = campaign_manager_for!(context)
|
|
126
|
+
manager.with_campaign_registration!(selected.fetch("campaign_id"), subject: selected.fetch("subject"),
|
|
127
|
+
contract_identity: selected.fetch("contract_identity"), policy: selected.fetch("policy")) { yield }
|
|
128
|
+
rescue KeyError, TypeError, JSON::ParserError, Ace::Review::Atoms::CampaignContract::Invalid
|
|
129
|
+
raise AttemptErrors::EvidenceUnavailable, "registered parent campaign is unavailable"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def campaign_manager_for!(context, **evidence)
|
|
133
|
+
descriptor = context.fetch(:descriptor)
|
|
134
|
+
map = context.fetch(:map)
|
|
135
|
+
project = descriptor.project(map.fetch("project_id"))
|
|
136
|
+
service = descriptor.authority(map.fetch("authority_id"))
|
|
137
|
+
%w[campaign_repository campaign_store_root].each do |key|
|
|
138
|
+
path = project.fetch(key)
|
|
139
|
+
wire.root_path!(path, directory: true, owner: service.fetch("uid"))
|
|
140
|
+
unless (File.stat(path).mode & 0o077).zero?
|
|
141
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign owner root must remain private"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
store = Ace::Review::Molecules::CampaignStore.new(root: project.fetch("campaign_store_root"))
|
|
145
|
+
Ace::Review::Organisms::CampaignManager.new(repo_root: project.fetch("campaign_repository"), store: store, **evidence)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def with_child_campaign_registration(params, context)
|
|
149
|
+
campaign = context.fetch(:campaign)
|
|
150
|
+
recheck_campaign_parent!(context, journals.fetch(context.fetch(:map).fetch("project_id")))
|
|
151
|
+
manager = campaign_manager_for!(campaign.fetch(:context))
|
|
152
|
+
deployment = @deployment
|
|
153
|
+
descriptor_sha = protected_descriptor_sha256!
|
|
154
|
+
policy_ref = deployment.project(context.fetch(:map).fetch("project_id")).fetch("campaign_policy")
|
|
155
|
+
CampaignConsumerPolicy.new.with(policy_ref) do |profiles, artifacts|
|
|
156
|
+
manager.with_execution_round!(campaign.fetch(:execution).fetch("campaign_id"),
|
|
157
|
+
round_id: campaign.fetch(:execution).fetch("round_id"), consumer_profiles: profiles) do |round, round_guard|
|
|
158
|
+
selected = campaign.fetch(:selected)
|
|
159
|
+
unless round.slice("campaign_id", "subject", "contract_identity", "policy") == selected.reject { |key, _| key == "version" }
|
|
160
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child differs from original parent contract"
|
|
161
|
+
end
|
|
162
|
+
CampaignExecution.validate_round!(campaign.fetch(:execution), parent: campaign.fetch(:definition).id, round: round)
|
|
163
|
+
guard = lambda do |journal, commit|
|
|
164
|
+
unless @deployment.equal?(deployment) && protected_descriptor_sha256! == descriptor_sha
|
|
165
|
+
raise AttemptErrors::Conflict, "current campaign policy descriptor changed"
|
|
166
|
+
end
|
|
167
|
+
artifacts.verify_unchanged!
|
|
168
|
+
round_guard.call
|
|
169
|
+
recheck_campaign_parent!(context, journal, commit: commit)
|
|
170
|
+
unique_campaign_child!(context, journal, commit) if params.key?("definition_bytes")
|
|
171
|
+
end
|
|
172
|
+
yield guard
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
rescue KeyError, TypeError, Ace::Review::Atoms::CampaignContract::Invalid, Ace::Runtime::RuntimeUnavailableError
|
|
176
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child pinned round is unavailable"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# One immutable phase definition, discovered from the existing canonical
|
|
180
|
+
# registration owner. A caller-provided child list cannot omit a sibling.
|
|
181
|
+
def unique_campaign_child!(context, journal, commit)
|
|
182
|
+
selected = context.fetch(:campaign).fetch(:execution)
|
|
183
|
+
fields = %w[parent_assignment_id parent_attempt_id parent_candidate_generation round_id phase]
|
|
184
|
+
fields += case selected.fetch("phase")
|
|
185
|
+
when "collection" then ["review_scope"]
|
|
186
|
+
when "check" then ["check_name"]
|
|
187
|
+
else []
|
|
188
|
+
end
|
|
189
|
+
inventory = journal.canonical_event_inventory!(commit: commit)
|
|
190
|
+
inventory.fetch("events").each do |assignment, events|
|
|
191
|
+
next if assignment == context.fetch(:campaign).fetch(:definition).id
|
|
192
|
+
events.select { |event| event.dig("payload", "operation") == "register_assignment" }.each do |event|
|
|
193
|
+
registration = event.fetch("payload").fetch("data")
|
|
194
|
+
entry = {selector: {"assignment_id" => assignment, "attempt_id" => nil}, registration: registration}
|
|
195
|
+
definition = inventory_definition!(journal, commit, entry)
|
|
196
|
+
child = definition.campaign_execution
|
|
197
|
+
next unless child && child.slice(*fields) == selected.slice(*fields)
|
|
198
|
+
# Re-registration of this exact child is permitted; a distinct
|
|
199
|
+
# assignment cannot replace the retained phase owner.
|
|
200
|
+
current_assignment = context.fetch(:campaign).fetch(:child_assignment)
|
|
201
|
+
next if assignment == current_assignment
|
|
202
|
+
original = control_original_descriptor!(registration.fetch("lifecycle_control"))
|
|
203
|
+
map_id = registration.fetch("mapping_id")
|
|
204
|
+
authenticate_inventory_registration!(journal, assignment, events, registration, map_id,
|
|
205
|
+
original.mapping(map_id), commit, inventory.fetch("introductions").fetch(assignment))
|
|
206
|
+
raise AttemptErrors::Conflict, "campaign phase already has a registered child"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
true
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|