ace-assign 0.70.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 -2099
- 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 -1
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- 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 -161
- 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/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/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_transfer.rb +0 -327
- 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/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -455
- 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 -207
- 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_settlement.rb +0 -152
- data/lib/ace/assign/authority/endcap_services.rb +0 -598
- 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/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 -94
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
- 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_results.rb +0 -196
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- 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 -304
- 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/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 -187
- 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 -153
- 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 -1912
- 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 -273
- 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 -314
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- 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,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
|
|
@@ -1,110 +0,0 @@
|
|
|
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
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require_relative "transfer_codec"
|
|
6
|
-
require_relative "../molecules/evidence_journal"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Assign
|
|
10
|
-
module Authority
|
|
11
|
-
# Shared endpoint lifetime and bounded wire protocol. Concrete source
|
|
12
|
-
# compositions authenticate their own installed principals before dispatch.
|
|
13
|
-
# Protected deployment/kernel requirements remain in Server.
|
|
14
|
-
class SocketServer
|
|
15
|
-
CONNECTIONS = 16
|
|
16
|
-
|
|
17
|
-
def initialize(service:, lifecycle:, services: [])
|
|
18
|
-
unless services.is_a?(Array) && services.uniq.size == services.size &&
|
|
19
|
-
services.all? { |owned| owned.respond_to?(:run) && owned.respond_to?(:stop) }
|
|
20
|
-
raise ArgumentError, "authority services require distinct source-owned run/stop lifetimes"
|
|
21
|
-
end
|
|
22
|
-
@service, @lifecycle, @services = service, lifecycle, services.dup.freeze
|
|
23
|
-
@service_threads = []
|
|
24
|
-
@activation_gates = []
|
|
25
|
-
@mutex = Mutex.new
|
|
26
|
-
@connections = []
|
|
27
|
-
@handlers = []
|
|
28
|
-
@transfers = 0
|
|
29
|
-
@stopped = false
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def serve
|
|
33
|
-
verify_listener!
|
|
34
|
-
directory = File.dirname(@service.fetch("socket_path"))
|
|
35
|
-
wire.root_path!(directory, directory: true, owner: @service.fetch("uid"))
|
|
36
|
-
lock = File.open(File.join(directory, "listener.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
|
|
37
|
-
unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
38
|
-
raise AttemptErrors::Conflict, "assignment authority already owns its listener"
|
|
39
|
-
end
|
|
40
|
-
path = @service.fetch("socket_path")
|
|
41
|
-
if File.exist?(path) || File.symlink?(path)
|
|
42
|
-
raise AttemptErrors::Conflict, "existing authority endpoint requires verified installer recovery"
|
|
43
|
-
end
|
|
44
|
-
@listener = UNIXServer.new(path)
|
|
45
|
-
@endpoint = wire.socket_identity(path)
|
|
46
|
-
File.chmod(0o660, path)
|
|
47
|
-
start_services!
|
|
48
|
-
verify_services!
|
|
49
|
-
@activation_gates.each { |gate| gate << :activate }
|
|
50
|
-
until @stopped
|
|
51
|
-
verify_services!
|
|
52
|
-
ready = IO.select([@listener], nil, nil, 0.25)
|
|
53
|
-
next unless ready
|
|
54
|
-
socket = @listener.accept_nonblock(exception: false)
|
|
55
|
-
next if socket == :wait_readable
|
|
56
|
-
admitted = @mutex.synchronize do
|
|
57
|
-
if @connections.size < CONNECTIONS
|
|
58
|
-
@connections << socket
|
|
59
|
-
true
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
unless admitted
|
|
63
|
-
refusal(socket, "busy")
|
|
64
|
-
socket.close
|
|
65
|
-
next
|
|
66
|
-
end
|
|
67
|
-
@mutex.synchronize do
|
|
68
|
-
@handlers.reject! { |handler| !handler.alive? }
|
|
69
|
-
@handlers << Thread.new(socket) { |connection| receive(connection) }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
rescue IOError, Errno::EBADF
|
|
73
|
-
raise unless @stopped
|
|
74
|
-
ensure
|
|
75
|
-
begin
|
|
76
|
-
stop
|
|
77
|
-
stop_services!
|
|
78
|
-
ensure
|
|
79
|
-
# Closing a stream does not cancel an admitted transaction. Hold
|
|
80
|
-
# the owner lock and endpoint until every handler has ended.
|
|
81
|
-
begin
|
|
82
|
-
@mutex.synchronize { @handlers.dup }.each(&:join)
|
|
83
|
-
@lifecycle.close if @endpoint && @lifecycle.respond_to?(:close)
|
|
84
|
-
ensure
|
|
85
|
-
begin
|
|
86
|
-
File.unlink(path) if @endpoint && same_endpoint?(path)
|
|
87
|
-
ensure
|
|
88
|
-
lock&.close
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def request_stop
|
|
95
|
-
@stopped = true
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def stop
|
|
99
|
-
@stopped = true
|
|
100
|
-
@listener&.close unless @listener&.closed?
|
|
101
|
-
@mutex.synchronize { @connections.each { |socket| socket.close unless socket.closed? } }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
# Installed HITL and merge listeners share the assignment owner lock.
|
|
107
|
-
# Every selected boundary must be ready before assignment ingress opens;
|
|
108
|
-
# a stopped boundary closes admission until the complete owner restarts.
|
|
109
|
-
def start_services!
|
|
110
|
-
@services.each do |owned|
|
|
111
|
-
ready = Queue.new
|
|
112
|
-
activation = Queue.new
|
|
113
|
-
@activation_gates << activation
|
|
114
|
-
thread = Thread.new do
|
|
115
|
-
owned.run(on_ready: lambda do
|
|
116
|
-
ready << :ready
|
|
117
|
-
unless activation.pop == :activate
|
|
118
|
-
raise Ace::Runtime::RuntimeUnavailableError, "authority startup was cancelled"
|
|
119
|
-
end
|
|
120
|
-
end)
|
|
121
|
-
rescue Exception => error
|
|
122
|
-
ready << error
|
|
123
|
-
ensure
|
|
124
|
-
ready << :stopped
|
|
125
|
-
end
|
|
126
|
-
thread.report_on_exception = false
|
|
127
|
-
@service_threads << [owned, thread]
|
|
128
|
-
result = ready.pop(timeout: 5)
|
|
129
|
-
raise Ace::Runtime::RuntimeUnavailableError, "authority service startup did not become ready" unless result == :ready || result.is_a?(Exception)
|
|
130
|
-
raise result if result.is_a?(Exception)
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def verify_services!
|
|
135
|
-
unless @service_threads.all? { |_, thread| thread.alive? }
|
|
136
|
-
raise Ace::Runtime::RuntimeUnavailableError, "an owned service stopped before its authority"
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def stop_services!
|
|
141
|
-
errors = []
|
|
142
|
-
@activation_gates.each { |gate| gate << :cancel }
|
|
143
|
-
@service_threads.reverse_each do |owned, _|
|
|
144
|
-
owned.stop
|
|
145
|
-
rescue Exception => error
|
|
146
|
-
errors << error
|
|
147
|
-
end
|
|
148
|
-
@service_threads.each do |_, thread|
|
|
149
|
-
thread.join
|
|
150
|
-
rescue Exception => error
|
|
151
|
-
errors << error
|
|
152
|
-
end
|
|
153
|
-
raise errors.first unless errors.empty?
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def same_endpoint?(path)
|
|
157
|
-
wire.socket_identity(path) == @endpoint
|
|
158
|
-
rescue StandardError
|
|
159
|
-
false
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def verify_listener!
|
|
163
|
-
raise NotImplementedError, "source composition must verify its installed listener"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def dispatch_request(socket, request, peer, role, deadline, frame_bytes:)
|
|
167
|
-
params = request.fetch("params")
|
|
168
|
-
admitted_transfer = false
|
|
169
|
-
Molecules::EvidenceJournal.with_event_read_operation do
|
|
170
|
-
binding = @lifecycle.transfer_binding(request) if @lifecycle.respond_to?(:transfer_binding)
|
|
171
|
-
if binding
|
|
172
|
-
raise AttemptErrors::UnauthorizedIdentity, "transfer role differs" unless binding.fetch(:roles).include?(role)
|
|
173
|
-
raise ArgumentError, "transfer control header is oversized" if frame_bytes > 16_384
|
|
174
|
-
authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
|
|
175
|
-
admitted_transfer = @mutex.synchronize do
|
|
176
|
-
if @transfers < 2
|
|
177
|
-
@transfers += 1
|
|
178
|
-
true
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
raise AttemptErrors::Conflict, "transfer capacity is busy" unless admitted_transfer
|
|
182
|
-
codec = transfer_codec
|
|
183
|
-
if binding.fetch(:direction) == :upload
|
|
184
|
-
result = codec.receive(socket, descriptor: params.fetch("transfer"), purpose: binding.fetch(:purpose), deadline: deadline) do |input|
|
|
185
|
-
dispatch_lifecycle(request: request, peer: peer, role: role, transfer: input, deadline: deadline)
|
|
186
|
-
end
|
|
187
|
-
else
|
|
188
|
-
result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
|
|
189
|
-
parts = result.fetch(:transfer_parts)
|
|
190
|
-
descriptor = codec.descriptor(parts, purpose: binding.fetch(:purpose))
|
|
191
|
-
result = result.merge(data: result.fetch(:data).merge("transfer" => descriptor))
|
|
192
|
-
end
|
|
193
|
-
else
|
|
194
|
-
raise ArgumentError, "unexpected transfer descriptor" if params.key?("transfer")
|
|
195
|
-
result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
|
|
196
|
-
end
|
|
197
|
-
wire.write(socket, {"status" => "ok", "data" => result.fetch(:data),
|
|
198
|
-
"transport" => {"replayed" => result.fetch(:replayed, false)}}, deadline: deadline, limit: 16_384)
|
|
199
|
-
if binding && binding.fetch(:direction) == :download
|
|
200
|
-
codec.send(socket, parts: parts, descriptor: descriptor, purpose: binding.fetch(:purpose), deadline: deadline)
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
ensure
|
|
204
|
-
@mutex.synchronize { @transfers -= 1 } if admitted_transfer
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def authorize_transfer!(request:, peer:, role:, deadline:)
|
|
208
|
-
@lifecycle.authorize_transfer!(request: request, peer: peer, role: role)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def dispatch_lifecycle(request:, peer:, role:, deadline:, transfer: nil)
|
|
212
|
-
options = {request: request, peer: peer, role: role}
|
|
213
|
-
options[:transfer] = transfer if transfer
|
|
214
|
-
@lifecycle.dispatch(**options)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# These closed read operations have no request body. Half-close proves
|
|
218
|
-
# framing completion before status or artifact data can be exposed.
|
|
219
|
-
def bodyless_read!(socket, deadline)
|
|
220
|
-
loop do
|
|
221
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
222
|
-
raise ArgumentError, "read request framing is incomplete" unless remaining.positive? && IO.select([socket], nil, nil, remaining)
|
|
223
|
-
bytes = socket.read_nonblock(1, exception: false)
|
|
224
|
-
next if bytes == :wait_readable
|
|
225
|
-
raise ArgumentError, "unexpected read request body" unless bytes.nil?
|
|
226
|
-
return
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def transfer_codec
|
|
231
|
-
@mutex.synchronize do
|
|
232
|
-
@transfer_codec ||= begin
|
|
233
|
-
root = @service.fetch("state_root")
|
|
234
|
-
PrivateDirectory.verify!(root)
|
|
235
|
-
spool = File.join(root, "transfers")
|
|
236
|
-
Dir.mkdir(spool, 0o700) unless File.exist?(spool) || File.symlink?(spool)
|
|
237
|
-
TransferCodec.new(root: spool)
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def refusal(socket, code)
|
|
243
|
-
wire.write(socket, {"status" => "error", "error" => {"code" => code, "message" => "Protected authority refused #{code}"}},
|
|
244
|
-
deadline: wire.deadline(0.1))
|
|
245
|
-
rescue StandardError
|
|
246
|
-
nil
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def wire
|
|
250
|
-
Ace::Runtime::Molecules::ProtectedSocket
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|