ace-assign 0.63.1 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +398 -0
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +59 -0
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +286 -0
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +187 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +38 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +56 -4
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -0,0 +1,101 @@
|
|
|
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
|
|
@@ -0,0 +1,286 @@
|
|
|
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
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
# Publication-specific facts remain artifacts of the existing service
|
|
7
|
+
# record and canonical import owner, not a second publication journal.
|
|
8
|
+
module ServicePublicationEvidence
|
|
9
|
+
PUBLICATION_NEED_FIELDS = %w[schema classification request_id input_digest claim_binding
|
|
10
|
+
candidate_generation head registry gem_name version artifact_digest dispatch_event_digest previous_challenge_digest].freeze
|
|
11
|
+
PUBLICATION_FIELDS = %w[publication_challenge_digest publication_challenge_ref publication_challenge_event_digest
|
|
12
|
+
publication_challenge_generation publication_issuing_event_digest].freeze
|
|
13
|
+
|
|
14
|
+
def publication_result!(bytes, record, state:)
|
|
15
|
+
value = JSON.parse(bytes, allow_duplicate_key: false, allow_nan: false, max_nesting: 16)
|
|
16
|
+
fields = %w[schema publication target classification code request_id input_digest claim_binding]
|
|
17
|
+
publication = value.fetch("publication")
|
|
18
|
+
allowed = state == "succeeded" ? ["registry_exact_artifact"] :
|
|
19
|
+
%w[provider_rejected_non_otp executor_credentials_unavailable registry_artifact_conflict]
|
|
20
|
+
unless bytes.bytesize.between?(1, 65536) && value.is_a?(Hash) && value.keys.sort == fields.sort &&
|
|
21
|
+
value["schema"] == "ace.publication-result/v1" && value["classification"] == state && allowed.include?(value["code"]) &&
|
|
22
|
+
%w[request_id input_digest claim_binding target].all? { |key| value[key] == record[key] } &&
|
|
23
|
+
publication.is_a?(Hash) && publication.keys.sort == %w[gem_name version head registry artifact_relative_path].sort &&
|
|
24
|
+
publication["head"] == record.fetch("candidate_head") && publication["registry"] == "https://rubygems.org" &&
|
|
25
|
+
publication["gem_name"].is_a?(String) && publication["gem_name"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
|
|
26
|
+
publication["version"].is_a?(String) && publication["version"].bytesize.between?(1, 128) &&
|
|
27
|
+
publication["version"].match?(/\A[0-9]+(?:\.[0-9A-Za-z]+)*(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?\z/) &&
|
|
28
|
+
"rubygems:#{publication['gem_name']}:#{publication['version']}" == record.dig("target", "resource") &&
|
|
29
|
+
publication["artifact_relative_path"].is_a?(String) && publication["artifact_relative_path"].bytesize.between?(1, 4096) &&
|
|
30
|
+
publication["artifact_relative_path"].end_with?(".gem") &&
|
|
31
|
+
publication["artifact_relative_path"].split("/", -1).all? { |part| part.match?(/\A[a-zA-Z0-9_.-]+\z/) && !%w[. ..].include?(part) }
|
|
32
|
+
raise AttemptErrors::ReceiptRejected, "publication result differs from original artifact"
|
|
33
|
+
end
|
|
34
|
+
value
|
|
35
|
+
rescue JSON::ParserError, KeyError, TypeError, NoMethodError
|
|
36
|
+
raise AttemptErrors::ReceiptRejected, "publication result is invalid"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def publication_collection!(references, record, state, pending)
|
|
40
|
+
unless %w[succeeded failed].include?(state) && references.size == 1
|
|
41
|
+
raise AttemptErrors::ReceiptRejected, "publication requires one exact result artifact"
|
|
42
|
+
end
|
|
43
|
+
expected = context(record, pending: pending)
|
|
44
|
+
bytes = if pending && pending[:pending_events]
|
|
45
|
+
@canonical.read_pending(references.first, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
|
|
46
|
+
else
|
|
47
|
+
@canonical.read(references.first, **expected, commit: pending && pending[:commit] || @journal.ref_value)
|
|
48
|
+
end
|
|
49
|
+
publication_result!(bytes, record, state: state)
|
|
50
|
+
[bytes].freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def publication_need!(bytes, record, previous:)
|
|
54
|
+
value = JSON.parse(bytes, allow_duplicate_key: false, allow_comments: false,
|
|
55
|
+
allow_nan: false, create_additions: false, max_nesting: 16)
|
|
56
|
+
unless bytes.bytesize.between?(1, 65536) && value.is_a?(Hash) &&
|
|
57
|
+
value.keys.sort == PUBLICATION_NEED_FIELDS.sort && value["schema"] == "ace.publication-otp-need/v1" &&
|
|
58
|
+
%w[otp-required otp-rejected otp-expired].include?(value["classification"]) &&
|
|
59
|
+
%w[request_id input_digest claim_binding candidate_generation].all? { |key| value[key] == record[key] } &&
|
|
60
|
+
value["head"] == record["candidate_head"] && value["registry"] == "https://rubygems.org" &&
|
|
61
|
+
value["previous_challenge_digest"] == previous && value["artifact_digest"] == record.dig("target", "artifact_digest") &&
|
|
62
|
+
"rubygems:#{value['gem_name']}:#{value['version']}" == record.dig("target", "resource") &&
|
|
63
|
+
value["dispatch_event_digest"].is_a?(String) && value["dispatch_event_digest"].match?(/\A[0-9a-f]{64}\z/)
|
|
64
|
+
raise AttemptErrors::ReceiptRejected, "publication OTP need differs from original dispatch"
|
|
65
|
+
end
|
|
66
|
+
value
|
|
67
|
+
rescue JSON::ParserError, KeyError, TypeError
|
|
68
|
+
raise AttemptErrors::ReceiptRejected, "publication OTP need is invalid"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def publication_challenge!(record, pending: nil)
|
|
72
|
+
context(record)
|
|
73
|
+
commit = pending && pending[:commit] || @journal.ref_value
|
|
74
|
+
events = pending && pending[:pending_events] ? pending.fetch(:current_events) + pending.fetch(:pending_events) :
|
|
75
|
+
@journal.read_events(record.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == record.fetch("attempt_id") }
|
|
76
|
+
unless Models::EvidenceEvent.chain_valid?(events)
|
|
77
|
+
raise AttemptErrors::EvidenceUnavailable, "publication challenge chain is corrupt"
|
|
78
|
+
end
|
|
79
|
+
event = events.find { |row| row["digest"] == record["publication_challenge_event_digest"] }
|
|
80
|
+
unless event && event["type"] == "service_publication_challenge" &&
|
|
81
|
+
event.dig("payload", "challenge_digest") == record["publication_challenge_digest"] &&
|
|
82
|
+
event.dig("payload", "request_id") == record["request_id"] &&
|
|
83
|
+
event.dig("payload", "generation") == record["publication_challenge_generation"]
|
|
84
|
+
raise AttemptErrors::EvidenceUnavailable, "publication challenge introduction is unavailable"
|
|
85
|
+
end
|
|
86
|
+
reference = record.fetch("publication_challenge_ref")
|
|
87
|
+
bytes = if pending && pending[:pending_events]
|
|
88
|
+
@canonical.read_pending(reference, **context(record), current_events: pending.fetch(:current_events),
|
|
89
|
+
pending_events: pending.fetch(:pending_events), blobs: pending.fetch(:blobs), commit: commit)
|
|
90
|
+
else
|
|
91
|
+
@canonical.read(reference, **context(record), commit: commit)
|
|
92
|
+
end
|
|
93
|
+
unless Digest::SHA256.hexdigest(bytes) == record.fetch("publication_challenge_digest")
|
|
94
|
+
raise AttemptErrors::EvidenceUnavailable, "publication challenge digest differs"
|
|
95
|
+
end
|
|
96
|
+
need = publication_need!(bytes, record, previous: event.dig("payload", "previous_challenge_digest"))
|
|
97
|
+
dispatch = events.find { |row| row["digest"] == need.fetch("dispatch_event_digest") }
|
|
98
|
+
unless dispatch && dispatch["type"] == "authority_mutation" && dispatch.dig("payload", "operation") == "begin_dispatch" &&
|
|
99
|
+
dispatch.dig("payload", "data", "request_id") == record.fetch("request_id") &&
|
|
100
|
+
dispatch.dig("payload", "data", "executor_process_binding") == record.fetch("executor_process_binding")
|
|
101
|
+
raise AttemptErrors::EvidenceUnavailable, "publication need has no original executor dispatch"
|
|
102
|
+
end
|
|
103
|
+
need
|
|
104
|
+
rescue KeyError, TypeError
|
|
105
|
+
raise AttemptErrors::EvidenceUnavailable, "publication challenge evidence is incomplete"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
# Structural admission of source-owned pins. Artifact authentication belongs
|
|
7
|
+
# to the selected entry owner; these references are never an execution grant.
|
|
8
|
+
module TaskContextEntry
|
|
9
|
+
LIMITS = {"manifest" => 65_536, "wrapper" => 33_554_432}.freeze
|
|
10
|
+
|
|
11
|
+
def self.validate!(value)
|
|
12
|
+
unless value.is_a?(Hash) && value.keys.sort == LIMITS.keys.sort
|
|
13
|
+
raise ArgumentError, "task context entry fields differ"
|
|
14
|
+
end
|
|
15
|
+
LIMITS.each do |key, limit|
|
|
16
|
+
reference = value[key]
|
|
17
|
+
unless reference.is_a?(Hash) && reference.keys.sort == %w[bytes path sha256] &&
|
|
18
|
+
reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, limit) &&
|
|
19
|
+
reference["sha256"].is_a?(String) && reference["sha256"].match?(/\A[0-9a-f]{64}\z/) &&
|
|
20
|
+
reference["path"].is_a?(String) && reference["path"].encoding == Encoding::UTF_8 &&
|
|
21
|
+
reference["path"].valid_encoding? && reference["path"].bytesize.between?(2, 4096) &&
|
|
22
|
+
!reference["path"].include?("\0") && reference["path"].start_with?("/") &&
|
|
23
|
+
File.expand_path(reference["path"]) == reference["path"]
|
|
24
|
+
raise ArgumentError, "task context entry reference differs"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|