ace-assign 0.71.0 → 0.73.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +5 -2116
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -4
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +22 -176
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
- data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/command_execution.rb +0 -180
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -457
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -41
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
- data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
- data/lib/ace/assign/authority/ordinary_results.rb +0 -206
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
- data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -309
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
- data/lib/ace/assign/authority/retained_input.rb +0 -53
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -189
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "ace/runtime/molecules/execution_network_selection"
|
|
3
|
-
require "socket"
|
|
4
|
-
require_relative "../errors"
|
|
5
|
-
require "ace/runtime/molecules/readiness_configuration"
|
|
6
|
-
require "ace/runtime/molecules/server_resource_observation"
|
|
7
|
-
require "ace/runtime/molecules/protected_linux"
|
|
8
|
-
require "ace/runtime/molecules/protected_socket"
|
|
9
|
-
require_relative "transfer_codec"
|
|
10
|
-
|
|
11
|
-
module Ace
|
|
12
|
-
module Assign
|
|
13
|
-
module Authority
|
|
14
|
-
# Fixed ExecStartPost action, deliberately independent of Deployment.load.
|
|
15
|
-
class ReadinessHook
|
|
16
|
-
class Files
|
|
17
|
-
def boundary_manifest!(config)
|
|
18
|
-
bytes = nil
|
|
19
|
-
Ace::Runtime::Molecules::ProtectedArtifactSet.new.with do |reader|
|
|
20
|
-
bytes, = reader.read_path!(config.dig("boundary_manifest", "path"), limit: 65_536)
|
|
21
|
-
unless Digest::SHA256.hexdigest(bytes) == config.dig("boundary_manifest", "sha256")
|
|
22
|
-
raise AttemptErrors::EvidenceUnavailable, "readiness boundary bytes differ"
|
|
23
|
-
end
|
|
24
|
-
reader.verify_unchanged!
|
|
25
|
-
end
|
|
26
|
-
bytes = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
27
|
-
raise AttemptErrors::EvidenceUnavailable, "readiness boundary encoding differs" unless bytes.valid_encoding?
|
|
28
|
-
JSON.parse(bytes, create_additions: false, max_nesting: 8, allow_duplicate_key: false, allow_comments: false)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def verify_native!(server, config)
|
|
32
|
-
File.open("/proc/#{server.fetch('pid')}/exe", File::RDONLY) do |executable|
|
|
33
|
-
unless Digest::SHA256.hexdigest(executable.read(268_435_457)) == config.dig("native", "executable_sha256") &&
|
|
34
|
-
File.read("/proc/#{server.fetch('pid')}/cmdline", 16_385).split("\0") == [config.dig("native", "executable"), "server"]
|
|
35
|
-
raise AttemptErrors::EvidenceUnavailable, "readiness native executable differs"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def initialize(slot:, configuration: nil, kernel: Ace::Runtime::Molecules::ProtectedLinux.new,
|
|
42
|
-
resources: nil, wire: Ace::Runtime::Molecules::ProtectedSocket, files: Files.new)
|
|
43
|
-
@configuration = configuration || Ace::Runtime::Molecules::ReadinessConfiguration.load(slot: slot)
|
|
44
|
-
@kernel, @wire = kernel, wire
|
|
45
|
-
@files = files
|
|
46
|
-
@resources = resources || Ace::Runtime::Molecules::ServerResourceObservation.new(kernel: kernel)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def run
|
|
50
|
-
config = @configuration.data
|
|
51
|
-
@kernel.supported!
|
|
52
|
-
hook = @kernel.capture(Process.pid)
|
|
53
|
-
expected = config.fetch("worker")
|
|
54
|
-
unless hook.values_at("uid", "gid", "groups") == expected.values_at("uid", "gid", "groups")
|
|
55
|
-
raise AttemptErrors::UnauthorizedIdentity, "readiness hook principal differs"
|
|
56
|
-
end
|
|
57
|
-
deadline = @wire.deadline(10)
|
|
58
|
-
authority = config.fetch("authority")
|
|
59
|
-
path = authority.fetch("socket_path")
|
|
60
|
-
@wire.root_path!(File.dirname(path), directory: true, owner: authority.fetch("uid"))
|
|
61
|
-
endpoint = @wire.socket_identity(path)
|
|
62
|
-
@wire.connect(path) do |socket|
|
|
63
|
-
peer = @kernel.peer(socket)
|
|
64
|
-
unless peer.values_at("uid", "gid", "groups") == authority.values_at("uid", "gid", "groups") &&
|
|
65
|
-
@wire.socket_identity(path) == endpoint
|
|
66
|
-
raise AttemptErrors::UnauthorizedIdentity, "readiness authority peer differs"
|
|
67
|
-
end
|
|
68
|
-
@wire.write(socket, {"version" => 1, "operation" => "native_readiness", "mutation_id" => nil,
|
|
69
|
-
"project_id" => config.fetch("project_id"), "params" => {"mapping_id" => config.fetch("mapping_id")}},
|
|
70
|
-
deadline: deadline, limit: 16_384)
|
|
71
|
-
challenge = @wire.read(socket, deadline: deadline, limit: 16_384)
|
|
72
|
-
unless challenge.is_a?(Hash) && challenge.keys.sort == %w[challenge_id server_identity version] &&
|
|
73
|
-
challenge["version"] == 1 && challenge["challenge_id"].is_a?(String) &&
|
|
74
|
-
challenge["challenge_id"].match?(/\A[0-9a-f]{64}\z/)
|
|
75
|
-
raise AttemptErrors::EvidenceUnavailable, "private readiness challenge differs"
|
|
76
|
-
end
|
|
77
|
-
manifest = @files.boundary_manifest!(config)
|
|
78
|
-
unless manifest.is_a?(Hash) && manifest.keys.sort == %w[network_installation resources schema slot_id] &&
|
|
79
|
-
manifest["schema"] == "ace.execution-boundary-manifest/v1" && manifest["slot_id"] == config.fetch("slot_id") &&
|
|
80
|
-
manifest["resources"].is_a?(Array) && manifest["resources"].size.between?(1, 64)
|
|
81
|
-
raise AttemptErrors::EvidenceUnavailable, "readiness boundary shape differs"
|
|
82
|
-
end
|
|
83
|
-
Ace::Runtime::Molecules::ExecutionNetworkSelection.validate_static!(manifest.fetch("network_installation"), slot_id: config.fetch("slot_id"))
|
|
84
|
-
manifest.fetch("resources").each do |entry|
|
|
85
|
-
paths = entry.is_a?(Hash) ? entry.values_at("host_path", "view_path") : []
|
|
86
|
-
unless entry.is_a?(Hash) && entry.keys.sort == %w[host_path read_only stage view_path worker_visible] &&
|
|
87
|
-
paths.size == 2 && paths.all? { |path| path.is_a?(String) && path.bytesize.between?(1, 4096) && path.start_with?("/") && !path.include?("\0") && File.expand_path(path) == path } &&
|
|
88
|
-
%w[parent native].include?(entry["stage"]) && [true, false].include?(entry["worker_visible"]) && [true, false].include?(entry["read_only"])
|
|
89
|
-
raise AttemptErrors::EvidenceUnavailable, "readiness resource declaration differs"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
entries = manifest.fetch("resources").select { |entry| entry.fetch("worker_visible") }
|
|
93
|
-
server = challenge.fetch("server_identity")
|
|
94
|
-
@files.verify_native!(server, config)
|
|
95
|
-
report = @resources.observe!(server: server, entries: entries, authority_socket: path)
|
|
96
|
-
report = report.merge("version" => 1, "challenge_id" => challenge.fetch("challenge_id"),
|
|
97
|
-
"server_identity" => challenge.fetch("server_identity"), "resource_observer_identity" => hook)
|
|
98
|
-
bytes = JSON.generate(report)
|
|
99
|
-
codec = TransferCodec.new
|
|
100
|
-
transfer = codec.descriptor([bytes], purpose: :scope_boundary_observation)
|
|
101
|
-
@wire.write(socket, {"version" => 1, "challenge_id" => challenge.fetch("challenge_id"), "transfer" => transfer},
|
|
102
|
-
deadline: deadline, limit: 16_384)
|
|
103
|
-
codec.send(socket, parts: [bytes], descriptor: transfer, purpose: :scope_boundary_observation, deadline: deadline)
|
|
104
|
-
socket.shutdown(Socket::SHUT_WR)
|
|
105
|
-
acknowledgment = @wire.read(socket, deadline: deadline, limit: 16_384)
|
|
106
|
-
unless acknowledgment == {"version" => 1, "challenge_id" => challenge.fetch("challenge_id"), "status" => "ready"}
|
|
107
|
-
raise AttemptErrors::EvidenceUnavailable, "private readiness acknowledgement differs"
|
|
108
|
-
end
|
|
109
|
-
@kernel.live!(hook)
|
|
110
|
-
@kernel.live!(peer)
|
|
111
|
-
raise AttemptErrors::EvidenceUnavailable, "readiness endpoint changed" unless @wire.socket_identity(path) == endpoint
|
|
112
|
-
end
|
|
113
|
-
true
|
|
114
|
-
rescue SystemCallError, IOError, JSON::ParserError, KeyError, TypeError
|
|
115
|
-
raise AttemptErrors::EvidenceUnavailable, "private readiness observation unavailable"
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "digest"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# Receipt-first binary framing. Field names come from source operations,
|
|
10
|
-
# never from the wire. Artifact order and declared digests are exact.
|
|
11
|
-
module ReceiptTransfer
|
|
12
|
-
def self.decode(input:, receipt_sha256:, artifact_field:, reference_key:)
|
|
13
|
-
unless input && input.count.between?(1, 17) && %w[artifacts evidence].include?(artifact_field) &&
|
|
14
|
-
%w[path ref].include?(reference_key) && receipt_sha256.is_a?(String) && receipt_sha256.match?(/\A[0-9a-f]{64}\z/)
|
|
15
|
-
raise AttemptErrors::ReceiptRejected, "receipt transfer binding is invalid"
|
|
16
|
-
end
|
|
17
|
-
bytes = input.bytes(index: 0)
|
|
18
|
-
unless bytes.bytesize.between?(1, 16 * 1024) && Digest::SHA256.hexdigest(bytes) == receipt_sha256
|
|
19
|
-
raise AttemptErrors::ReceiptRejected, "first transfer part must be the exact receipt"
|
|
20
|
-
end
|
|
21
|
-
receipt = JSON.parse(bytes)
|
|
22
|
-
unless receipt.is_a?(Hash) && !Models::ExecutionReceipt.forbidden_field?(receipt)
|
|
23
|
-
raise AttemptErrors::ReceiptRejected, "receipt must contain bounded structured evidence"
|
|
24
|
-
end
|
|
25
|
-
references = receipt.fetch(artifact_field)
|
|
26
|
-
unless references.is_a?(Array) && references.length == input.count - 1 && references.length <= 16 &&
|
|
27
|
-
references.all? { |entry| entry.is_a?(Hash) && entry.keys.sort == [reference_key, "sha256"].sort &&
|
|
28
|
-
entry[reference_key].is_a?(String) && entry[reference_key].bytesize.between?(1, 256) &&
|
|
29
|
-
entry["sha256"].is_a?(String) && entry["sha256"].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
30
|
-
references.map { |entry| entry[reference_key] }.uniq.length == references.length
|
|
31
|
-
raise AttemptErrors::ReceiptRejected, "receipt artifact parts are incomplete or duplicated"
|
|
32
|
-
end
|
|
33
|
-
artifacts = references.each_with_index.map do |reference, index|
|
|
34
|
-
artifact = input.bytes(index: index + 1)
|
|
35
|
-
unless artifact.bytesize <= 64 * 1024 && Digest::SHA256.hexdigest(artifact) == reference.fetch("sha256")
|
|
36
|
-
raise AttemptErrors::ReceiptRejected, "receipt artifact part differs from declaration"
|
|
37
|
-
end
|
|
38
|
-
artifact
|
|
39
|
-
end
|
|
40
|
-
raise AttemptErrors::ReceiptRejected, "receipt evidence exceeds its transfer limit" if artifacts.sum(&:bytesize) > 256 * 1024
|
|
41
|
-
{receipt: receipt, artifacts: artifacts, receipt_sha256: receipt_sha256}
|
|
42
|
-
rescue JSON::ParserError, KeyError
|
|
43
|
-
raise AttemptErrors::ReceiptRejected, "receipt transfer schema is invalid"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "json"
|
|
5
|
-
require "stringio"
|
|
6
|
-
require "zlib"
|
|
7
|
-
require "rubygems/package"
|
|
8
|
-
require_relative "../molecules/canonical_evidence"
|
|
9
|
-
|
|
10
|
-
module Ace
|
|
11
|
-
module Assign
|
|
12
|
-
module Authority
|
|
13
|
-
# One retained gem and the source-owned publication input it binds.
|
|
14
|
-
# Inspect bounded metadata only; never extract or evaluate build files.
|
|
15
|
-
module ReleaseArtifactManifest
|
|
16
|
-
FIELDS = %w[gem_name version platform registry sha256 bytes].freeze
|
|
17
|
-
NAME = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
|
|
18
|
-
MAX_METADATA_BYTES = 1024 * 1024
|
|
19
|
-
REGISTRY = "https://rubygems.org"
|
|
20
|
-
|
|
21
|
-
def self.validate!(value)
|
|
22
|
-
unless value.is_a?(Hash) && value.keys.sort == FIELDS.sort &&
|
|
23
|
-
value["gem_name"].is_a?(String) && NAME.match?(value["gem_name"]) &&
|
|
24
|
-
value["version"].is_a?(String) && value["version"].bytesize.between?(1, 128) && Gem::Version.correct?(value["version"]) &&
|
|
25
|
-
value["platform"].is_a?(String) && NAME.match?(value["platform"]) &&
|
|
26
|
-
value["registry"] == REGISTRY && value["sha256"].is_a?(String) && value["sha256"].match?(/\A[0-9a-f]{64}\z/) &&
|
|
27
|
-
value["bytes"].is_a?(Integer) && value["bytes"].between?(1, Molecules::CanonicalEvidence::MAX_RELEASE_ARTIFACT_BYTES)
|
|
28
|
-
raise ArgumentError, "invalid release artifact manifest"
|
|
29
|
-
end
|
|
30
|
-
value
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.verify!(bytes, manifest)
|
|
34
|
-
validate!(manifest)
|
|
35
|
-
unless bytes.is_a?(String) && bytes.bytesize == manifest.fetch("bytes") &&
|
|
36
|
-
Digest::SHA256.hexdigest(bytes.b) == manifest.fetch("sha256")
|
|
37
|
-
raise AttemptErrors::ReceiptRejected, "release artifact bytes differ from the manifest"
|
|
38
|
-
end
|
|
39
|
-
metadata = metadata!(bytes)
|
|
40
|
-
unless metadata == manifest.slice("gem_name", "version", "platform")
|
|
41
|
-
raise AttemptErrors::ReceiptRejected, "release artifact gem metadata differs from the manifest"
|
|
42
|
-
end
|
|
43
|
-
true
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Shared immutable input for retention and the later fixed publisher.
|
|
47
|
-
# Candidate/reference come from the installed canonical owner, not wire
|
|
48
|
-
# claims. Its digest is the existing service authorization input_digest.
|
|
49
|
-
def self.publication_input(manifest:, candidate:, reference:)
|
|
50
|
-
validate!(manifest)
|
|
51
|
-
{"target" => {"resource" => "rubygems:#{manifest.fetch('gem_name')}:#{manifest.fetch('version')}",
|
|
52
|
-
"artifact_digest" => manifest.fetch("sha256")},
|
|
53
|
-
"publication" => manifest.slice("gem_name", "version", "platform", "registry").merge(
|
|
54
|
-
"head" => candidate.fetch("head"), "tree" => candidate.fetch("tree"), "candidate_reference" => reference,
|
|
55
|
-
"artifact_bytes" => manifest.fetch("bytes"), "artifact_relative_path" => "artifacts/#{manifest.fetch('sha256')}.gem")}
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.metadata!(bytes)
|
|
59
|
-
metadata = nil
|
|
60
|
-
Gem::Package::TarReader.new(StringIO.new(bytes)) do |archive|
|
|
61
|
-
archive.each do |entry|
|
|
62
|
-
next unless entry.full_name == "metadata.gz"
|
|
63
|
-
unless metadata.nil? && entry.file? && entry.header.size.between?(1, MAX_METADATA_BYTES)
|
|
64
|
-
raise AttemptErrors::ReceiptRejected, "release artifact metadata is missing, repeated or oversized"
|
|
65
|
-
end
|
|
66
|
-
compressed = entry.read
|
|
67
|
-
Zlib::GzipReader.wrap(StringIO.new(compressed)) do |reader|
|
|
68
|
-
metadata = reader.read(MAX_METADATA_BYTES + 1)
|
|
69
|
-
if metadata.bytesize > MAX_METADATA_BYTES
|
|
70
|
-
raise AttemptErrors::ReceiptRejected, "release artifact metadata exceeds its bound"
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
raise AttemptErrors::ReceiptRejected, "release artifact metadata is unavailable" unless metadata
|
|
76
|
-
specification = Gem::Specification.from_yaml(metadata)
|
|
77
|
-
unless specification.is_a?(Gem::Specification)
|
|
78
|
-
raise AttemptErrors::ReceiptRejected, "release artifact metadata is invalid"
|
|
79
|
-
end
|
|
80
|
-
{"gem_name" => specification.name, "version" => specification.version.to_s,
|
|
81
|
-
"platform" => specification.platform.to_s}
|
|
82
|
-
rescue AttemptErrors::ReceiptRejected
|
|
83
|
-
raise
|
|
84
|
-
rescue StandardError
|
|
85
|
-
raise AttemptErrors::ReceiptRejected, "release artifact metadata is invalid"
|
|
86
|
-
end
|
|
87
|
-
private_class_method :metadata!
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "digest"
|
|
5
|
-
require_relative "private_directory"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Authority
|
|
10
|
-
# Immutable requester inputs, never evidence that an operation executed.
|
|
11
|
-
module RetainedInput
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
def read_file(path, limit)
|
|
15
|
-
File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
|
|
16
|
-
before = file.stat
|
|
17
|
-
raise ArgumentError, "retained input is not a bounded regular file" unless before.file? && before.size.between?(0, limit)
|
|
18
|
-
bytes = (file.read(limit + 1) || "").b
|
|
19
|
-
after, selected = file.stat, File.lstat(path)
|
|
20
|
-
identity = ->(stat) { [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime] }
|
|
21
|
-
unless bytes.bytesize == before.size && identity.call(before) == identity.call(after) &&
|
|
22
|
-
identity.call(after) == identity.call(selected) && !selected.symlink?
|
|
23
|
-
raise ArgumentError, "retained input changed during read"
|
|
24
|
-
end
|
|
25
|
-
bytes.freeze
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def json(path, limit = 16_384)
|
|
30
|
-
JSON.parse(read_file(path, limit), allow_duplicate_key: false, create_additions: false, max_nesting: 24)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def create_output(path)
|
|
34
|
-
raise ArgumentError, "output must be a new directory path" unless path.is_a?(String) && !path.empty?
|
|
35
|
-
directory = File.expand_path(path)
|
|
36
|
-
Dir.mkdir(directory, 0o700)
|
|
37
|
-
PrivateDirectory.verify!(directory)
|
|
38
|
-
directory
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def write_file(directory, name, bytes)
|
|
42
|
-
raise ArgumentError, "retained filename is invalid" unless File.basename(name) == name && !%w[. ..].include?(name)
|
|
43
|
-
File.open(File.join(directory, name), File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
|
|
44
|
-
file.write(bytes)
|
|
45
|
-
file.flush
|
|
46
|
-
file.fsync
|
|
47
|
-
end
|
|
48
|
-
File.open(directory) { |file| file.fsync }
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
module Ace
|
|
3
|
-
module Assign
|
|
4
|
-
module Authority
|
|
5
|
-
# Source-owned handlers share one listener. Requests never name a method.
|
|
6
|
-
class Router
|
|
7
|
-
def initialize(launch: nil, handlers: [])
|
|
8
|
-
@launch = launch
|
|
9
|
-
@routes = {}
|
|
10
|
-
([launch].compact + handlers).each do |handler|
|
|
11
|
-
handler.class::OPERATIONS.each do |operation|
|
|
12
|
-
raise ArgumentError, "duplicate authority operation" if @routes.key?(operation)
|
|
13
|
-
@routes[operation] = handler
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
@routes.freeze
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def dispatch(request:, peer:, role:, transfer: nil, deadline: nil)
|
|
20
|
-
validate_review_namespace!(request)
|
|
21
|
-
handler = @routes.fetch(request.fetch("operation")) { raise ArgumentError, "unknown authority operation" }
|
|
22
|
-
options = {request: request, peer: peer, role: role}
|
|
23
|
-
options[:transfer] = transfer if transfer
|
|
24
|
-
options[:deadline] = deadline if deadline
|
|
25
|
-
handler.dispatch(**options)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def transfer_binding(request)
|
|
29
|
-
validate_review_namespace!(request)
|
|
30
|
-
operation = request.fetch("operation")
|
|
31
|
-
handler = @routes.fetch(operation) { raise ArgumentError, "unknown authority operation" }
|
|
32
|
-
return nil unless handler.class.const_defined?(:TRANSFER_OPERATIONS, false)
|
|
33
|
-
binding = handler.class::TRANSFER_OPERATIONS[operation]
|
|
34
|
-
if operation == "evidence_fetch" && %w[prepared_work campaign_candidate].include?(request.dig("params", "kind"))
|
|
35
|
-
binding = {direction: :download, purpose: :candidate, roles: [:worker]}
|
|
36
|
-
end
|
|
37
|
-
return nil unless binding
|
|
38
|
-
unless binding.is_a?(Hash) && binding.keys.sort == %i[direction purpose roles] &&
|
|
39
|
-
%i[upload download].include?(binding[:direction]) && TransferCodec::LIMITS.key?(binding[:purpose]) &&
|
|
40
|
-
binding[:roles].is_a?(Array) && binding[:roles].all? { |role| %i[worker launcher reviewer executor supervisor observer signer].include?(role) }
|
|
41
|
-
raise ArgumentError, "invalid source transfer binding"
|
|
42
|
-
end
|
|
43
|
-
binding
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def authorize_transfer!(request:, peer:, role:, deadline: nil)
|
|
47
|
-
validate_review_namespace!(request)
|
|
48
|
-
handler = @routes.fetch(request.fetch("operation"))
|
|
49
|
-
options = {request: request, peer: peer, role: role}
|
|
50
|
-
options[:deadline] = deadline if deadline
|
|
51
|
-
handler.authorize_transfer!(**options)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def validate_review_namespace!(request)
|
|
55
|
-
if request["mutation_id"].is_a?(String) && request["mutation_id"].start_with?("review-reply.")
|
|
56
|
-
raise ArgumentError, "reserved review reply namespace"
|
|
57
|
-
end
|
|
58
|
-
if request["mutation_id"].is_a?(String) && request["mutation_id"].start_with?("review-delegate.") &&
|
|
59
|
-
request["operation"] != "assign_review"
|
|
60
|
-
raise ArgumentError, "reserved review delegation namespace"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
private :validate_review_namespace!
|
|
64
|
-
|
|
65
|
-
def close
|
|
66
|
-
@routes.values.uniq.each { |handler| handler.close if handler.respond_to?(:close) }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def serve_launch_control!(**options)
|
|
70
|
-
raise AttemptErrors::EvidenceUnavailable, "launch control is unavailable in this composition" unless @launch
|
|
71
|
-
@launch.serve_launch_control!(**options)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def gate_ready(**options)
|
|
75
|
-
raise AttemptErrors::EvidenceUnavailable, "launch gate is unavailable in this composition" unless @launch
|
|
76
|
-
@launch.gate_ready(**options)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "deployment"
|
|
4
|
-
require_relative "socket_server"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# Protected admission is mandatory for this source-selected composition.
|
|
10
|
-
class Server < SocketServer
|
|
11
|
-
def initialize(authority_id:, lifecycle:, deployment: Deployment.load,
|
|
12
|
-
kernel: Ace::Runtime::Molecules::ProtectedLinux.new, composition: "launch", services: [])
|
|
13
|
-
@authority_id, @deployment, @kernel, @composition = authority_id, deployment, kernel, composition
|
|
14
|
-
deployment.verify_composition!(authority_id, composition: composition)
|
|
15
|
-
super(service: deployment.authority(authority_id), lifecycle: lifecycle, services: services)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def verify_listener!
|
|
21
|
-
@kernel.supported!
|
|
22
|
-
me = @kernel.capture(Process.pid)
|
|
23
|
-
unless principal?(me, @service, "uid", "gid", "groups")
|
|
24
|
-
raise AttemptErrors::UnauthorizedIdentity, "listener principal differs from installed authority"
|
|
25
|
-
end
|
|
26
|
-
@deployment.verify_receiver_paths!(@authority_id)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def receive(socket)
|
|
30
|
-
deadline = wire.deadline(30)
|
|
31
|
-
peer = @kernel.peer(socket)
|
|
32
|
-
frame = wire.read(socket, deadline: [deadline, wire.deadline(5)].min, with_size: true)
|
|
33
|
-
request = frame.fetch(:data)
|
|
34
|
-
unless request.is_a?(Hash) && request.keys.sort == %w[mutation_id operation params project_id version] &&
|
|
35
|
-
request["version"].is_a?(Integer) && request["version"] == 1 && request["operation"].is_a?(String) && request["params"].is_a?(Hash)
|
|
36
|
-
raise ArgumentError, "invalid authority envelope"
|
|
37
|
-
end
|
|
38
|
-
deadline = wire.deadline(90) if %w[reserve_attempt close_execution_scope stop_attempt].include?(request.fetch("operation"))
|
|
39
|
-
params = request.fetch("params")
|
|
40
|
-
map = @deployment.verify!(params.fetch("mapping_id"), kernel: @kernel, authority_state: true)
|
|
41
|
-
unless map["authority_id"] == @authority_id && map["project_id"] == request["project_id"]
|
|
42
|
-
raise AttemptErrors::UnauthorizedIdentity, "mapping belongs to another authority or project"
|
|
43
|
-
end
|
|
44
|
-
project = @deployment.project(map.fetch("project_id"))
|
|
45
|
-
owner_contexts = project.fetch("inbox_contexts", {}).select do |_id, context|
|
|
46
|
-
context.fetch("native_mapping_id") == params.fetch("mapping_id") &&
|
|
47
|
-
context.fetch("owner_credentials").values_at("uid", "gid", "groups") == peer.values_at("uid", "gid", "groups")
|
|
48
|
-
end
|
|
49
|
-
context_query = %w[inbox_context_original].include?(request["operation"])
|
|
50
|
-
if context_query || !owner_contexts.empty?
|
|
51
|
-
unless context_query && @composition == "services" && request["mutation_id"].nil? &&
|
|
52
|
-
owner_contexts.key?(params.fetch("inbox_context_id")) && frame.fetch(:bytesize) <= 16_384
|
|
53
|
-
raise AttemptErrors::UnauthorizedIdentity, "context owner is exclusive to its fixed read-only inbox queries"
|
|
54
|
-
end
|
|
55
|
-
role = :context_owner
|
|
56
|
-
end
|
|
57
|
-
if request["operation"] == "native_readiness"
|
|
58
|
-
unless @composition == "launch" && request["mutation_id"].nil? && params.keys == ["mapping_id"] &&
|
|
59
|
-
frame.fetch(:bytesize) <= 16_384
|
|
60
|
-
raise ArgumentError, "private readiness envelope differs"
|
|
61
|
-
end
|
|
62
|
-
admitted_transfer = @mutex.synchronize do
|
|
63
|
-
if @transfers < 2
|
|
64
|
-
@transfers += 1
|
|
65
|
-
true
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
raise AttemptErrors::Conflict, "readiness transfer capacity is busy" unless admitted_transfer
|
|
69
|
-
@lifecycle.native_readiness!(mapping_id: params.fetch("mapping_id"), peer: peer, socket: socket,
|
|
70
|
-
codec: transfer_codec, deadline: wire.deadline(10))
|
|
71
|
-
return
|
|
72
|
-
end
|
|
73
|
-
role ||= if principal?(peer, map, "launcher_uid", "launcher_gid", "launcher_groups")
|
|
74
|
-
:launcher
|
|
75
|
-
elsif principal?(peer, map, "worker_uid", "worker_gid", "worker_groups")
|
|
76
|
-
:worker
|
|
77
|
-
end
|
|
78
|
-
unless role
|
|
79
|
-
project = @deployment.project(map.fetch("project_id"))
|
|
80
|
-
configured = project.fetch("peer_credentials")[peer.fetch("uid").to_s]
|
|
81
|
-
if configured && peer["gid"] == configured["gid"] && peer["groups"] == configured["groups"]
|
|
82
|
-
{reviewer: "reviewer_uids", executor: "service_executor_uids", supervisor: "supervisor_uids"}.each do |candidate, key|
|
|
83
|
-
role ||= candidate if project.fetch(key).include?(peer.fetch("uid"))
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
raise AttemptErrors::UnauthorizedIdentity, "unmapped kernel peer" unless role
|
|
88
|
-
if request["operation"] == "launch_control"
|
|
89
|
-
raise AttemptErrors::UnauthorizedIdentity, "Private launcher control requires launcher peer" unless role == :launcher
|
|
90
|
-
raise ArgumentError, "Private launcher header is oversized" unless frame.fetch(:bytesize) <= 16_384
|
|
91
|
-
@lifecycle.serve_launch_control!(request: request, peer: peer, socket: socket, codec: transfer_codec, deadline: deadline)
|
|
92
|
-
return
|
|
93
|
-
end
|
|
94
|
-
if %w[bind_inbox finish recover request_review review_status launch_review_intent cancel_review evidence_fetch assignment_inventory observe_execution_scope close_execution_scope stop_attempt prompt_status launch_input_inhibit_selection launch_input_inhibit_completion launch_prompt_intent launch_prompt_completion claim_service_settlement workspace_prune_preview_context].include?(request["operation"]) ||
|
|
95
|
-
@composition == "services" && %w[attempt_status inbox_context_original].include?(request["operation"])
|
|
96
|
-
raise ArgumentError, "review control header is oversized" if %w[request_review review_status cancel_review launch_review_intent].include?(request["operation"]) && frame.fetch(:bytesize) > 16_384
|
|
97
|
-
bodyless_read!(socket, deadline)
|
|
98
|
-
end
|
|
99
|
-
if request["operation"] == "gate_ready"
|
|
100
|
-
raise AttemptErrors::UnauthorizedIdentity, "gate readiness requires worker peer" unless role == :worker
|
|
101
|
-
@lifecycle.gate_ready(request: request, peer: peer, socket: socket, deadline: deadline)
|
|
102
|
-
else
|
|
103
|
-
dispatch_request(socket, request, peer, role, deadline, frame_bytes: frame.fetch(:bytesize))
|
|
104
|
-
end
|
|
105
|
-
rescue ArgumentError, KeyError, AttemptErrors::MalformedTransfer
|
|
106
|
-
refusal(socket, "invalid_input")
|
|
107
|
-
rescue AttemptErrors::UnauthorizedIdentity, SecurityError
|
|
108
|
-
refusal(socket, "unauthorized")
|
|
109
|
-
rescue AttemptErrors::Conflict
|
|
110
|
-
refusal(socket, "conflict")
|
|
111
|
-
rescue AttemptErrors::NotFound
|
|
112
|
-
refusal(socket, "missing")
|
|
113
|
-
rescue AttemptErrors::BoundedResultUnavailable
|
|
114
|
-
refusal(socket, "bounded_result")
|
|
115
|
-
rescue StandardError
|
|
116
|
-
refusal(socket, "evidence_unavailable")
|
|
117
|
-
ensure
|
|
118
|
-
socket.close unless socket.closed?
|
|
119
|
-
@mutex.synchronize do
|
|
120
|
-
@connections.delete(socket)
|
|
121
|
-
@transfers -= 1 if admitted_transfer
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def principal?(identity, mapping, uid, gid, groups)
|
|
126
|
-
identity["uid"] == mapping[uid] && identity["gid"] == mapping[gid] && identity["groups"] == mapping[groups]
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# Canonical-only original dispatch proof shared by status, root admission
|
|
7
|
-
# and initial/retained inspection imports. Installed peer policy stays in
|
|
8
|
-
# the fixed root transport owner.
|
|
9
|
-
module ServiceCleanupDispatch
|
|
10
|
-
CLEANUP_MUTABLE_SETTLEMENT_FIELDS = %w[state receipt reason claimed_at failed_at no_effect_challenge
|
|
11
|
-
challenge_generation challenge_event_digest completion_digest no_effect_completion_digest].freeze
|
|
12
|
-
|
|
13
|
-
def cleanup_dispatch_context!(record, commit:, selectors: nil, pending: nil)
|
|
14
|
-
unless @journal.evidence_mode == :protected &&
|
|
15
|
-
record.values_at("operation", "dispatch_phase") == ["prune-preserved-workspace", "dispatch_started"]
|
|
16
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup original dispatch is unavailable"
|
|
17
|
-
end
|
|
18
|
-
pending ||= {commit: commit}
|
|
19
|
-
@journal.verify_canonical_prefix!(commit: commit) unless service_read_view(pending)
|
|
20
|
-
# The existing index authenticates accepted record provenance without
|
|
21
|
-
# recursively asking this terminal collection to validate itself.
|
|
22
|
-
selected = @journal.service_request_records(commit: commit)
|
|
23
|
-
.select { |entry| entry["request_id"] == record.fetch("request_id") }
|
|
24
|
-
# Initial import receives the lower writer's proposed terminal
|
|
25
|
-
# replacement. Its settlement fields are validated by that writer
|
|
26
|
-
# and collection; the original immutable dispatch must still match.
|
|
27
|
-
unless selected.one? && selected.first.except(*CLEANUP_MUTABLE_SETTLEMENT_FIELDS) ==
|
|
28
|
-
record.except(*CLEANUP_MUTABLE_SETTLEMENT_FIELDS)
|
|
29
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup selected record differs"
|
|
30
|
-
end
|
|
31
|
-
context(record, pending: {commit: commit})
|
|
32
|
-
events = service_read_events(record, pending)
|
|
33
|
-
names = %w[request_event_digest dispatch_event_digest]
|
|
34
|
-
operations = %w[request_service begin_dispatch]
|
|
35
|
-
if selectors.nil?
|
|
36
|
-
selectors = names.zip(operations).to_h do |name, operation|
|
|
37
|
-
matches = events.select do |event|
|
|
38
|
-
event["type"] == "authority_mutation" && event.dig("payload", "operation") == operation &&
|
|
39
|
-
event.dig("payload", "data", "request_id") == record.fetch("request_id") &&
|
|
40
|
-
event.dig("payload", "data", "claim_binding") == record.fetch("claim_binding") &&
|
|
41
|
-
event.dig("payload", "data", operation == "request_service" ? "claim" : "invocation") ==
|
|
42
|
-
(operation == "request_service" ? "created" : "permitted")
|
|
43
|
-
end
|
|
44
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup original acceptance is ambiguous" unless matches.one?
|
|
45
|
-
[name, matches.first.fetch("digest")]
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
unless selectors.is_a?(Hash) && selectors.keys.sort == names.sort &&
|
|
49
|
-
selectors.values.all? { |value| value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/) } && selectors.values.uniq.size == 2
|
|
50
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup original acceptance selectors differ"
|
|
51
|
-
end
|
|
52
|
-
introductions = service_read_introductions(record, selectors.values, pending)
|
|
53
|
-
dispatch_record = nil
|
|
54
|
-
names.zip(operations).each do |name, operation|
|
|
55
|
-
selector = selectors.fetch(name)
|
|
56
|
-
matches = events.select { |event| event.fetch("digest") == selector }
|
|
57
|
-
event = matches.one? && matches.first
|
|
58
|
-
payload = event && event.fetch("payload")
|
|
59
|
-
unless event && event.fetch("type") == "authority_mutation" && payload.fetch("operation") == operation &&
|
|
60
|
-
payload.slice("assignment_id", "attempt_id") == record.slice("assignment_id", "attempt_id") &&
|
|
61
|
-
payload.fetch("data").slice("request_id", "claim_binding") == record.slice("request_id", "claim_binding") &&
|
|
62
|
-
payload.fetch("data").fetch(operation == "request_service" ? "claim" : "invocation") ==
|
|
63
|
-
(operation == "request_service" ? "created" : "permitted")
|
|
64
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup original acceptance differs"
|
|
65
|
-
end
|
|
66
|
-
prefix = @journal.read_events(record.fetch("assignment_id"), commit: introductions.fetch(selector))
|
|
67
|
-
.select { |entry| entry.fetch("attempt_id") == record.fetch("attempt_id") }
|
|
68
|
-
original = @journal.service_request(record.fetch("request_id"), commit: introductions.fetch(selector))
|
|
69
|
-
expected = operation == "request_service" ? original&.merge("dispatch_phase" => "dispatch_started",
|
|
70
|
-
"operation_owner_binding" => record.fetch("operation_owner_binding"),
|
|
71
|
-
"executor_process_binding" => record.fetch("executor_process_binding")) : original
|
|
72
|
-
unless expected&.except(*CLEANUP_MUTABLE_SETTLEMENT_FIELDS) == record.except(*CLEANUP_MUTABLE_SETTLEMENT_FIELDS) &&
|
|
73
|
-
prefix.last == event && prefix[-2] &&
|
|
74
|
-
prefix[-2].fetch("type") == (operation == "request_service" ? "service_claim" : "service_transition") &&
|
|
75
|
-
prefix[-2].dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(original) &&
|
|
76
|
-
event.fetch("previous_digest") == prefix[-2].fetch("digest")
|
|
77
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup acceptance is not its original canonical introduction"
|
|
78
|
-
end
|
|
79
|
-
dispatch_record = original if operation == "begin_dispatch"
|
|
80
|
-
end
|
|
81
|
-
freeze_projection(JSON.parse(JSON.generate(selectors.merge("commit" => commit, "dispatch_record" => dispatch_record))))
|
|
82
|
-
rescue KeyError, TypeError, ArgumentError
|
|
83
|
-
raise AttemptErrors::EvidenceUnavailable, "cleanup original dispatch proof is incomplete"
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|