ace-assign 0.71.0 → 0.73.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +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 +5 -2116
- 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/molecules/runtime_control_surface_runner.rb +6 -2
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
- 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 +22 -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,179 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Assign
|
|
7
|
-
module Molecules
|
|
8
|
-
# A decision projection of the existing qjl event chain. No effect
|
|
9
|
-
# executor, extra ref, file registry, or claim ledger lives here.
|
|
10
|
-
module ProposalJournal
|
|
11
|
-
PROPOSAL_REFERENCE = /\Aproposal-[0-9a-f]{24}-r[1-9][0-9]*\z/
|
|
12
|
-
PROPOSAL_BINDING = %w[operation project_id assignment_id attempt_id input_digest target candidate_head caller_uid].freeze
|
|
13
|
-
|
|
14
|
-
PROPOSAL_IMMUTABLE = (PROPOSAL_BINDING + %w[schema proposal_id revision revision_id request_id requester content_digest created_at authorization context options recommendation rationale prerequisites lifecycle_request revision_operation_id expected_source_revision silence_delegated]).freeze
|
|
15
|
-
|
|
16
|
-
def proposal_history(id)
|
|
17
|
-
assignment_ids.flat_map do |assignment|
|
|
18
|
-
events = read_events(assignment)
|
|
19
|
-
events.group_by { |event| event["attempt_id"] }.each_value do |chain|
|
|
20
|
-
unless Models::EvidenceEvent.chain_valid?(chain)
|
|
21
|
-
raise AttemptErrors::EvidenceUnavailable, "Proposal evidence chain is corrupt"
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
events.select { |event| event["type"] == "proposal_state" && event.dig("payload", "record", "proposal_id") == id }
|
|
25
|
-
.map { |event| event.fetch("payload").fetch("record") }
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def proposal_record(id)
|
|
30
|
-
proposal_history(id).last
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def proposals
|
|
34
|
-
assignment_ids.flat_map do |assignment|
|
|
35
|
-
read_events(assignment).select { |event| event["type"] == "proposal_state" }
|
|
36
|
-
.map { |event| event.dig("payload", "record", "proposal_id") }
|
|
37
|
-
end.uniq.map { |id| proposal_record(id) }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def proposal_operation_record(operation_id)
|
|
41
|
-
proposals.each do |record|
|
|
42
|
-
found = proposal_history(record["proposal_id"]).reverse.find do |entry|
|
|
43
|
-
entry["revision_operation_id"] == operation_id
|
|
44
|
-
end
|
|
45
|
-
return found if found
|
|
46
|
-
end
|
|
47
|
-
nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Admitted HITL policy runs this block while the same canonical
|
|
51
|
-
# mutation lock/CAS boundary excludes service claims and other replies.
|
|
52
|
-
def change_proposal(id, assignment_id:, attempt_id:, &policy)
|
|
53
|
-
3.times do
|
|
54
|
-
events = read_events(assignment_id).select { |event| event["attempt_id"] == attempt_id }
|
|
55
|
-
generation = events.count { |event| event["type"] == "authority_mutation" }
|
|
56
|
-
begin
|
|
57
|
-
return mutate(assignment_id: assignment_id, attempt_id: attempt_id,
|
|
58
|
-
mutation_id: "proposal-#{SecureRandom.hex(16)}", operation: "proposal-decision",
|
|
59
|
-
parameters_digest: Digest::SHA256.hexdigest(id), expected_generation: generation) do |current, _commit, _generation|
|
|
60
|
-
record = current.reverse.find do |event|
|
|
61
|
-
event["type"] == "proposal_state" && event.dig("payload", "record", "proposal_id") == id
|
|
62
|
-
end&.dig("payload", "record")
|
|
63
|
-
global = proposal_record(id)
|
|
64
|
-
if global && (global["assignment_id"] != assignment_id || global["attempt_id"] != attempt_id)
|
|
65
|
-
raise AttemptErrors::ReceiptRejected, "Proposal identity belongs to another assignment or attempt"
|
|
66
|
-
end
|
|
67
|
-
updated = policy.call(record)
|
|
68
|
-
return record if updated == record
|
|
69
|
-
unless updated.is_a?(Hash) && updated["proposal_id"] == id &&
|
|
70
|
-
updated["assignment_id"] == assignment_id && updated["attempt_id"] == attempt_id
|
|
71
|
-
raise AttemptErrors::ReceiptRejected, "Proposal policy changed immutable ownership"
|
|
72
|
-
end
|
|
73
|
-
validate_proposal_revision!(record, updated)
|
|
74
|
-
entries = []
|
|
75
|
-
if record && record["revision_id"] != updated["revision_id"]
|
|
76
|
-
entries << {type: "proposal_state", payload: {"record" => record.merge("state" => "superseded", "resolution" => "revised")}}
|
|
77
|
-
end
|
|
78
|
-
if record.nil? || record["revision_id"] != updated["revision_id"]
|
|
79
|
-
entries << {type: "proposal_state", payload: {"record" => updated.merge("state" => "proposed")}}
|
|
80
|
-
end
|
|
81
|
-
entries << {type: "proposal_state", payload: {"record" => updated}}
|
|
82
|
-
{events: entries, data: {"record" => updated}}
|
|
83
|
-
end.fetch("record")
|
|
84
|
-
rescue AttemptErrors::Conflict => e
|
|
85
|
-
raise unless e.message == "Authority registration generation changed"
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
raise AttemptErrors::EvidenceUnavailable, "Proposal generation stayed conflicting"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def validate_proposal_revision!(previous, updated)
|
|
92
|
-
if previous && previous["revision_id"] == updated["revision_id"]
|
|
93
|
-
unless PROPOSAL_IMMUTABLE.all? { |field| previous[field] == updated[field] }
|
|
94
|
-
raise AttemptErrors::ReceiptRejected, "Proposal revision content is immutable"
|
|
95
|
-
end
|
|
96
|
-
return
|
|
97
|
-
end
|
|
98
|
-
expected = previous ? previous.fetch("revision") + 1 : 1
|
|
99
|
-
reference = "#{updated.fetch("proposal_id")}-r#{expected}"
|
|
100
|
-
valid = updated["revision"] == expected && updated["revision_id"] == reference &&
|
|
101
|
-
updated["request_id"] == reference && updated["authorization"] == reference &&
|
|
102
|
-
updated["state"] == "awaiting-delivery" && PROPOSAL_REFERENCE.match?(reference)
|
|
103
|
-
if previous
|
|
104
|
-
valid &&= updated["expected_source_revision"] == previous["revision"] &&
|
|
105
|
-
updated["revision_operation_id"].is_a?(String) &&
|
|
106
|
-
updated["revision_operation_id"].match?(/\Arevision-[0-9a-f]{24}\z/) &&
|
|
107
|
-
proposal_operation_record(updated["revision_operation_id"]).nil? &&
|
|
108
|
-
%w[assignment_id attempt_id project_id requester caller_uid].all? { |field| previous[field] == updated[field] }
|
|
109
|
-
else
|
|
110
|
-
valid &&= updated["revision_operation_id"].nil? && updated["expected_source_revision"].nil?
|
|
111
|
-
end
|
|
112
|
-
unresolved = previous && proposal_claims(previous["revision_id"]).any? do |claim|
|
|
113
|
-
claim["state"] != "succeeded" && !proposal_no_effect?(claim)
|
|
114
|
-
end
|
|
115
|
-
raise AttemptErrors::ReceiptRejected, "Claimed proposal cannot be revised; reconcile the existing effect" if unresolved
|
|
116
|
-
raise AttemptErrors::ReceiptRejected, "Proposal revision must begin with exact immutable delivery scope" unless valid
|
|
117
|
-
end
|
|
118
|
-
private :validate_proposal_revision!
|
|
119
|
-
|
|
120
|
-
def proposal_claims(reference)
|
|
121
|
-
claims = (service_request_records + fixed_merge_request_records).select do |request|
|
|
122
|
-
request["authorization"] == reference &&
|
|
123
|
-
!(request["state"] == "rejected" && request["consumed"] == false)
|
|
124
|
-
end
|
|
125
|
-
return claims if claims.empty?
|
|
126
|
-
events = read_events(claims.first.fetch("assignment_id"))
|
|
127
|
-
claims.sort_by do |claim|
|
|
128
|
-
kind = claim.key?("queue_entry") ? "merge_admission" : "service_claim"
|
|
129
|
-
events.index do |event|
|
|
130
|
-
event["type"] == kind && event["attempt_id"] == claim.fetch("attempt_id") &&
|
|
131
|
-
event.dig("payload", "request_id") == claim.fetch("request_id")
|
|
132
|
-
end || raise(AttemptErrors::EvidenceUnavailable, "Proposal claim has no canonical admission event")
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Request IDs are scoped by record family. A same-text ID in the other
|
|
137
|
-
# family is another effect, never an idempotent replay.
|
|
138
|
-
def proposal_claim_conflict(reference, request_id:, fixed:)
|
|
139
|
-
[[service_request_records, false], [fixed_merge_request_records, true]].each do |records, family|
|
|
140
|
-
conflict = records.find do |claim|
|
|
141
|
-
claim["authorization"] == reference &&
|
|
142
|
-
!(family == fixed && claim["request_id"] == request_id) &&
|
|
143
|
-
!(claim["state"] == "rejected" && claim["consumed"] == false) &&
|
|
144
|
-
!proposal_no_effect?(claim)
|
|
145
|
-
end
|
|
146
|
-
return conflict if conflict
|
|
147
|
-
end
|
|
148
|
-
nil
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def proposal_no_effect?(claim)
|
|
152
|
-
(claim["state"] == "failed-settled" && settlement_evidence_intact?(claim)) ||
|
|
153
|
-
(claim["state"] == "failed" && claim.key?("queue_entry") && fixed_no_effect_intact?(claim))
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def proposal_authorize!(reference, binding)
|
|
157
|
-
unless reference.is_a?(String) && PROPOSAL_REFERENCE.match?(reference)
|
|
158
|
-
raise AttemptErrors::ReceiptRejected, "Invalid proposal authorization reference"
|
|
159
|
-
end
|
|
160
|
-
id = reference.sub(/-r[1-9][0-9]*\z/, "")
|
|
161
|
-
record = proposal_record(id)
|
|
162
|
-
unless record && record["revision_id"] == reference &&
|
|
163
|
-
record["stop_requested"] != true &&
|
|
164
|
-
%w[approved-explicitly approved-by-silence].include?(record["state"]) &&
|
|
165
|
-
PROPOSAL_BINDING.all? { |field| record[field] == binding[field] }
|
|
166
|
-
raise AttemptErrors::ReceiptRejected, "Proposal is not authorized for this exact effect"
|
|
167
|
-
end
|
|
168
|
-
attempt = derived_attempts(binding.fetch("assignment_id")).find do |candidate|
|
|
169
|
-
candidate.attempt_id == binding.fetch("attempt_id")
|
|
170
|
-
end
|
|
171
|
-
unless attempt && attempt.state == "running"
|
|
172
|
-
raise AttemptErrors::ReceiptRejected, "Proposal effect needs an authoritative bound running attempt"
|
|
173
|
-
end
|
|
174
|
-
record
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "protected_workspace_exclusion"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Assign
|
|
7
|
-
module Molecules
|
|
8
|
-
class LifecycleExclusion
|
|
9
|
-
# Authority-side canonical guards. This owner never selects a Git/cache
|
|
10
|
-
# root, resets a fence, or grants the lifetime workspace writer lease.
|
|
11
|
-
class ControlExclusion
|
|
12
|
-
class Protection < Ace::Runtime::Molecules::ProtectedArtifactSet::Protection
|
|
13
|
-
def initialize(authority_uid:, acl: Authority::PosixAcl.new, **options)
|
|
14
|
-
super(**options)
|
|
15
|
-
@authority_uid, @acl = authority_uid, acl
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def verify!(path, handle, directory:)
|
|
19
|
-
super
|
|
20
|
-
unless (handle.stat.mode & 0o7000).zero? && @acl.entries(path).nil? &&
|
|
21
|
-
(!directory || @acl.entries(path, attribute: "system.posix_acl_default").nil?)
|
|
22
|
-
raise AttemptErrors::EvidenceUnavailable, "Lifecycle control protection differs"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
def trusted_owner?(stat) = [0, @authority_uid].include?(stat.uid)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def initialize(authority:, project_id:, descriptor_sha256:, root_identity: nil, files: File, protection: nil)
|
|
31
|
-
unless authority.is_a?(Hash) && project_id.is_a?(String) && token?(project_id) &&
|
|
32
|
-
descriptor_sha256.is_a?(String) && descriptor_sha256.match?(/\A[0-9a-f]{64}\z/) &&
|
|
33
|
-
%w[uid gid].all? { |key| authority[key].is_a?(Integer) && authority[key].positive? } &&
|
|
34
|
-
authority["state_root"].is_a?(String) && authority["state_root"].valid_encoding? &&
|
|
35
|
-
authority["state_root"].bytesize.between?(1, 4096) && !authority["state_root"].include?("\0") &&
|
|
36
|
-
authority["state_root"].start_with?("/") && File.expand_path(authority["state_root"]) == authority["state_root"]
|
|
37
|
-
unavailable!("Lifecycle control selection is malformed")
|
|
38
|
-
end
|
|
39
|
-
@root = File.join(authority.fetch("state_root"), "lifecycle-exclusion", project_id, "control")
|
|
40
|
-
unavailable!("Lifecycle control path is oversized") if @root.bytesize > 4096
|
|
41
|
-
@uid, @gid, @descriptor = authority.values_at("uid", "gid") + [descriptor_sha256.dup.freeze]
|
|
42
|
-
if root_identity
|
|
43
|
-
unless root_identity.is_a?(Hash) && root_identity.keys.sort == %w[device gid inode uid] &&
|
|
44
|
-
root_identity.values.all? { |value| value.is_a?(Integer) && value >= 0 } &&
|
|
45
|
-
root_identity.values_at("uid", "gid") == [@uid, @gid]
|
|
46
|
-
unavailable!("Lifecycle control original identity differs")
|
|
47
|
-
end
|
|
48
|
-
@identity = root_identity.dup.freeze
|
|
49
|
-
end
|
|
50
|
-
@files = files
|
|
51
|
-
@protection = protection || Protection.new(authority_uid: @uid)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def task_key(id) = key!("task", id)
|
|
55
|
-
def assignment_key(id) = key!("assignment", id)
|
|
56
|
-
|
|
57
|
-
def selection!
|
|
58
|
-
with_root do |handles, identities|
|
|
59
|
-
stat = handles.fetch(@root).stat
|
|
60
|
-
@identity ||= {"device" => stat.dev, "inode" => stat.ino, "uid" => stat.uid, "gid" => stat.gid}.freeze
|
|
61
|
-
{"descriptor_sha256" => @descriptor, "root_identity" => @identity}.freeze
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Invoked only after the existing complete registration admission.
|
|
66
|
-
# A partial creation is unavailable, not a missing-fence reset grant.
|
|
67
|
-
def provision_keys!(keys:)
|
|
68
|
-
ordered = keys!(keys)
|
|
69
|
-
with_root do |handles, identities|
|
|
70
|
-
root = handles.fetch(@root)
|
|
71
|
-
ordered.each do |key|
|
|
72
|
-
lock_path, marker_path = paths(key)
|
|
73
|
-
lock, created = create_or_open!(lock_path, "", root)
|
|
74
|
-
begin
|
|
75
|
-
validate_file!(lock_path, lock, empty: true)
|
|
76
|
-
marker = if created
|
|
77
|
-
create_or_open!(marker_path, Atoms::EvidenceDigest.canonical_json(
|
|
78
|
-
{"key" => key, "removed" => false, "removed_at" => nil}), root).first
|
|
79
|
-
else
|
|
80
|
-
@files.open(marker_path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
81
|
-
end
|
|
82
|
-
begin
|
|
83
|
-
live_marker!(key, marker_path, marker)
|
|
84
|
-
ensure
|
|
85
|
-
marker&.close
|
|
86
|
-
end
|
|
87
|
-
ensure
|
|
88
|
-
lock&.close
|
|
89
|
-
end
|
|
90
|
-
unchanged!(handles, identities)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
self
|
|
94
|
-
rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
|
|
95
|
-
unavailable!("Lifecycle control provisioning is unavailable")
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def with_shared_multi(keys, &block) = with_locks(keys, File::LOCK_SH, &block)
|
|
99
|
-
def with_shared(key, &block) = with_shared_multi([key], &block)
|
|
100
|
-
def with_exclusive(key, deadline: nil, &block) = with_locks([key], File::LOCK_EX, deadline: deadline, &block)
|
|
101
|
-
|
|
102
|
-
# The canonical writer calls this again immediately before CAS. It
|
|
103
|
-
# authenticates the same held keys, not freshly reopened substitutes.
|
|
104
|
-
def verify_unchanged!
|
|
105
|
-
checks = Thread.current[:ace_assign_control_checks]&.fetch(object_id, nil)
|
|
106
|
-
unavailable!("Lifecycle control is not held") unless checks && !checks.empty?
|
|
107
|
-
checks.each(&:call)
|
|
108
|
-
true
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def with_locks(keys, mode, deadline: nil)
|
|
114
|
-
callback_error = nil
|
|
115
|
-
ordered = keys!(keys)
|
|
116
|
-
deadline!(deadline) if deadline
|
|
117
|
-
with_root do |handles, identities|
|
|
118
|
-
locks, markers, marker_facts, lock_facts = [], [], [], []
|
|
119
|
-
begin
|
|
120
|
-
ordered.each do |key|
|
|
121
|
-
path, marker_path = paths(key)
|
|
122
|
-
lock = @files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
123
|
-
locks << lock
|
|
124
|
-
validate_file!(path, lock, empty: true)
|
|
125
|
-
lock_facts << file_metadata(lock.stat)
|
|
126
|
-
unless lock.flock(mode | File::LOCK_NB)
|
|
127
|
-
raise AttemptErrors::MaintenanceBusy, "Lifecycle control is under maintenance"
|
|
128
|
-
end
|
|
129
|
-
marker = @files.open(marker_path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
130
|
-
markers << marker
|
|
131
|
-
live_marker!(key, marker_path, marker)
|
|
132
|
-
marker_facts << file_metadata(marker.stat)
|
|
133
|
-
end
|
|
134
|
-
deadline!(deadline) if deadline
|
|
135
|
-
unchanged!(handles, identities)
|
|
136
|
-
check = proc do
|
|
137
|
-
ordered.each_with_index do |key, index|
|
|
138
|
-
path, marker_path = paths(key)
|
|
139
|
-
lock, marker = locks.fetch(index), markers.fetch(index)
|
|
140
|
-
validate_file!(path, lock, empty: true)
|
|
141
|
-
unless file_metadata(lock.stat) == lock_facts.fetch(index) &&
|
|
142
|
-
file_metadata(marker.stat) == marker_facts.fetch(index)
|
|
143
|
-
unavailable!("Lifecycle control key changed")
|
|
144
|
-
end
|
|
145
|
-
marker.rewind
|
|
146
|
-
live_marker!(key, marker_path, marker)
|
|
147
|
-
end
|
|
148
|
-
unchanged!(handles, identities)
|
|
149
|
-
end
|
|
150
|
-
registry = Thread.current[:ace_assign_control_checks] ||= {}
|
|
151
|
-
checks = registry[object_id] ||= []
|
|
152
|
-
checks << check
|
|
153
|
-
begin
|
|
154
|
-
begin
|
|
155
|
-
result = yield
|
|
156
|
-
rescue Exception => error
|
|
157
|
-
callback_error = error
|
|
158
|
-
raise
|
|
159
|
-
end
|
|
160
|
-
verify_unchanged!
|
|
161
|
-
ensure
|
|
162
|
-
checks.pop
|
|
163
|
-
registry.delete(object_id) if checks.empty?
|
|
164
|
-
end
|
|
165
|
-
deadline!(deadline) if deadline
|
|
166
|
-
result
|
|
167
|
-
ensure
|
|
168
|
-
close_all!(locks + markers)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
|
|
172
|
-
raise if callback_error
|
|
173
|
-
unavailable!("Lifecycle control admission is unavailable")
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def with_root
|
|
177
|
-
callback_error = nil
|
|
178
|
-
handles, identities = {}, {}
|
|
179
|
-
begin
|
|
180
|
-
parts = @root.split("/").reject(&:empty?)
|
|
181
|
-
(["/"] + parts.each_index.map { |index| "/" + parts.take(index + 1).join("/") }).each do |path|
|
|
182
|
-
handle = @files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
183
|
-
handles[path] = handle
|
|
184
|
-
unless handle.stat.directory? && file_identity(handle.stat) == file_identity(@files.lstat(path))
|
|
185
|
-
unavailable!("Lifecycle control directory changed")
|
|
186
|
-
end
|
|
187
|
-
@protection.verify!(path, handle, directory: true)
|
|
188
|
-
identities[path] = file_identity(handle.stat)
|
|
189
|
-
end
|
|
190
|
-
root = handles.fetch(@root).stat
|
|
191
|
-
unless [root.uid, root.gid, root.mode & 0o7777] == [@uid, @gid, 0o700] &&
|
|
192
|
-
(!@identity || [root.dev, root.ino, root.uid, root.gid] == @identity.values_at("device", "inode", "uid", "gid"))
|
|
193
|
-
unavailable!("Lifecycle control original root differs")
|
|
194
|
-
end
|
|
195
|
-
begin
|
|
196
|
-
result = yield handles, identities
|
|
197
|
-
rescue Exception => error
|
|
198
|
-
callback_error = error
|
|
199
|
-
raise
|
|
200
|
-
end
|
|
201
|
-
unchanged!(handles, identities)
|
|
202
|
-
result
|
|
203
|
-
ensure
|
|
204
|
-
close_all!(handles.values)
|
|
205
|
-
end
|
|
206
|
-
rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
|
|
207
|
-
raise if callback_error
|
|
208
|
-
unavailable!("Lifecycle control root is unavailable")
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def unchanged!(handles, identities)
|
|
212
|
-
handles.each do |path, handle|
|
|
213
|
-
unless file_identity(handle.stat) == identities.fetch(path) &&
|
|
214
|
-
identities.fetch(path) == file_identity(@files.lstat(path))
|
|
215
|
-
unavailable!("Lifecycle control directory changed")
|
|
216
|
-
end
|
|
217
|
-
@protection.verify!(path, handle, directory: true)
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def create_or_open!(path, bytes, root)
|
|
222
|
-
created = true
|
|
223
|
-
file = begin
|
|
224
|
-
@files.open(path, File::RDWR | File::CREAT | File::EXCL | File::NOFOLLOW | File::NONBLOCK, 0o600)
|
|
225
|
-
rescue Errno::EEXIST
|
|
226
|
-
created = false
|
|
227
|
-
@files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
228
|
-
end
|
|
229
|
-
if created
|
|
230
|
-
file.write(bytes)
|
|
231
|
-
file.flush
|
|
232
|
-
file.fsync
|
|
233
|
-
root.fsync
|
|
234
|
-
end
|
|
235
|
-
file.rewind
|
|
236
|
-
[file, created]
|
|
237
|
-
rescue Exception
|
|
238
|
-
file&.close
|
|
239
|
-
raise
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def validate_file!(path, file, empty: false)
|
|
243
|
-
stat = file.stat
|
|
244
|
-
unless stat.file? && stat.nlink == 1 && [stat.uid, stat.gid, stat.mode & 0o7777] == [@uid, @gid, 0o600] &&
|
|
245
|
-
file_identity(stat) == file_identity(@files.lstat(path)) && (empty ? stat.size.zero? : stat.size.between?(1, 8192))
|
|
246
|
-
unavailable!("Lifecycle control file differs")
|
|
247
|
-
end
|
|
248
|
-
@protection.verify!(path, file, directory: false)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def live_marker!(key, path, file)
|
|
252
|
-
validate_file!(path, file)
|
|
253
|
-
before = file.stat
|
|
254
|
-
bytes = file.read(8193)
|
|
255
|
-
state = LifecycleExclusion.workspace_marker!(bytes: bytes, key: key)
|
|
256
|
-
unless bytes.bytesize == before.size && file_metadata(before) == file_metadata(file.stat) &&
|
|
257
|
-
file_metadata(before) == file_metadata(@files.lstat(path))
|
|
258
|
-
unavailable!("Lifecycle control marker changed")
|
|
259
|
-
end
|
|
260
|
-
raise AttemptErrors::Conflict, "Lifecycle control identity is fenced" if state.fetch("removed")
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def keys!(keys)
|
|
264
|
-
values = Array(keys).uniq
|
|
265
|
-
unless values.any? && values.all? { |key| key.is_a?(String) && key.match?(/\A(?:task|assignment):[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/) }
|
|
266
|
-
unavailable!("Lifecycle control keys differ")
|
|
267
|
-
end
|
|
268
|
-
values.sort_by { |key| [key.start_with?("task:") ? 0 : 1, key] }
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def key!(kind, id)
|
|
272
|
-
unavailable!("Lifecycle control key differs") unless id.is_a?(String) && token?(id)
|
|
273
|
-
"#{kind}:#{id}"
|
|
274
|
-
end
|
|
275
|
-
def token?(value) = value.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/)
|
|
276
|
-
def paths(key)
|
|
277
|
-
stem = File.join(@root, Digest::SHA256.hexdigest(key))
|
|
278
|
-
[stem + ".lock", stem + ".state.json"]
|
|
279
|
-
end
|
|
280
|
-
def file_identity(stat) = [stat.dev, stat.ino, stat.uid, stat.gid, stat.mode]
|
|
281
|
-
def file_metadata(stat) = file_identity(stat) + [stat.size, stat.mtime, stat.ctime]
|
|
282
|
-
def unavailable!(message) = raise(AttemptErrors::EvidenceUnavailable, message)
|
|
283
|
-
def deadline!(deadline)
|
|
284
|
-
unless deadline.is_a?(Numeric) && deadline.finite? && deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
285
|
-
raise AttemptErrors::MaintenanceBusy, "Lifecycle control admission deadline elapsed"
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
def close_all!(files)
|
|
289
|
-
error = nil
|
|
290
|
-
files.reverse_each do |file|
|
|
291
|
-
begin
|
|
292
|
-
file.close unless file.closed?
|
|
293
|
-
rescue SystemCallError, IOError => caught
|
|
294
|
-
error ||= caught
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
raise error if error
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
end
|