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,180 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/runtime/molecules/protected_artifact_set"
|
|
4
|
-
require_relative "cleanup_result_protection"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# The existing service evidence owner authenticates this complete pair.
|
|
10
|
-
# Root-private archives remain outside the authority evidence graph.
|
|
11
|
-
module ServiceCleanupEvidence
|
|
12
|
-
ROOT_SELECTION_SCHEMA = "ace.protected-workspace-prune-root-selection/v1"
|
|
13
|
-
ROOT_RECEIPT_SCHEMA = "ace.protected-workspace-prune-receipt/v1"
|
|
14
|
-
ROOT_SELECTION_FIELDS = %w[schema request_id input_digest operation_owner_binding_digest receipt_ref].freeze
|
|
15
|
-
ROOT_INSPECTION_SELECTION_SCHEMA = "ace.protected-workspace-prune-inspection-selection/v1"
|
|
16
|
-
ROOT_INSPECTION_SELECTION_FIELDS = %w[schema request_id input_digest operation_owner_binding_digest inspection_ref].freeze
|
|
17
|
-
ROOT_RECEIPT_FIELDS = %w[schema request_id input_digest maintenance target publication canonical_snapshots preservation removal].freeze
|
|
18
|
-
|
|
19
|
-
# Pre-import decoding reuses the collection validator. Only the lower
|
|
20
|
-
# writer's pending collection can authenticate protected storage and
|
|
21
|
-
# accept a terminal update.
|
|
22
|
-
def cleanup_inspection_inputs!(contents, record, challenge)
|
|
23
|
-
unless record["operation"] == "prune-preserved-workspace" && contents.is_a?(Array) && contents.size == 2
|
|
24
|
-
raise AttemptErrors::ReceiptRejected, "cleanup inspection requires its complete pair"
|
|
25
|
-
end
|
|
26
|
-
cleanup_documents!(contents, record, no_effect: true, challenge: challenge)
|
|
27
|
-
true
|
|
28
|
-
rescue KeyError, TypeError, JSON::ParserError, JSON::GeneratorError
|
|
29
|
-
raise AttemptErrors::ReceiptRejected, "cleanup inspection pair is unavailable"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def cleanup_collection!(references, record, state, pending)
|
|
35
|
-
unless record["operation"] == "prune-preserved-workspace" && %w[succeeded failed-settled].include?(state) && references.size == 2
|
|
36
|
-
raise AttemptErrors::ReceiptRejected, "cleanup completion requires its complete original evidence pair"
|
|
37
|
-
end
|
|
38
|
-
no_effect = state == "failed-settled"
|
|
39
|
-
challenge = no_effect ? challenge!(record, pending: pending) : nil
|
|
40
|
-
expected = no_effect ? context_for_challenge(record, challenge) : context(record)
|
|
41
|
-
proof = cleanup_dispatch_context!(record, commit: pending && pending[:commit] || @journal.ref_value, pending: pending)
|
|
42
|
-
contents = references.map do |reference|
|
|
43
|
-
if pending && pending[:pending_events]
|
|
44
|
-
@canonical.read_pending(reference, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
|
|
45
|
-
else
|
|
46
|
-
@canonical.read(reference, **expected, commit: pending && pending[:commit] || @journal.ref_value)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
reference, operation_bytes = cleanup_documents!(contents, record, no_effect: no_effect,
|
|
50
|
-
challenge: challenge)
|
|
51
|
-
path = reference.fetch("path")
|
|
52
|
-
# The lower writer supplies its pending import transaction. Historical
|
|
53
|
-
# consumers have only a pinned commit and never reopen mutable storage.
|
|
54
|
-
if pending && pending[:pending_events]
|
|
55
|
-
selected_artifacts = @cleanup_artifacts || Ace::Runtime::Molecules::ProtectedArtifactSet.new(
|
|
56
|
-
protection: CleanupResultProtection.new(result_path: path, authority_gid: Process.egid))
|
|
57
|
-
selected_artifacts.with do |artifacts|
|
|
58
|
-
unless artifacts.read!(reference) == operation_bytes
|
|
59
|
-
raise AttemptErrors::ReceiptRejected, "cleanup retained result bytes differ"
|
|
60
|
-
end
|
|
61
|
-
artifacts.verify_unchanged!
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
if (view = service_read_view(pending))
|
|
65
|
-
view.projection = settlement_projection(record, pending, challenge, proof)
|
|
66
|
-
end
|
|
67
|
-
contents.freeze
|
|
68
|
-
rescue KeyError, TypeError, JSON::ParserError, JSON::GeneratorError, Ace::Runtime::RuntimeUnavailableError
|
|
69
|
-
raise AttemptErrors::ReceiptRejected, "cleanup original evidence is unavailable"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def cleanup_documents!(contents, record, no_effect:, challenge:)
|
|
73
|
-
documents = contents.map { |bytes| cleanup_json!(bytes) }
|
|
74
|
-
selection_schema = no_effect ? ROOT_INSPECTION_SELECTION_SCHEMA : ROOT_SELECTION_SCHEMA
|
|
75
|
-
selection_fields = no_effect ? ROOT_INSPECTION_SELECTION_FIELDS : ROOT_SELECTION_FIELDS
|
|
76
|
-
selection_index = documents.each_index.select { |index| documents[index]["schema"] == selection_schema }
|
|
77
|
-
receipt_index = documents.each_index.select do |index|
|
|
78
|
-
no_effect ? documents[index].keys.sort == ServiceEvidence::INSPECTION_FIELDS.sort : documents[index]["schema"] == ROOT_RECEIPT_SCHEMA
|
|
79
|
-
end
|
|
80
|
-
unless selection_index.one? && receipt_index.one?
|
|
81
|
-
raise AttemptErrors::ReceiptRejected, "cleanup original evidence selection is ambiguous"
|
|
82
|
-
end
|
|
83
|
-
selection = documents.fetch(selection_index.first)
|
|
84
|
-
receipt = documents.fetch(receipt_index.first)
|
|
85
|
-
closed_cleanup!(selection, selection_fields)
|
|
86
|
-
closed_cleanup!(receipt, no_effect ? ServiceEvidence::INSPECTION_FIELDS : ROOT_RECEIPT_FIELDS)
|
|
87
|
-
unless [selection, receipt].all? { |value| value["request_id"] == record.fetch("request_id") && value["input_digest"] == record.fetch("input_digest") } &&
|
|
88
|
-
selection["operation_owner_binding_digest"] == Atoms::EvidenceDigest.digest(record.fetch("operation_owner_binding"))
|
|
89
|
-
raise AttemptErrors::ReceiptRejected, "cleanup original owner or request differs"
|
|
90
|
-
end
|
|
91
|
-
reference = selection.fetch(no_effect ? "inspection_ref" : "receipt_ref")
|
|
92
|
-
closed_cleanup!(reference, %w[path bytes sha256])
|
|
93
|
-
path = reference.fetch("path")
|
|
94
|
-
operation_bytes = contents.fetch(receipt_index.first)
|
|
95
|
-
unless path.is_a?(String) && path.bytesize <= 4096 && path.valid_encoding? && !path.include?("\0") &&
|
|
96
|
-
path.start_with?("/") && File.expand_path(path) == path &&
|
|
97
|
-
reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, 65_536) &&
|
|
98
|
-
reference["bytes"] == operation_bytes.bytesize && reference["sha256"] == Digest::SHA256.hexdigest(operation_bytes)
|
|
99
|
-
raise AttemptErrors::ReceiptRejected, "cleanup retained result reference differs"
|
|
100
|
-
end
|
|
101
|
-
if no_effect
|
|
102
|
-
inspection_projection!(receipt, record, challenge)
|
|
103
|
-
else
|
|
104
|
-
cleanup_receipt_projection!(receipt, record)
|
|
105
|
-
end
|
|
106
|
-
[reference, operation_bytes]
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def cleanup_json!(bytes)
|
|
110
|
-
unless bytes.is_a?(String) && bytes.bytesize.between?(1, 65_536)
|
|
111
|
-
raise AttemptErrors::ReceiptRejected, "cleanup evidence exceeds bounds"
|
|
112
|
-
end
|
|
113
|
-
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
114
|
-
raise AttemptErrors::ReceiptRejected, "cleanup evidence is invalid UTF8" unless text.valid_encoding? && !text.include?("\0")
|
|
115
|
-
value = JSON.parse(text, create_additions: false, max_nesting: 16, allow_duplicate_key: false, allow_comments: false)
|
|
116
|
-
raise AttemptErrors::ReceiptRejected, "cleanup evidence is not an object" unless value.is_a?(Hash)
|
|
117
|
-
value
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def closed_cleanup!(value, fields)
|
|
121
|
-
unless value.is_a?(Hash) && value.keys.sort == fields.sort
|
|
122
|
-
raise AttemptErrors::ReceiptRejected, "cleanup evidence fields differ"
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def cleanup_receipt_projection!(receipt, record)
|
|
127
|
-
preservation = receipt.fetch("preservation")
|
|
128
|
-
closed_cleanup!(preservation, %w[head branch destinations manifest_sha256 inventory_sha256 file_count total_bytes private_manifest_sha256 archives_sha256])
|
|
129
|
-
unless preservation["file_count"].is_a?(Integer) && preservation["file_count"].between?(0, 4096) &&
|
|
130
|
-
preservation["total_bytes"].is_a?(Integer) && preservation["total_bytes"].between?(0, 256 * 1024 * 1024) &&
|
|
131
|
-
%w[manifest_sha256 inventory_sha256 private_manifest_sha256 archives_sha256].all? { |key| cleanup_sha?(preservation[key]) } &&
|
|
132
|
-
preservation.values_at("manifest_sha256", "inventory_sha256", "private_manifest_sha256").uniq.one?
|
|
133
|
-
raise AttemptErrors::ReceiptRejected, "cleanup preservation observation differs"
|
|
134
|
-
end
|
|
135
|
-
input = receipt.slice("maintenance", "target", "publication").merge("schema" => "ace.protected-workspace-prune/v1",
|
|
136
|
-
"preservation" => preservation.slice("head", "branch", "destinations", "manifest_sha256"))
|
|
137
|
-
unless Atoms::EvidenceDigest.digest(input) == record.fetch("input_digest") &&
|
|
138
|
-
receipt.fetch("maintenance") == record.slice("project_id", "mapping_id", "assignment_id", "attempt_id")
|
|
139
|
-
raise AttemptErrors::ReceiptRejected, "cleanup original input differs"
|
|
140
|
-
end
|
|
141
|
-
snapshots = receipt.fetch("canonical_snapshots")
|
|
142
|
-
unless snapshots.is_a?(Array) && snapshots.size.between?(1, 256)
|
|
143
|
-
raise AttemptErrors::ReceiptRejected, "cleanup canonical snapshots exceed bounds"
|
|
144
|
-
end
|
|
145
|
-
snapshots.each do |snapshot|
|
|
146
|
-
closed_cleanup!(snapshot, %w[mapping_id journal_commit binding_event_digest release_event_digest proof_event_digest])
|
|
147
|
-
unless snapshot["mapping_id"].is_a?(String) && snapshot["mapping_id"].match?(Molecules::JournalMutation::ID) &&
|
|
148
|
-
snapshot["journal_commit"].is_a?(String) && snapshot["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
149
|
-
%w[binding_event_digest release_event_digest proof_event_digest].all? { |key| cleanup_sha?(snapshot[key]) }
|
|
150
|
-
raise AttemptErrors::ReceiptRejected, "cleanup canonical snapshot differs"
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
ids = snapshots.map { |snapshot| snapshot.fetch("mapping_id") }
|
|
154
|
-
raise AttemptErrors::ReceiptRejected, "cleanup canonical snapshots are not uniquely ordered" unless ids == ids.uniq.sort
|
|
155
|
-
removal = receipt.fetch("removal")
|
|
156
|
-
closed_cleanup!(removal, %w[original captured outcome fence_digest])
|
|
157
|
-
unless removal["outcome"] == "removed" && cleanup_sha?(removal["fence_digest"])
|
|
158
|
-
raise AttemptErrors::ReceiptRejected, "cleanup has no positive removal observation"
|
|
159
|
-
end
|
|
160
|
-
%w[original captured].each do |key|
|
|
161
|
-
object = removal.fetch(key)
|
|
162
|
-
closed_cleanup!(object, %w[device inode worktree_admin_id head branch])
|
|
163
|
-
unless %w[device inode].all? { |field| object[field].is_a?(Integer) && object[field].positive? } &&
|
|
164
|
-
object["worktree_admin_id"].is_a?(String) && object["worktree_admin_id"].bytesize.between?(1, 200) &&
|
|
165
|
-
object["worktree_admin_id"].match?(/\A[a-zA-Z0-9_.-]+\z/) &&
|
|
166
|
-
object["head"] == preservation["head"] && object["branch"] == preservation["branch"]
|
|
167
|
-
raise AttemptErrors::ReceiptRejected, "cleanup captured object differs"
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
unless removal.fetch("original") == removal.fetch("captured")
|
|
171
|
-
raise AttemptErrors::ReceiptRejected, "cleanup replaced object cannot attest removal"
|
|
172
|
-
end
|
|
173
|
-
true
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def cleanup_sha?(value) = value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/)
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "ace/git/atoms/service_merge_evidence"
|
|
3
|
-
require "ace/git/atoms/service_pr_evidence"
|
|
4
|
-
require_relative "service_evidence"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# The existing service completion/import transaction owns delivery writes.
|
|
10
|
-
class ServiceDeliveryEvidence
|
|
11
|
-
def initialize(journal:)
|
|
12
|
-
@journal = journal
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def completion_event(record:, receipt:, completion_digest:, artifacts:)
|
|
16
|
-
unless artifacts.length == 1
|
|
17
|
-
raise AttemptErrors::ReceiptRejected, "merge completion requires its one fixed evidence artifact"
|
|
18
|
-
end
|
|
19
|
-
proof = validate_artifact!(record, artifacts.first)
|
|
20
|
-
{type: "delivery", payload: payload(record, receipt, completion_digest, proof)}
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def verified(record:, events:, commit:)
|
|
24
|
-
unless %w[create update ready merge].include?(record["operation"]) && record["state"] == "succeeded" &&
|
|
25
|
-
record.fetch("receipt").fetch("evidence").length == 1
|
|
26
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical merge completion is unavailable"
|
|
27
|
-
end
|
|
28
|
-
receipt = record.fetch("receipt")
|
|
29
|
-
reference = receipt.fetch("evidence").first
|
|
30
|
-
bytes = Molecules::CanonicalEvidence.new(journal: @journal).read(reference,
|
|
31
|
-
**ServiceEvidence.new(journal: @journal).context(record), commit: commit)
|
|
32
|
-
expected = payload(record, receipt, record.fetch("completion_digest"), validate_artifact!(record, bytes))
|
|
33
|
-
results = events.select { |event| event["type"] == "delivery" && event.dig("payload", "service_request_id") == record.fetch("request_id") }
|
|
34
|
-
unless results.one? && Atoms::EvidenceDigest.digest(results.first.fetch("payload")) == Atoms::EvidenceDigest.digest(expected)
|
|
35
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical merge delivery result differs"
|
|
36
|
-
end
|
|
37
|
-
transitions = events.select { |event| event["type"] == "service_transition" &&
|
|
38
|
-
event.dig("payload", "request_id") == record.fetch("request_id") && event.dig("payload", "state") == "succeeded" &&
|
|
39
|
-
event.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(record) &&
|
|
40
|
-
event.dig("payload", "receipt_digest") == Atoms::EvidenceDigest.digest(receipt) }
|
|
41
|
-
imports = events.select { |event| event["type"] == "evidence_import" &&
|
|
42
|
-
"evidence/imports/#{event.dig('payload', 'artifact_id')}" == reference.fetch("ref") }
|
|
43
|
-
unless transitions.one? && imports.one?
|
|
44
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical merge completion introduction differs"
|
|
45
|
-
end
|
|
46
|
-
# The first completion's authority mutation immediately follows its
|
|
47
|
-
# terminal transition. Later identical completion observations may
|
|
48
|
-
# have their own mutation IDs; they cannot replace this introduction.
|
|
49
|
-
completion = events[events.index(transitions.first) + 1]
|
|
50
|
-
unless completion && completion["type"] == "authority_mutation" &&
|
|
51
|
-
completion.dig("payload", "operation") == "complete_service" &&
|
|
52
|
-
completion.dig("payload", "data", "request_id") == record.fetch("request_id")
|
|
53
|
-
raise AttemptErrors::EvidenceUnavailable, "original merge completion mutation differs"
|
|
54
|
-
end
|
|
55
|
-
selected = [results.first, transitions.first, completion, imports.first]
|
|
56
|
-
introductions = @journal.event_commits!(assignment_id: record.fetch("assignment_id"),
|
|
57
|
-
event_digests: selected.map { |event| event.fetch("digest") }, commit: commit)
|
|
58
|
-
unless introductions.values.uniq.length == 1
|
|
59
|
-
raise AttemptErrors::EvidenceUnavailable, "merge delivery result was not atomically imported and completed"
|
|
60
|
-
end
|
|
61
|
-
results.first
|
|
62
|
-
rescue KeyError, TypeError, ArgumentError, AttemptErrors::ReceiptRejected
|
|
63
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical merge delivery evidence is unverifiable"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def validate_artifact!(record, bytes)
|
|
69
|
-
options = {request_id: record.fetch("request_id"), input_digest: record.fetch("input_digest"),
|
|
70
|
-
target: record.fetch("target"), head: record.fetch("candidate_head")}
|
|
71
|
-
proof = if record.fetch("operation") == "merge"
|
|
72
|
-
Ace::Git::Atoms::ServiceMergeEvidence.validate(bytes, **options)
|
|
73
|
-
else
|
|
74
|
-
Ace::Git::Atoms::ServicePrEvidence.validate(bytes, operation: record.fetch("operation"), **options)
|
|
75
|
-
end
|
|
76
|
-
unless Atoms::EvidenceDigest.digest(proof.fetch(:input)) == record.fetch("input_digest")
|
|
77
|
-
raise AttemptErrors::ReceiptRejected, "merge evidence does not reconstruct the originally accepted input"
|
|
78
|
-
end
|
|
79
|
-
proof
|
|
80
|
-
rescue ArgumentError, KeyError
|
|
81
|
-
raise AttemptErrors::ReceiptRejected, "merge evidence original input or outcome differs"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def payload(record, receipt, completion_digest, proof)
|
|
85
|
-
self.class.payload(record, receipt, completion_digest, proof)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Pure result shape shared with the read-only worker consumer. This
|
|
89
|
-
# function creates no evidence or authorization on its own.
|
|
90
|
-
def self.payload(record, receipt, completion_digest, proof)
|
|
91
|
-
{"stage" => "result", "operation" => record.fetch("operation"), "head" => record.fetch("candidate_head"), "outcome" => "succeeded",
|
|
92
|
-
"intent_digest" => nil, "intent_attempt_id" => nil,
|
|
93
|
-
"pr" => proof.fetch(:pr).slice("number", "url", "head_sha", "draft", "state", "merge_commit_sha"),
|
|
94
|
-
"service_request_id" => record.fetch("request_id"), "service_completion_digest" => completion_digest,
|
|
95
|
-
"service_receipt_digest" => Atoms::EvidenceDigest.digest(receipt), "service_evidence" => receipt.fetch("evidence"),
|
|
96
|
-
"producer" => {"actor" => "service:#{record.fetch('executor_uid')}", "role" => "service", "runtime" => "herdr"}}
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../molecules/evidence_journal"
|
|
4
|
-
require_relative "../molecules/canonical_evidence"
|
|
5
|
-
require_relative "service_cleanup_evidence"
|
|
6
|
-
require_relative "service_cleanup_dispatch"
|
|
7
|
-
require_relative "service_publication_evidence"
|
|
8
|
-
require_relative "../molecules/execution_scope_lineage"
|
|
9
|
-
|
|
10
|
-
module Ace
|
|
11
|
-
module Assign
|
|
12
|
-
module Authority
|
|
13
|
-
# One source-owned context for initial and retained service evidence.
|
|
14
|
-
# The journal supplies the record and transient pending view; callers
|
|
15
|
-
# never supply an alternate reader, artifact path or trusted flag.
|
|
16
|
-
class ServiceEvidence
|
|
17
|
-
include ServiceCleanupEvidence
|
|
18
|
-
include ServiceCleanupDispatch
|
|
19
|
-
include ServicePublicationEvidence
|
|
20
|
-
FIELDS = (Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS +
|
|
21
|
-
%w[dispatch_ticket_id claim_binding candidate_generation claim_generation policy_digest]).freeze
|
|
22
|
-
CHALLENGE_FIELDS = %w[version request_id input_digest claim_binding dispatch_ticket_id no_effect_challenge
|
|
23
|
-
challenge_generation failure_event_digest failure_generation].freeze
|
|
24
|
-
INSPECTION_FIELDS = %w[version request_id input_digest claim_binding no_effect_challenge challenge_generation
|
|
25
|
-
challenge_event_digest failure_event_digest failure_generation target dispatch_phase effect_absent
|
|
26
|
-
handler_terminated writers_absent].freeze
|
|
27
|
-
|
|
28
|
-
def initialize(journal:, cleanup_artifacts: nil)
|
|
29
|
-
@journal = journal
|
|
30
|
-
@canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
31
|
-
@cleanup_artifacts = cleanup_artifacts
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Executor-only projection from the same authenticated original record;
|
|
35
|
-
# selectors never come from the receiver or a caller-supplied target.
|
|
36
|
-
def settlement_context(record, commit: @journal.ref_value, read_view: nil)
|
|
37
|
-
pending = {commit: commit, service_read_view: read_view}
|
|
38
|
-
context(record, pending: pending)
|
|
39
|
-
challenge = challenge!(record, pending: pending, current: false) if
|
|
40
|
-
%w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| record.key?(key) }
|
|
41
|
-
proof = cleanup_dispatch_context!(record, commit: commit, pending: pending) if
|
|
42
|
-
record.values_at("operation", "dispatch_phase") == ["prune-preserved-workspace", "dispatch_started"]
|
|
43
|
-
settlement_projection(record, pending, challenge, proof)
|
|
44
|
-
rescue KeyError, TypeError
|
|
45
|
-
raise AttemptErrors::EvidenceUnavailable, "original service settlement context is incomplete"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def context(record, no_effect: false, pending: nil)
|
|
49
|
-
binding = FIELDS.to_h { |key| [key, record.fetch(key)] }
|
|
50
|
-
if record.fetch("operation") == "prune-preserved-workspace" && record["dispatch_phase"] == "dispatch_started"
|
|
51
|
-
binding["operation_owner_binding"] = record.fetch("operation_owner_binding")
|
|
52
|
-
executor = record.fetch("executor_process_binding")
|
|
53
|
-
boot = record.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
|
|
54
|
-
Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
|
|
55
|
-
raise AttemptErrors::EvidenceUnavailable, "original cleanup executor differs" unless executor.fetch("uid") == record.fetch("executor_uid")
|
|
56
|
-
binding["executor_process_binding"] = executor
|
|
57
|
-
end
|
|
58
|
-
if record["operation"] == "publish" && record["dispatch_phase"] != "issued"
|
|
59
|
-
executor = record.fetch("executor_process_binding")
|
|
60
|
-
boot = record.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
|
|
61
|
-
Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
|
|
62
|
-
raise AttemptErrors::EvidenceUnavailable, "original publication executor differs" unless executor.fetch("uid") == record.fetch("executor_uid")
|
|
63
|
-
binding["executor_process_binding"] = executor
|
|
64
|
-
end
|
|
65
|
-
unless %w[claim_binding policy_digest].all? { |key| binding[key].is_a?(String) && binding[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
66
|
-
%w[candidate_generation claim_generation].all? { |key| binding[key].is_a?(Integer) && binding[key].positive? } &&
|
|
67
|
-
binding["dispatch_ticket_id"].is_a?(String) && binding["dispatch_ticket_id"].match?(Molecules::JournalMutation::ID)
|
|
68
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical dispatch binding is invalid"
|
|
69
|
-
end
|
|
70
|
-
if no_effect
|
|
71
|
-
return context_for_challenge(record, challenge!(record, pending: pending))
|
|
72
|
-
end
|
|
73
|
-
{kind: "service", project_id: record.fetch("project_id"), assignment_id: record.fetch("assignment_id"),
|
|
74
|
-
attempt_id: record.fetch("attempt_id"), peer_uid: record.fetch("executor_uid"), binding: binding,
|
|
75
|
-
request_id_or_event_id: record.fetch("request_id"), generation: record.fetch("claim_generation")}
|
|
76
|
-
rescue KeyError
|
|
77
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical service evidence binding is incomplete"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Exact original challenge and failure selected from the same immutable
|
|
81
|
-
# canonical prefix as its import. Pending events are the journal owner's
|
|
82
|
-
# held CAS plan, never caller-supplied alternative history.
|
|
83
|
-
def challenge!(record, pending: nil, current: true)
|
|
84
|
-
context(record)
|
|
85
|
-
selector = record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
|
|
86
|
-
unless selector.keys.sort == %w[no_effect_challenge challenge_generation challenge_event_digest].sort &&
|
|
87
|
-
selector["no_effect_challenge"].is_a?(String) && selector["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
88
|
-
selector["challenge_generation"].is_a?(Integer) && selector["challenge_generation"].positive? &&
|
|
89
|
-
selector["challenge_event_digest"].is_a?(String) && selector["challenge_event_digest"].match?(/\A[0-9a-f]{64}\z/)
|
|
90
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical challenge selector is invalid"
|
|
91
|
-
end
|
|
92
|
-
commit = pending && pending[:commit] || @journal.ref_value
|
|
93
|
-
events = if pending && pending[:pending_events]
|
|
94
|
-
pending.fetch(:current_events) + pending.fetch(:pending_events)
|
|
95
|
-
else
|
|
96
|
-
service_read_events(record, pending)
|
|
97
|
-
end
|
|
98
|
-
unless Models::EvidenceEvent.chain_valid?(events)
|
|
99
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical challenge chain is corrupt"
|
|
100
|
-
end
|
|
101
|
-
selected = events.select { |event| event["digest"] == record.fetch("challenge_event_digest") }
|
|
102
|
-
unless selected.size == 1 && selected.first["type"] == "service_no_effect_challenge"
|
|
103
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical challenge event is missing"
|
|
104
|
-
end
|
|
105
|
-
challenge = selected.first
|
|
106
|
-
payload = challenge.fetch("payload")
|
|
107
|
-
before = events.take_while { |event| event["digest"] != challenge.fetch("digest") }
|
|
108
|
-
failures = events.select do |event|
|
|
109
|
-
%w[service_claim service_transition].include?(event["type"]) &&
|
|
110
|
-
event.dig("payload", "request_id") == record.fetch("request_id") &&
|
|
111
|
-
%w[uncertain failed].include?(event.dig("payload", "state"))
|
|
112
|
-
end
|
|
113
|
-
failure = before.reverse.find { |event| failures.include?(event) }
|
|
114
|
-
failure_prefix = before.take_while { |event| event["digest"] != failure&.fetch("digest") }
|
|
115
|
-
generation = failure_prefix.count { |event| event["type"] == "authority_mutation" } + 1
|
|
116
|
-
unless payload.is_a?(Hash) && payload.keys.sort == CHALLENGE_FIELDS.sort &&
|
|
117
|
-
payload["version"].is_a?(Integer) && payload["version"] == 1 &&
|
|
118
|
-
%w[challenge_generation failure_generation].all? { |key| payload[key].is_a?(Integer) && payload[key].positive? } &&
|
|
119
|
-
payload["no_effect_challenge"].is_a?(String) && payload["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
120
|
-
%w[request_id input_digest claim_binding dispatch_ticket_id no_effect_challenge challenge_generation].all? { |key| payload[key] == record[key] } &&
|
|
121
|
-
failure && (!current || failures.last == failure) && before.any? { |event| event["digest"] == failure.fetch("digest") } &&
|
|
122
|
-
payload["failure_event_digest"] == failure.fetch("digest") && payload["failure_generation"] == generation &&
|
|
123
|
-
payload["challenge_generation"] == before.count { |event| event["type"] == "authority_mutation" } + 1
|
|
124
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical challenge or current failure binding differs"
|
|
125
|
-
end
|
|
126
|
-
position = events.index(challenge)
|
|
127
|
-
if pending && pending[:pending_events]&.any? { |event| event["digest"] == challenge.fetch("digest") }
|
|
128
|
-
unless pending[:operation] == "claim_service_settlement"
|
|
129
|
-
raise AttemptErrors::EvidenceUnavailable, "pending challenge has no fixed authority owner"
|
|
130
|
-
end
|
|
131
|
-
else
|
|
132
|
-
update, accepted = events.values_at(position + 1, position + 2)
|
|
133
|
-
expected_params = record.slice("mapping_id", "assignment_id", "attempt_id", "request_id", "candidate_generation").merge(
|
|
134
|
-
"head" => record.fetch("candidate_head"), "expected_generation" => payload.fetch("challenge_generation") - 1)
|
|
135
|
-
unless update && update["type"] == "service_challenge" && update.dig("payload", "request_id") == record.fetch("request_id") &&
|
|
136
|
-
accepted && accepted["type"] == "authority_mutation" && accepted.dig("payload", "operation") == "claim_service_settlement" &&
|
|
137
|
-
accepted.dig("payload", "assignment_id") == record.fetch("assignment_id") && accepted.dig("payload", "attempt_id") == record.fetch("attempt_id") &&
|
|
138
|
-
accepted.dig("payload", "parameters_digest") == Atoms::EvidenceDigest.digest(expected_params) &&
|
|
139
|
-
accepted.dig("payload", "data", "generation").is_a?(Integer) &&
|
|
140
|
-
accepted.dig("payload", "data", "generation") == payload.fetch("challenge_generation") &&
|
|
141
|
-
accepted.dig("payload", "data", "reconciliation_challenge", "challenge_generation").is_a?(Integer) &&
|
|
142
|
-
accepted.dig("payload", "data", "request_id") == record.fetch("request_id") &&
|
|
143
|
-
accepted.dig("payload", "data", "reconciliation_challenge") == record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
|
|
144
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical challenge lacks its accepted authority mutation"
|
|
145
|
-
end
|
|
146
|
-
commits = service_read_introductions(record,
|
|
147
|
-
[challenge.fetch("digest"), failure.fetch("digest"), update.fetch("digest"), accepted.fetch("digest")], pending)
|
|
148
|
-
unless commits.fetch(challenge.fetch("digest")) == commits.fetch(update.fetch("digest")) &&
|
|
149
|
-
commits.fetch(challenge.fetch("digest")) == commits.fetch(accepted.fetch("digest"))
|
|
150
|
-
raise AttemptErrors::EvidenceUnavailable, "challenge record and authority acceptance were not atomic"
|
|
151
|
-
end
|
|
152
|
-
original = @journal.service_request(record.fetch("request_id"), commit: commits.fetch(challenge.fetch("digest")))
|
|
153
|
-
unless original && %w[uncertain failed].include?(original["state"]) &&
|
|
154
|
-
update.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(original) &&
|
|
155
|
-
(FIELDS + %w[mapping_id service_id caller_uid authorization worker_process_binding launch_ticket reservation_generation dispatch_phase]).all? { |key| original[key] == record[key] } &&
|
|
156
|
-
original.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest") ==
|
|
157
|
-
record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
|
|
158
|
-
raise AttemptErrors::EvidenceUnavailable, "challenge accepted record differs from original binding"
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
challenge
|
|
162
|
-
rescue KeyError, TypeError, NoMethodError
|
|
163
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical challenge binding is incomplete"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def inspection!(content, record, challenge)
|
|
167
|
-
unless content.is_a?(String) && content.bytesize <= Molecules::CanonicalEvidence::MAX_ARTIFACT_BYTES
|
|
168
|
-
raise AttemptErrors::EvidenceUnavailable, "no-effect inspection exceeds artifact bounds"
|
|
169
|
-
end
|
|
170
|
-
text = content.dup.force_encoding(Encoding::UTF_8)
|
|
171
|
-
raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is invalid UTF8" unless text.valid_encoding?
|
|
172
|
-
lines = text.lines.filter_map { |line| line.delete_prefix("ace-service-no-effect ").chomp if line.start_with?("ace-service-no-effect ") }
|
|
173
|
-
raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is missing or repeated" unless lines.size == 1
|
|
174
|
-
inspection = JSON.parse(lines.first, create_additions: false, max_nesting: 8,
|
|
175
|
-
allow_duplicate_key: false, allow_comments: false)
|
|
176
|
-
inspection_projection!(inspection, record, challenge)
|
|
177
|
-
rescue JSON::ParserError, KeyError, TypeError
|
|
178
|
-
raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is malformed"
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def inspection_projection!(inspection, record, challenge)
|
|
182
|
-
expected = challenge.fetch("payload").slice("request_id", "input_digest", "claim_binding", "no_effect_challenge",
|
|
183
|
-
"challenge_generation", "failure_event_digest", "failure_generation").merge("version" => 1,
|
|
184
|
-
"challenge_event_digest" => challenge.fetch("digest"), "target" => record.fetch("target"),
|
|
185
|
-
"dispatch_phase" => record.fetch("dispatch_phase"), "effect_absent" => true,
|
|
186
|
-
"handler_terminated" => true, "writers_absent" => true)
|
|
187
|
-
unless inspection.is_a?(Hash) && inspection.keys.sort == INSPECTION_FIELDS.sort &&
|
|
188
|
-
%w[version challenge_generation failure_generation].all? { |key| inspection[key].is_a?(Integer) } &&
|
|
189
|
-
inspection == expected
|
|
190
|
-
raise AttemptErrors::EvidenceUnavailable, "no-effect inspection differs from original challenge"
|
|
191
|
-
end
|
|
192
|
-
true
|
|
193
|
-
rescue KeyError, TypeError
|
|
194
|
-
raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is malformed"
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def call(reference, record, state, pending)
|
|
198
|
-
if reference.is_a?(Array)
|
|
199
|
-
return publication_collection!(reference, record, state, pending) if record["operation"] == "publish"
|
|
200
|
-
return cleanup_collection!(reference, record, state, pending)
|
|
201
|
-
end
|
|
202
|
-
no_effect = state == "failed-settled"
|
|
203
|
-
expected = context(record, no_effect: no_effect, pending: pending)
|
|
204
|
-
bytes = if pending && pending[:pending_events]
|
|
205
|
-
@canonical.read_pending(reference, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
|
|
206
|
-
else
|
|
207
|
-
@canonical.read(reference, **expected, commit: pending && pending[:commit] || @journal.ref_value)
|
|
208
|
-
end
|
|
209
|
-
if no_effect
|
|
210
|
-
challenge = challenge!(record, pending: pending)
|
|
211
|
-
inspection!(bytes, record, challenge)
|
|
212
|
-
end
|
|
213
|
-
bytes
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
private
|
|
217
|
-
|
|
218
|
-
def settlement_projection(record, pending, challenge, proof)
|
|
219
|
-
selected = nil
|
|
220
|
-
if challenge
|
|
221
|
-
latest_failure = service_read_events(record, pending).reverse.find do |event|
|
|
222
|
-
event["attempt_id"] == record.fetch("attempt_id") &&
|
|
223
|
-
%w[service_claim service_transition].include?(event["type"]) &&
|
|
224
|
-
event.dig("payload", "request_id") == record.fetch("request_id") &&
|
|
225
|
-
%w[uncertain failed].include?(event.dig("payload", "state"))
|
|
226
|
-
end
|
|
227
|
-
if latest_failure && latest_failure.fetch("digest") == challenge.dig("payload", "failure_event_digest")
|
|
228
|
-
selected = challenge.fetch("payload").slice("request_id", "input_digest", "claim_binding",
|
|
229
|
-
"no_effect_challenge", "challenge_generation", "failure_event_digest", "failure_generation")
|
|
230
|
-
.merge("challenge_event_digest" => challenge.fetch("digest"))
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
execution = record.slice("mapping_id", "candidate_generation", "claim_binding", "dispatch_phase")
|
|
234
|
-
.merge("head" => record.fetch("candidate_head"))
|
|
235
|
-
if record["operation"] == "prune-preserved-workspace" && record["dispatch_phase"] == "dispatch_started"
|
|
236
|
-
execution.merge!(record.slice("operation_owner_binding", "executor_process_binding"))
|
|
237
|
-
execution.merge!(proof.slice("request_event_digest", "dispatch_event_digest"))
|
|
238
|
-
end
|
|
239
|
-
value = {"version" => 1, "request" => record.slice(*Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS),
|
|
240
|
-
"execution" => execution, "challenge" => selected}
|
|
241
|
-
freeze_projection(JSON.parse(JSON.generate(value)))
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def context_for_challenge(record, selection)
|
|
245
|
-
expected = context(record)
|
|
246
|
-
binding = expected.fetch(:binding)
|
|
247
|
-
binding.merge!(record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest"))
|
|
248
|
-
binding.merge!(selection.fetch("payload").slice("failure_event_digest", "failure_generation"))
|
|
249
|
-
binding["dispatch_phase"] = record.fetch("dispatch_phase")
|
|
250
|
-
expected
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def service_read_view(pending)
|
|
254
|
-
view = pending && pending[:service_read_view]
|
|
255
|
-
return nil unless view
|
|
256
|
-
unless view.is_a?(Molecules::EvidenceJournal::ServiceReadView) && view.journal.equal?(@journal) &&
|
|
257
|
-
view.commit == pending.fetch(:commit) && view.inventory.fetch("commit") == view.commit
|
|
258
|
-
raise AttemptErrors::EvidenceUnavailable, "service read view differs from its selected owner"
|
|
259
|
-
end
|
|
260
|
-
view
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def service_read_events(record, pending)
|
|
264
|
-
view = service_read_view(pending)
|
|
265
|
-
selected = view ? view.inventory.fetch("events").fetch(record.fetch("assignment_id")) :
|
|
266
|
-
@journal.read_events(record.fetch("assignment_id"), commit: pending && pending[:commit] || @journal.ref_value)
|
|
267
|
-
selected.select { |event| event.fetch("attempt_id") == record.fetch("attempt_id") }
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def service_read_introductions(record, selectors, pending)
|
|
271
|
-
view = service_read_view(pending)
|
|
272
|
-
return @journal.event_commits!(assignment_id: record.fetch("assignment_id"), event_digests: selectors,
|
|
273
|
-
commit: pending && pending[:commit] || @journal.ref_value) unless view
|
|
274
|
-
facts = view.inventory.fetch("introductions").fetch(record.fetch("assignment_id"))
|
|
275
|
-
selectors.to_h { |selector| [selector, facts.fetch(selector)] }.freeze
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def freeze_projection(value)
|
|
279
|
-
value.each_value { |child| freeze_projection(child) } if value.is_a?(Hash)
|
|
280
|
-
value.each { |child| freeze_projection(child) } if value.is_a?(Array)
|
|
281
|
-
value.freeze
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
end
|