ace-assign 0.71.0 → 0.73.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 +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +3 -2119
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -4
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +4 -176
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
- data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/command_execution.rb +0 -180
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -457
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -41
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
- data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
- data/lib/ace/assign/authority/ordinary_results.rb +0 -206
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
- data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -309
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
- data/lib/ace/assign/authority/retained_input.rb +0 -53
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -189
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "service_evidence"
|
|
4
|
-
require_relative "../molecules/ordinary_merge_contract"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# Completion reads the existing effect owners at the held CAS prefix.
|
|
10
|
-
# A prompt, acknowledgment or failed dispatch never settles an effect.
|
|
11
|
-
class OrdinaryEffectSettlement
|
|
12
|
-
def initialize(journal:)
|
|
13
|
-
@journal = journal
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def verify!(project:, assignment:, attempt:, events:, commit:)
|
|
17
|
-
retained = @journal.read_events(assignment, commit: commit).select { |event| event["attempt_id"] == attempt }
|
|
18
|
-
unless retained == events && Models::EvidenceEvent.chain_valid?(events)
|
|
19
|
-
unavailable!("ordinary settlement needs its exact canonical attempt")
|
|
20
|
-
end
|
|
21
|
-
selection = {"project_id" => project, "assignment_id" => assignment, "attempt_id" => attempt}
|
|
22
|
-
services = records!(@journal.service_request_records(commit: commit), events, selection, %w[service_claim service_transition])
|
|
23
|
-
merges = records!(@journal.fixed_merge_request_records(commit: commit), events, selection, %w[merge_admission merge_transition])
|
|
24
|
-
services.each { |record| service!(record, commit) }
|
|
25
|
-
merges.each { |record| merge!(record, commit) }
|
|
26
|
-
assignment_events = @journal.read_events(assignment, commit: commit)
|
|
27
|
-
assignment_events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
|
|
28
|
-
unavailable!("ordinary delivery history is corrupt") unless Models::EvidenceEvent.chain_valid?(chain)
|
|
29
|
-
end
|
|
30
|
-
delivery!(events, merges, assignment_events)
|
|
31
|
-
{"journal_commit" => commit, "service_requests" => services.map { |record| record.fetch("request_id") }.sort,
|
|
32
|
-
"merge_requests" => merges.map { |record| record.fetch("request_id") }.sort}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def records!(inventory, events, selection, types)
|
|
38
|
-
records = inventory.select { |record| record.values_at("assignment_id", "attempt_id") == selection.values_at("assignment_id", "attempt_id") }
|
|
39
|
-
references = events.select { |event| types.include?(event["type"]) }
|
|
40
|
-
ids = references.map { |event| event.fetch("payload").fetch("request_id") }.uniq.sort
|
|
41
|
-
unless records.map { |record| record.fetch("request_id") }.sort == ids
|
|
42
|
-
unavailable!("ordinary effect inventory and event set differ")
|
|
43
|
-
end
|
|
44
|
-
records.each do |record|
|
|
45
|
-
latest = references.reverse.find { |event| event.dig("payload", "request_id") == record.fetch("request_id") }
|
|
46
|
-
unless record.slice(*selection.keys) == selection && latest.dig("payload", "state") == record["state"] &&
|
|
47
|
-
latest.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(record) &&
|
|
48
|
-
latest.dig("payload", "receipt_digest") == (record["receipt"] && Atoms::EvidenceDigest.digest(record["receipt"]))
|
|
49
|
-
unavailable!("ordinary effect record differs from its accepted owner event")
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
records
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def service!(record, commit)
|
|
56
|
-
return if record["state"] == "rejected" && record["consumed"] == false
|
|
57
|
-
pending!("ordinary service effect is unresolved") unless %w[succeeded failed-settled].include?(record["state"])
|
|
58
|
-
receipt = record.fetch("receipt")
|
|
59
|
-
@journal.validate_terminal_receipt_structure!(record, record.fetch("state"), receipt)
|
|
60
|
-
# The installed owner consumes the existing canonical import, never
|
|
61
|
-
# the old local receipt file. This is independent of journal mode.
|
|
62
|
-
owner = ServiceEvidence.new(journal: @journal)
|
|
63
|
-
references = receipt.fetch("evidence")
|
|
64
|
-
if %w[publish prune-preserved-workspace].include?(record["operation"])
|
|
65
|
-
contents = owner.call(references, record, record.fetch("state"), {commit: commit})
|
|
66
|
-
unless contents.is_a?(Array) && contents.size == references.size
|
|
67
|
-
unavailable!("ordinary service evidence collection differs")
|
|
68
|
-
end
|
|
69
|
-
else
|
|
70
|
-
contents = references.map { |reference| owner.call(reference, record, record.fetch("state"), {commit: commit}) }
|
|
71
|
-
end
|
|
72
|
-
references.zip(contents).each do |reference, bytes|
|
|
73
|
-
unavailable!("ordinary service evidence digest differs") unless bytes.is_a?(String) && Digest::SHA256.hexdigest(bytes.b) == reference.fetch("sha256")
|
|
74
|
-
@journal.verify_service_attestation!(bytes, record, record.fetch("state")) unless %w[publish prune-preserved-workspace].include?(record["operation"])
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def merge!(record, commit)
|
|
79
|
-
unless @journal.fixed_merge_settlement!(record.fetch("request_id"), commit: commit) == record
|
|
80
|
-
unavailable!("ordinary merge settlement differs from its selected record")
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def delivery!(events, merges, assignment_events)
|
|
85
|
-
deliveries = assignment_events.select { |event| event["type"] == "delivery" }
|
|
86
|
-
events.select { |event| event["type"] == "delivery" }.each do |intent|
|
|
87
|
-
payload = intent.fetch("payload")
|
|
88
|
-
case payload["stage"]
|
|
89
|
-
when "intent"
|
|
90
|
-
settled = deliveries.any? { |event| event.dig("payload", "stage") == "result" &&
|
|
91
|
-
event.dig("payload", "intent_digest") == intent.fetch("digest") &&
|
|
92
|
-
event.dig("payload", "intent_attempt_id") == intent.fetch("attempt_id") &&
|
|
93
|
-
event.dig("payload", "operation") == payload.fetch("operation") &&
|
|
94
|
-
event.dig("payload", "head") == payload.fetch("head") && event.dig("payload", "outcome") == "succeeded" }
|
|
95
|
-
pending!("ordinary delivery intent is unresolved") unless settled
|
|
96
|
-
when "ordinary-merge-intent"
|
|
97
|
-
result = deliveries.reverse.find { |event| event.dig("payload", "stage") == "ordinary-merge-result" &&
|
|
98
|
-
event.dig("payload", "request_id") == payload.fetch("request_id") && event.dig("payload", "input_digest") == payload.fetch("input_digest") }
|
|
99
|
-
pending!("ordinary merge delivery is unresolved") unless result
|
|
100
|
-
accepted = result.fetch("payload")
|
|
101
|
-
record = merges.find { |item| item["request_id"] == payload.fetch("request_id") }
|
|
102
|
-
if accepted["outcome"] == "refused_no_effect" && record.nil?
|
|
103
|
-
refusal = accepted.slice("code", "message", "request_id", "input_digest").merge("schema" => "ace.lab.service.ordinary-merge-refusal/v1")
|
|
104
|
-
Molecules::OrdinaryMergeContract.validate_refusal!(refusal)
|
|
105
|
-
pending!("ordinary merge receiver availability is unresolved") if refusal["code"] == Molecules::OrdinaryMergeContract::UNAVAILABLE_CODE
|
|
106
|
-
elsif !record || record["input_digest"] != payload.fetch("input_digest") || record["receipt"] != accepted["receipt"]
|
|
107
|
-
unavailable!("ordinary delivery result differs from the merge owner")
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def unavailable!(message) = raise AttemptErrors::EvidenceUnavailable, message
|
|
114
|
-
def pending!(message) = raise AttemptErrors::ServiceSettlementPending, message
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/herdr"
|
|
4
|
-
require_relative "prepared_work"
|
|
5
|
-
require_relative "assignment_definition"
|
|
6
|
-
require_relative "ordinary_queue"
|
|
7
|
-
require_relative "ordinary_attempt_start"
|
|
8
|
-
require_relative "ordinary_progress"
|
|
9
|
-
require_relative "ordinary_results"
|
|
10
|
-
require_relative "ordinary_completion"
|
|
11
|
-
require_relative "ordinary_supervisor"
|
|
12
|
-
require_relative "ordinary_reviews"
|
|
13
|
-
require_relative "ordinary_merge_evidence"
|
|
14
|
-
require_relative "ordinary_release_artifacts"
|
|
15
|
-
require_relative "../molecules/lifecycle_exclusion"
|
|
16
|
-
|
|
17
|
-
module Ace
|
|
18
|
-
module Assign
|
|
19
|
-
module Authority
|
|
20
|
-
# Ordinary lifecycle uses the existing canonical execution ref and CAS.
|
|
21
|
-
# The installed Lab composition supplies one journal and exclusion owner
|
|
22
|
-
# shared with HITL and merge. No requester cache supplies accepted state.
|
|
23
|
-
class OrdinaryLifecycle
|
|
24
|
-
include OrdinaryAttemptStart
|
|
25
|
-
include OrdinaryProgress
|
|
26
|
-
include OrdinaryResults
|
|
27
|
-
include OrdinaryCompletion
|
|
28
|
-
include OrdinarySupervisor
|
|
29
|
-
include OrdinaryReviews
|
|
30
|
-
include OrdinaryMergeEvidence
|
|
31
|
-
include OrdinaryReleaseArtifacts
|
|
32
|
-
OPERATIONS = (%w[register_assignment registration_status start_attempt finish merge_evidence] + OrdinaryProgress::PROGRESS_OPERATIONS + OrdinaryResults::RESULT_OPERATIONS + OrdinarySupervisor::SUPERVISOR_OPERATIONS + OrdinaryReviews::REVIEW_OPERATIONS + OrdinaryReleaseArtifacts::RELEASE_OPERATIONS).freeze
|
|
33
|
-
TRANSFER_OPERATIONS = {"register_assignment" => {direction: :upload, purpose: :candidate, roles: [:supervisor]}}
|
|
34
|
-
.merge(OrdinaryProgress::PROGRESS_TRANSFERS).merge(OrdinaryResults::RESULT_TRANSFERS).merge(OrdinaryReviews::REVIEW_TRANSFERS).merge(OrdinaryReleaseArtifacts::RELEASE_TRANSFERS).freeze
|
|
35
|
-
REGISTER_KEYS = %w[assignment_id definition_digest prepared_head prepared_tree manifest_sha256 expected_generation pane transfer].freeze
|
|
36
|
-
|
|
37
|
-
def initialize(project:, admission:, journal:, exclusion:, input_root:, inbox: nil, command_evidence: nil, release_authorizer: nil)
|
|
38
|
-
@project, @admission, @journal, @exclusion = project, admission, journal, exclusion
|
|
39
|
-
@input_root = PrivateDirectory.verify!(input_root)
|
|
40
|
-
unless inbox.nil? || inbox.is_a?(Ace::Herdr::Organisms::Inbox)
|
|
41
|
-
raise ArgumentError, "ordinary completion requires the installed Inbox owner"
|
|
42
|
-
end
|
|
43
|
-
unless release_authorizer.nil? || release_authorizer.respond_to?(:call)
|
|
44
|
-
raise ArgumentError, "release artifact authorization requires its installed policy owner"
|
|
45
|
-
end
|
|
46
|
-
@inbox, @command_evidence, @release_authorizer = inbox, command_evidence, release_authorizer
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def authorize_transfer!(request:, peer:, role:, deadline:)
|
|
50
|
-
authorize!(request, peer, role, deadline)
|
|
51
|
-
if OrdinaryReleaseArtifacts::RELEASE_TRANSFERS.key?(request.fetch("operation"))
|
|
52
|
-
authorize_ordinary_release!(request, peer, role, deadline)
|
|
53
|
-
return
|
|
54
|
-
end
|
|
55
|
-
if OrdinaryReviews::REVIEW_TRANSFERS.key?(request.fetch("operation"))
|
|
56
|
-
authorize_ordinary_review!(request, peer, role, deadline)
|
|
57
|
-
return
|
|
58
|
-
end
|
|
59
|
-
if OrdinaryResults::RESULT_TRANSFERS.key?(request.fetch("operation"))
|
|
60
|
-
validate_result!(request)
|
|
61
|
-
authorize_start_replay!(request, peer, role, deadline) if request.fetch("operation") == "submit_result"
|
|
62
|
-
return
|
|
63
|
-
end
|
|
64
|
-
if OrdinaryProgress::PROGRESS_TRANSFERS.key?(request.fetch("operation"))
|
|
65
|
-
validate_progress!(request)
|
|
66
|
-
return
|
|
67
|
-
end
|
|
68
|
-
unless request.fetch("operation") == "register_assignment" && role == :supervisor
|
|
69
|
-
raise AttemptErrors::UnauthorizedIdentity, "ordinary registration requires the installed supervisor"
|
|
70
|
-
end
|
|
71
|
-
validate_registration!(request)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def dispatch(request:, peer:, role:, deadline:, transfer: nil)
|
|
75
|
-
authorize!(request, peer, role, deadline)
|
|
76
|
-
return ordinary_review_operation(request, peer, role, deadline, transfer) if OrdinaryReviews::REVIEW_OPERATIONS.include?(request.fetch("operation"))
|
|
77
|
-
case request.fetch("operation")
|
|
78
|
-
when "retain_release_artifact"
|
|
79
|
-
retain_release_artifact(request, peer, role, deadline, transfer)
|
|
80
|
-
when "merge_evidence"
|
|
81
|
-
raise ArgumentError, "merge evidence read forbids transfer" if transfer
|
|
82
|
-
ordinary_merge_evidence(request, peer, role, deadline)
|
|
83
|
-
when "supervisor_status", "adopt_supervisor"
|
|
84
|
-
raise ArgumentError, "ordinary supervisor operation forbids transfer" if transfer
|
|
85
|
-
supervisor_operation(request, peer, role, deadline)
|
|
86
|
-
when "registration_status"
|
|
87
|
-
strict!(request.fetch("params"), %w[assignment_id])
|
|
88
|
-
raise ArgumentError, "status mutation must be null" unless request.fetch("mutation_id").nil?
|
|
89
|
-
assignment = request.fetch("params").fetch("assignment_id")
|
|
90
|
-
token!(assignment)
|
|
91
|
-
commit = @journal.ref_value
|
|
92
|
-
data = registration(assignment, commit: commit) || {"assignment_id" => assignment, "generation" => 0}
|
|
93
|
-
{data: data.merge("journal_commit" => commit), replayed: false}
|
|
94
|
-
when "register_assignment"
|
|
95
|
-
authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
|
|
96
|
-
register(request, peer, deadline, transfer)
|
|
97
|
-
when "start_attempt"
|
|
98
|
-
raise ArgumentError, "ordinary start forbids transfer" if transfer
|
|
99
|
-
start_attempt(request, peer, role, deadline)
|
|
100
|
-
when "assignment_snapshot"
|
|
101
|
-
raise ArgumentError, "ordinary snapshot forbids upload" if transfer
|
|
102
|
-
assignment_snapshot(request, peer, role, deadline)
|
|
103
|
-
when "queue_start", "queue_finish", "queue_fail"
|
|
104
|
-
progress(request, peer, role, deadline, transfer)
|
|
105
|
-
when "submit_result", "result_status", "result_snapshot"
|
|
106
|
-
ordinary_result(request, peer, role, deadline, transfer)
|
|
107
|
-
when "finish"
|
|
108
|
-
raise ArgumentError, "ordinary completion forbids transfer" if transfer
|
|
109
|
-
finish_result(request, peer, role, deadline)
|
|
110
|
-
else
|
|
111
|
-
raise ArgumentError, "unknown ordinary lifecycle operation"
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
def register(request, peer, deadline, transfer)
|
|
118
|
-
params = request.fetch("params")
|
|
119
|
-
# Native observation and candidate subprocesses happen before CAS.
|
|
120
|
-
identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
|
|
121
|
-
unless identity.role == "coordinator"
|
|
122
|
-
raise AttemptErrors::UnauthorizedIdentity, "ordinary registration native owner differs"
|
|
123
|
-
end
|
|
124
|
-
raise AttemptErrors::MalformedTransfer, "prepared input requires one bundle" unless transfer && transfer.count == 1
|
|
125
|
-
bytes = transfer.bytes.freeze
|
|
126
|
-
descriptor = params.fetch("transfer")
|
|
127
|
-
work = PreparedWork.admit(bytes: bytes, head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree"),
|
|
128
|
-
sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), root: @input_root, deadline: deadline)
|
|
129
|
-
unless work.manifest.values_at("assignment_id", "project_id") == [params.fetch("assignment_id"), @project] &&
|
|
130
|
-
work.manifest_sha256 == params.fetch("manifest_sha256")
|
|
131
|
-
raise ArgumentError, "ordinary prepared assignment association differs"
|
|
132
|
-
end
|
|
133
|
-
definition = work.definition_bytes(head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree"))
|
|
134
|
-
assignment = AssignmentDefinition.parse(bytes: definition, digest: params.fetch("definition_digest"),
|
|
135
|
-
assignment_id: params.fetch("assignment_id"), project_id: @project)
|
|
136
|
-
# Campaign registration needs its existing canonical campaign owner;
|
|
137
|
-
# it cannot be manufactured by attaching unchecked parent metadata.
|
|
138
|
-
if assignment.review_campaign || assignment.campaign_execution
|
|
139
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary campaign registration requires its campaign owner"
|
|
140
|
-
end
|
|
141
|
-
principal = {"uid" => peer.fetch("uid"), "actor" => identity.actor}
|
|
142
|
-
queue = OrdinaryQueue.new(work: work, root: @input_root)
|
|
143
|
-
queue_sha = Digest::SHA256.hexdigest(queue.bytes)
|
|
144
|
-
digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "principal" => principal})
|
|
145
|
-
keys = [@exclusion.task_key(assignment.task_id), @exclusion.assignment_key(assignment.id)]
|
|
146
|
-
@exclusion.with_shared_multi(keys, deadline: deadline) do
|
|
147
|
-
# Replay bypasses the CAS callback; current authority is mandatory
|
|
148
|
-
# before that lookup as well as on every fresh CAS attempt.
|
|
149
|
-
authorize!(request, peer, :supervisor, deadline)
|
|
150
|
-
@journal.mutate(assignment_id: assignment.id, attempt_id: "definition-#{assignment.id}",
|
|
151
|
-
mutation_id: request.fetch("mutation_id"), operation: "register_assignment", parameters_digest: digest,
|
|
152
|
-
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
153
|
-
replay_authorizer: -> { authorize!(request, peer, :supervisor, deadline) }) do |events, commit, _generation|
|
|
154
|
-
authorize!(request, peer, :supervisor, deadline)
|
|
155
|
-
unless events.empty? && registration(assignment.id, commit: commit).nil?
|
|
156
|
-
raise AttemptErrors::Conflict, "ordinary assignment is already registered; retain its original definition"
|
|
157
|
-
end
|
|
158
|
-
bundle_sha = Digest::SHA256.hexdigest(bytes)
|
|
159
|
-
definition_path = "execution/definitions/#{assignment.id}-#{params.fetch('definition_digest')}.json"
|
|
160
|
-
bundle_path = "execution/prepared/#{assignment.id}-#{bundle_sha}.bundle"
|
|
161
|
-
queue_path = "execution/definitions/#{assignment.id}-queue-#{queue_sha}.json"
|
|
162
|
-
{events: [], blobs: {definition_path => definition, bundle_path => bytes, queue_path => queue.bytes}, data: {
|
|
163
|
-
"authority" => "ordinary", "assignment_id" => assignment.id, "project_id" => @project,
|
|
164
|
-
"task_id" => assignment.task_id, "registered_by" => principal, "phase" => "registered",
|
|
165
|
-
"supervisor_binding" => identity.runtime_binding,
|
|
166
|
-
"definition_ref" => definition_path, "definition_digest" => params.fetch("definition_digest"),
|
|
167
|
-
"queue_ref" => queue_path, "queue_sha256" => queue_sha,
|
|
168
|
-
"prepared_work" => work.reference(head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree")),
|
|
169
|
-
"prepared_bundle_ref" => bundle_path, "prepared_bundle_sha256" => bundle_sha,
|
|
170
|
-
"prepared_bundle_bytes" => bytes.bytesize}}
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def registration(assignment, commit:)
|
|
176
|
-
return nil unless commit
|
|
177
|
-
events = @journal.read_events(assignment, commit: commit)
|
|
178
|
-
events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
|
|
179
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary assignment evidence is corrupt" unless Models::EvidenceEvent.chain_valid?(chain)
|
|
180
|
-
end
|
|
181
|
-
registrations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "register_assignment" }
|
|
182
|
-
return nil if registrations.empty?
|
|
183
|
-
data = registrations.one? && registrations.first.dig("payload", "data")
|
|
184
|
-
unless data && data.values_at("authority", "assignment_id", "project_id") == ["ordinary", assignment, @project]
|
|
185
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary assignment registration owner differs"
|
|
186
|
-
end
|
|
187
|
-
data
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def authorize!(request, peer, role, deadline)
|
|
191
|
-
unless deadline.is_a?(Numeric) && deadline.finite? && deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
192
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary lifecycle deadline elapsed"
|
|
193
|
-
end
|
|
194
|
-
unless request.fetch("project_id") == @project && @admission.authorize!(request: request, peer: peer, deadline: deadline) == role
|
|
195
|
-
raise AttemptErrors::UnauthorizedIdentity, "ordinary lifecycle principal differs"
|
|
196
|
-
end
|
|
197
|
-
# Project commands share the canonical journal but never the managed
|
|
198
|
-
# assignment namespace. Reserve their prefix on every ordinary route,
|
|
199
|
-
# including transfer admission, status and registration replay.
|
|
200
|
-
assignment = request.fetch("params")["assignment_id"]
|
|
201
|
-
if assignment.is_a?(String) && assignment.start_with?("command-")
|
|
202
|
-
raise ArgumentError, "ordinary assignment uses the reserved project-command namespace"
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def validate_registration!(request)
|
|
207
|
-
params = request.fetch("params")
|
|
208
|
-
strict!(params, REGISTER_KEYS)
|
|
209
|
-
token!(request.fetch("mutation_id"))
|
|
210
|
-
token!(params.fetch("assignment_id"))
|
|
211
|
-
token!("definition-#{params.fetch('assignment_id')}")
|
|
212
|
-
unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation") >= 0
|
|
213
|
-
raise ArgumentError, "ordinary generation is invalid"
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def strict!(value, keys)
|
|
218
|
-
raise ArgumentError, "ordinary authority fields differ" unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def token!(value)
|
|
222
|
-
raise ArgumentError, "ordinary identity is invalid" unless value.is_a?(String) && Molecules::JournalMutation::ID.match?(value)
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# The installed fixed merge consumes this same lifecycle, never a local
|
|
7
|
-
# receipt file. Locks precede journal CAS; no callback acquires an inverted
|
|
8
|
-
# lifecycle lock while holding the journal writer lock.
|
|
9
|
-
module OrdinaryMergeEvidence
|
|
10
|
-
def with_merge_scope(record:, deadline:, exclusive: false)
|
|
11
|
-
assignment, attempt = record.values_at("assignment_id", "attempt_id")
|
|
12
|
-
token!(assignment)
|
|
13
|
-
token!(attempt)
|
|
14
|
-
registered = registration(assignment, commit: @journal.ref_value)
|
|
15
|
-
raise AttemptErrors::EvidenceUnavailable, "merge assignment is unregistered" unless registered
|
|
16
|
-
@exclusion.with_shared_multi([@exclusion.task_key(registered.fetch("task_id"))], deadline: deadline) do
|
|
17
|
-
key = @exclusion.assignment_key(assignment)
|
|
18
|
-
if exclusive
|
|
19
|
-
@exclusion.with_exclusive(key, deadline: deadline) { yield }
|
|
20
|
-
else
|
|
21
|
-
@exclusion.with_shared_multi([key], deadline: deadline) { yield }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def verify_merge_evidence_at!(record:, commit:, deadline:)
|
|
27
|
-
unless record.fetch("project_id") == @project
|
|
28
|
-
raise AttemptErrors::ReceiptRejected, "merge evidence project differs"
|
|
29
|
-
end
|
|
30
|
-
request = record.fetch("request")
|
|
31
|
-
events = result_events(record, commit)
|
|
32
|
-
current = ordinary_candidate_event(events)&.dig("payload", "data")
|
|
33
|
-
raise AttemptErrors::ReceiptRejected, "merge candidate is unavailable" unless current
|
|
34
|
-
projection = merge_evidence_at!(events: events, commit: commit, head: request.fetch("candidate_head"),
|
|
35
|
-
generation: current.fetch("candidate_generation"), execution_ids: request.fetch("test_evidence").map { |reference| reference.fetch("reference") }, deadline: deadline)
|
|
36
|
-
unless request.fetch("review_evidence") == projection.fetch("review_evidence") && request.fetch("test_evidence") == projection.fetch("test_evidence")
|
|
37
|
-
raise AttemptErrors::ReceiptRejected, "merge requires exact canonical review and executed test references"
|
|
38
|
-
end
|
|
39
|
-
origin = origin_at!(events: events, commit: commit)
|
|
40
|
-
observed = @admission.recorded_execution!(binding: origin.fetch("runtime_binding"), head: request.fetch("candidate_head"), deadline: deadline)
|
|
41
|
-
unless observed.fetch(:runtime_binding) == origin.fetch("runtime_binding")
|
|
42
|
-
raise AttemptErrors::UnauthorizedIdentity, "merge producer native identity changed"
|
|
43
|
-
end
|
|
44
|
-
true
|
|
45
|
-
rescue KeyError, TypeError
|
|
46
|
-
raise AttemptErrors::ReceiptRejected, "canonical merge evidence is incomplete"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def merge_evidence_at!(events:, commit:, head:, generation:, execution_ids:, deadline:)
|
|
50
|
-
unless @command_evidence && @command_evidence.journal.equal?(@journal) && execution_ids.is_a?(Array) &&
|
|
51
|
-
execution_ids.size.between?(1, 16) && execution_ids.uniq == execution_ids
|
|
52
|
-
raise AttemptErrors::EvidenceUnavailable, "merge requires the installed canonical executed-test owner"
|
|
53
|
-
end
|
|
54
|
-
candidate = candidate_at!(events: events, commit: commit, head: head, generation: generation)
|
|
55
|
-
review = review_at!(events: events, commit: commit, head: head, generation: generation)
|
|
56
|
-
tests = execution_ids.map { |id| @command_evidence.test_evidence_at!(execution_id: id, commit: commit,
|
|
57
|
-
head: head, tree: candidate.fetch("tree"), deadline: deadline) }
|
|
58
|
-
{"review_evidence" => {"schema" => "ace.review.acceptance-reference/v1", "reference" => review.fetch("review_id"),
|
|
59
|
-
"reviewer" => review.fetch("reviewer_actor"), "verdict" => "approved", "reviewed_head" => head,
|
|
60
|
-
"recorded_at" => review.fetch("recorded_at"), "record_sha256" => review.fetch("acceptance_event_digest")},
|
|
61
|
-
"test_evidence" => tests}
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def ordinary_merge_evidence(request, peer, role, deadline)
|
|
67
|
-
params = request.fetch("params")
|
|
68
|
-
strict!(params, %w[assignment_id attempt_id head candidate_generation test_execution_ids])
|
|
69
|
-
raise ArgumentError, "merge evidence read mutation must be null" unless request.fetch("mutation_id").nil?
|
|
70
|
-
with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
|
|
71
|
-
prefix = @journal.ref_value
|
|
72
|
-
projection = merge_evidence_at!(events: result_events(params, prefix), commit: prefix, head: params.fetch("head"),
|
|
73
|
-
generation: params.fetch("candidate_generation"), execution_ids: params.fetch("test_execution_ids"), deadline: deadline)
|
|
74
|
-
authorize!(request, peer, role, deadline)
|
|
75
|
-
{data: projection.merge("journal_commit" => prefix), replayed: false}
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "ace/runtime/molecules/process_identity"
|
|
5
|
-
require "ace/herdr/molecules/bounded_observation"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Authority
|
|
10
|
-
# Ordinary UNIX peer attribution. This is not a protected launch observer:
|
|
11
|
-
# it confers no Yama, capability, cgroup or descendant-closure guarantee.
|
|
12
|
-
class OrdinaryPeer
|
|
13
|
-
def initialize(identity: nil)
|
|
14
|
-
@identity = identity
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def capture(socket, deadline: default_deadline)
|
|
18
|
-
identity = identity_observer(deadline)
|
|
19
|
-
uid, gid = socket.getpeereid
|
|
20
|
-
pid = if RUBY_PLATFORM.include?("darwin")
|
|
21
|
-
socket.getsockopt(0, 2).int # SOL_LOCAL / LOCAL_PEERPID
|
|
22
|
-
elsif Socket.const_defined?(:SO_PEERCRED)
|
|
23
|
-
observed_pid, observed_uid, observed_gid = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_PEERCRED).data.unpack("i!3")
|
|
24
|
-
unavailable! unless [observed_uid, observed_gid] == [uid, gid]
|
|
25
|
-
observed_pid
|
|
26
|
-
end
|
|
27
|
-
value = identity.capture(pid)
|
|
28
|
-
unavailable! unless value && value["uid"] == uid && socket.getpeereid == [uid, gid] && identity.capture(pid) == value
|
|
29
|
-
value.merge("gid" => gid).freeze
|
|
30
|
-
rescue SystemCallError, IOError, SocketError, NotImplementedError
|
|
31
|
-
unavailable!
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def live!(peer, deadline: default_deadline)
|
|
35
|
-
unavailable! unless peer.is_a?(Hash) && identity_observer(deadline).capture(peer["pid"]) == peer.slice("pid", "uid", "started_at", "host")
|
|
36
|
-
peer
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
def default_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
42
|
-
|
|
43
|
-
def identity_observer(deadline)
|
|
44
|
-
@identity || Ace::Runtime::Molecules::ProcessIdentity.new(
|
|
45
|
-
executor: Ace::Herdr::Molecules::BoundedObservation.new(deadline: deadline))
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def unavailable!
|
|
49
|
-
raise AttemptErrors::UnauthorizedIdentity, "ordinary kernel peer is unavailable or changed"
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# Source-owned progress for the already accepted work. Queue completion
|
|
7
|
-
# remains a worker report, never a successful execution result or merge.
|
|
8
|
-
module OrdinaryProgress
|
|
9
|
-
PROGRESS_OPERATIONS = %w[queue_start queue_finish queue_fail assignment_snapshot].freeze
|
|
10
|
-
PROGRESS_TRANSFERS = {
|
|
11
|
-
"queue_finish" => {direction: :upload, purpose: :artifacts, roles: %i[worker supervisor]},
|
|
12
|
-
"assignment_snapshot" => {direction: :download, purpose: :assignment_snapshot, roles: %i[worker supervisor reviewer]}
|
|
13
|
-
}.freeze
|
|
14
|
-
PROGRESS_COMMON = %w[assignment_id attempt_id expected_generation pane].freeze
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def validate_progress!(request)
|
|
19
|
-
params = request.fetch("params")
|
|
20
|
-
operation = request.fetch("operation")
|
|
21
|
-
if operation == "assignment_snapshot"
|
|
22
|
-
strict!(params, %w[assignment_id attempt_id kind])
|
|
23
|
-
token!(params.fetch("attempt_id")) unless params.fetch("attempt_id").nil?
|
|
24
|
-
unless request.fetch("mutation_id").nil? && %w[prepared_work queue].include?(params.fetch("kind"))
|
|
25
|
-
raise ArgumentError, "ordinary snapshot must be a read of accepted work or queue"
|
|
26
|
-
end
|
|
27
|
-
else
|
|
28
|
-
fields = case operation
|
|
29
|
-
when "queue_start" then %w[step]
|
|
30
|
-
when "queue_finish" then %w[step transfer]
|
|
31
|
-
when "queue_fail" then %w[message]
|
|
32
|
-
else raise ArgumentError, "unknown ordinary progress operation"
|
|
33
|
-
end
|
|
34
|
-
strict!(params, PROGRESS_COMMON + fields)
|
|
35
|
-
token!(params.fetch("attempt_id")); token!(request.fetch("mutation_id"))
|
|
36
|
-
unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation").positive?
|
|
37
|
-
raise ArgumentError, "ordinary progress generation is invalid"
|
|
38
|
-
end
|
|
39
|
-
if params.key?("step") && !params["step"].nil? &&
|
|
40
|
-
!(params["step"].is_a?(String) && params["step"].bytesize.between?(1, 128) && PreparedWork::SCOPE.match?(params["step"]))
|
|
41
|
-
raise ArgumentError, "ordinary progress step is invalid"
|
|
42
|
-
end
|
|
43
|
-
if params.key?("message") && !(params["message"].is_a?(String) && params["message"].bytesize.between?(1, 1024) && !params["message"].include?("\0"))
|
|
44
|
-
raise ArgumentError, "ordinary failure message is invalid"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
token!(params.fetch("assignment_id"))
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def assignment_snapshot(request, peer, role, deadline)
|
|
51
|
-
validate_progress!(request)
|
|
52
|
-
params = request.fetch("params")
|
|
53
|
-
commit = @journal.ref_value
|
|
54
|
-
registered = registration(params.fetch("assignment_id"), commit: commit)
|
|
55
|
-
raise AttemptErrors::NotFound, "ordinary assignment is not registered" unless registered
|
|
56
|
-
queue_record = current_queue_record(registered, commit)
|
|
57
|
-
selected_events = if params.fetch("attempt_id")
|
|
58
|
-
result_events(params, commit)
|
|
59
|
-
else
|
|
60
|
-
[]
|
|
61
|
-
end
|
|
62
|
-
kind = params.fetch("kind")
|
|
63
|
-
source = kind == "prepared_work" ? registered : queue_record
|
|
64
|
-
path_key, digest_key, size = if kind == "prepared_work"
|
|
65
|
-
["prepared_bundle_ref", "prepared_bundle_sha256", registered.fetch("prepared_bundle_bytes")]
|
|
66
|
-
else
|
|
67
|
-
["queue_ref", "queue_sha256", PreparedWork::MAX_TOTAL]
|
|
68
|
-
end
|
|
69
|
-
bytes = @journal.bounded_blob(source.fetch(path_key), commit: commit, max_bytes: size)
|
|
70
|
-
unless Digest::SHA256.hexdigest(bytes) == source.fetch(digest_key)
|
|
71
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary snapshot bytes differ from accepted selection"
|
|
72
|
-
end
|
|
73
|
-
authorize!(request, peer, role, deadline)
|
|
74
|
-
{data: {"project_id" => @project, "assignment_id" => params.fetch("assignment_id"), "kind" => kind,
|
|
75
|
-
"attempt_id" => params.fetch("attempt_id"), "generation" => @journal.authority_generation(selected_events),
|
|
76
|
-
"scope" => selected_events.find { |event| event["type"] == "intent" }&.dig("payload", "scope"),
|
|
77
|
-
"journal_commit" => commit, "sha256" => source.fetch(digest_key), "bytes" => bytes.bytesize,
|
|
78
|
-
"prepared_work" => registered.fetch("prepared_work"), "definition_digest" => registered.fetch("definition_digest")},
|
|
79
|
-
transfer_parts: [bytes], replayed: false}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def progress(request, peer, role, deadline, transfer)
|
|
83
|
-
validate_progress!(request)
|
|
84
|
-
unless %i[supervisor worker].include?(role)
|
|
85
|
-
raise AttemptErrors::UnauthorizedIdentity, "ordinary progress requires its original worker"
|
|
86
|
-
end
|
|
87
|
-
params, operation = request.fetch("params"), request.fetch("operation")
|
|
88
|
-
report = if operation == "queue_finish"
|
|
89
|
-
raise ArgumentError, "ordinary progress report needs one bounded part" unless transfer && transfer.count == 1
|
|
90
|
-
bytes = transfer.bytes.dup.force_encoding(Encoding::UTF_8)
|
|
91
|
-
unless bytes.bytesize.between?(1, 65_536) && bytes.valid_encoding? && !bytes.include?("\0")
|
|
92
|
-
raise ArgumentError, "ordinary progress report must be bounded UTF-8"
|
|
93
|
-
end
|
|
94
|
-
bytes
|
|
95
|
-
elsif transfer
|
|
96
|
-
raise ArgumentError, "ordinary progress operation forbids transfer"
|
|
97
|
-
end
|
|
98
|
-
commit = @journal.ref_value
|
|
99
|
-
registered = registration(params.fetch("assignment_id"), commit: commit)
|
|
100
|
-
raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
|
|
101
|
-
work = registered_work(registered, commit, deadline)
|
|
102
|
-
digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
|
|
103
|
-
keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
|
|
104
|
-
@exclusion.with_shared_multi(keys, deadline: deadline) do
|
|
105
|
-
authorize!(request, peer, role, deadline)
|
|
106
|
-
@journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
107
|
-
mutation_id: request.fetch("mutation_id"), operation: operation, parameters_digest: digest,
|
|
108
|
-
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
109
|
-
replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, _generation|
|
|
110
|
-
authorize_start_replay!(request, peer, role, deadline)
|
|
111
|
-
unless Molecules::CanonicalAttemptState.derive(events) == "running"
|
|
112
|
-
raise AttemptErrors::Conflict, "ordinary progress requires the original running attempt"
|
|
113
|
-
end
|
|
114
|
-
record = current_queue_record(registered, prefix)
|
|
115
|
-
scope = events.find { |event| event["type"] == "intent" }.fetch("payload").fetch("scope")
|
|
116
|
-
queue = OrdinaryQueue.new(work: work, root: @input_root, bytes: queue_bytes(record, prefix))
|
|
117
|
-
next_queue = case operation
|
|
118
|
-
when "queue_start" then queue.transition(operation: :start, scope: scope, step: params.fetch("step"))
|
|
119
|
-
when "queue_finish" then queue.transition(operation: :finish, scope: scope, step: params.fetch("step"), report: report)
|
|
120
|
-
when "queue_fail" then queue.transition(operation: :fail, scope: scope, message: params.fetch("message"))
|
|
121
|
-
end
|
|
122
|
-
sha = Digest::SHA256.hexdigest(next_queue.bytes)
|
|
123
|
-
path = "execution/definitions/#{params.fetch('assignment_id')}-queue-#{sha}.json"
|
|
124
|
-
{events: [], blobs: {path => next_queue.bytes}, data: {
|
|
125
|
-
"authority" => "ordinary", "assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
|
|
126
|
-
"project_id" => @project, "scope" => scope, "queue_ref" => path, "queue_sha256" => sha, "state" => "running",
|
|
127
|
-
"queue_complete" => next_queue.status.subtree_complete?(scope)}}
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def current_queue_record(registered, commit)
|
|
133
|
-
assignment = registered.fetch("assignment_id")
|
|
134
|
-
events = @journal.read_events(assignment, commit: commit)
|
|
135
|
-
events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
|
|
136
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary queue history is corrupt" unless Models::EvidenceEvent.chain_valid?(chain)
|
|
137
|
-
end
|
|
138
|
-
updates = events.select do |entry|
|
|
139
|
-
entry["type"] == "authority_mutation" &&
|
|
140
|
-
%w[start_attempt queue_start queue_finish queue_fail].include?(entry.dig("payload", "operation"))
|
|
141
|
-
end
|
|
142
|
-
return registered if updates.empty?
|
|
143
|
-
event = @journal.latest_event!(assignment_id: assignment, event_digests: updates.map { |entry| entry.fetch("digest") }, commit: commit)
|
|
144
|
-
record = event.dig("payload", "data")
|
|
145
|
-
unless record && record.values_at("authority", "assignment_id", "attempt_id", "project_id") ==
|
|
146
|
-
["ordinary", assignment, event.fetch("attempt_id"), @project]
|
|
147
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary canonical queue is unavailable"
|
|
148
|
-
end
|
|
149
|
-
record
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def queue_bytes(record, commit)
|
|
153
|
-
bytes = @journal.bounded_blob(record.fetch("queue_ref"), commit: commit, max_bytes: PreparedWork::MAX_TOTAL)
|
|
154
|
-
unless Digest::SHA256.hexdigest(bytes) == record.fetch("queue_sha256")
|
|
155
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary canonical queue bytes differ"
|
|
156
|
-
end
|
|
157
|
-
bytes
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|