ace-assign 0.64.0 → 0.66.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 +11 -0
- data/CHANGELOG.md +25 -0
- data/docs/protected-authority.md +23 -7
- data/docs/usage.md +117 -4
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +156 -6
- data/lib/ace/assign/authority/deployment.rb +201 -39
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +131 -18
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +36 -18
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +221 -18
- data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +298 -19
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
- data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
- data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
- data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +5 -2
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/router.rb +24 -2
- data/lib/ace/assign/authority/server.rb +103 -33
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +243 -12
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +44 -12
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
- data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
- data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +2 -0
- data/lib/ace/assign/cli/commands/delivery.rb +54 -4
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +12 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +36 -5
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +3 -3
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -112
- metadata +90 -11
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "deployment_history"
|
|
4
|
+
require_relative "prepared_input"
|
|
5
|
+
require_relative "prepared_queue"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Assign
|
|
9
|
+
module Authority
|
|
10
|
+
# Installed and retained deployment owners determine protected principals.
|
|
11
|
+
# A removed original mapping never silently restores ordinary local mode.
|
|
12
|
+
class ProtectedAssignmentContext
|
|
13
|
+
attr_reader :uid
|
|
14
|
+
|
|
15
|
+
def self.load
|
|
16
|
+
paths = [Deployment::PATH, DeploymentHistory::PATH]
|
|
17
|
+
installed = paths.any? do |path|
|
|
18
|
+
begin
|
|
19
|
+
File.lstat(path)
|
|
20
|
+
true
|
|
21
|
+
rescue Errno::ENOENT
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
return new(deployment: nil, history: nil) unless installed
|
|
26
|
+
deployment = Deployment.load
|
|
27
|
+
history = DeploymentHistory.load
|
|
28
|
+
unless history.selects?(deployment)
|
|
29
|
+
raise AttemptErrors::EvidenceUnavailable, "installed protected descriptor is not retained"
|
|
30
|
+
end
|
|
31
|
+
new(deployment: deployment, history: history)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(deployment:, history:, uid: Process.uid, kernel: nil, env: ENV,
|
|
35
|
+
artifacts_factory: -> { Ace::Runtime::Molecules::ProtectedArtifactSet.new })
|
|
36
|
+
@deployment, @history, @uid, @kernel, @env = deployment, history, uid, kernel, env
|
|
37
|
+
@artifacts_factory = artifacts_factory
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A declared installed account never falls through to local authority.
|
|
41
|
+
# This is classification only; each wire operation still authenticates
|
|
42
|
+
# the current kernel peer and its exact permitted role.
|
|
43
|
+
def protected_participant?
|
|
44
|
+
return false unless @deployment
|
|
45
|
+
raise AttemptErrors::EvidenceUnavailable, "protected retained owners are unavailable" unless @history
|
|
46
|
+
[@deployment, *@history.descriptors].any? do |owner|
|
|
47
|
+
data = owner.data
|
|
48
|
+
principals = data.fetch("authorities").values.map { |authority| authority.fetch("uid") }
|
|
49
|
+
principals.concat(data.fetch("launch_mappings").values.flat_map { |map| map.values_at("launcher_uid", "worker_uid") })
|
|
50
|
+
data.fetch("projects").each_value do |project|
|
|
51
|
+
%w[launcher_uids reviewer_uids worker_uids service_executor_uids supervisor_uids].each do |key|
|
|
52
|
+
principals.concat(project.fetch(key, []))
|
|
53
|
+
end
|
|
54
|
+
principals.concat(project.fetch("service_receivers", {}).values.map { |receiver| receiver.fetch("executor_uid") })
|
|
55
|
+
principals.concat(project.fetch("inbox_contexts", {}).values.map { |context| context.fetch("owner_credentials").fetch("uid") })
|
|
56
|
+
end
|
|
57
|
+
principals.include?(@uid)
|
|
58
|
+
end
|
|
59
|
+
rescue KeyError, TypeError, NoMethodError
|
|
60
|
+
raise AttemptErrors::EvidenceUnavailable, "protected installed principal inventory is malformed"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Read-only current selection; no role grant or private Inbox access.
|
|
64
|
+
def inbox_context_selection(project:, mapping:, inbox_context:)
|
|
65
|
+
unless installed? && [project, mapping, inbox_context].all? { |id| id.is_a?(String) && Deployment::TOKEN.match?(id) }
|
|
66
|
+
raise AttemptErrors::EvidenceUnavailable, "installed inbox selectors differ"
|
|
67
|
+
end
|
|
68
|
+
descriptor = @deployment.artifact_reference
|
|
69
|
+
history = @history.artifact_reference
|
|
70
|
+
unless descriptor && history && @history.selects?(@deployment)
|
|
71
|
+
raise AttemptErrors::EvidenceUnavailable, "installed inbox references are unavailable"
|
|
72
|
+
end
|
|
73
|
+
@artifacts_factory.call.with do |held|
|
|
74
|
+
held.read!(descriptor)
|
|
75
|
+
held.read!(history)
|
|
76
|
+
map = @deployment.mapping(mapping)
|
|
77
|
+
unless map.fetch("project_id") == project
|
|
78
|
+
raise AttemptErrors::EvidenceUnavailable, "installed inbox project differs"
|
|
79
|
+
end
|
|
80
|
+
selected = @deployment.inbox_context(mapping, inbox_context)
|
|
81
|
+
authority = @deployment.authority(map.fetch("authority_id"))
|
|
82
|
+
result = {"uid" => @uid, "project_id" => project, "mapping_id" => mapping,
|
|
83
|
+
"inbox_context_id" => inbox_context, "descriptor" => descriptor, "history" => history,
|
|
84
|
+
"context" => selected.slice("control_socket_path", "owner_credentials", "native_mapping_id"),
|
|
85
|
+
"authority" => authority.slice("socket_path", "uid", "gid", "groups")
|
|
86
|
+
.merge("authority_id" => map.fetch("authority_id"))}
|
|
87
|
+
held.verify_unchanged!
|
|
88
|
+
value = yield result
|
|
89
|
+
held.verify_unchanged!
|
|
90
|
+
value
|
|
91
|
+
end
|
|
92
|
+
rescue ArgumentError, KeyError, TypeError, Ace::Runtime::Error
|
|
93
|
+
raise AttemptErrors::EvidenceUnavailable, "installed inbox selection is unavailable"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def protected_worker?
|
|
97
|
+
return false unless @deployment
|
|
98
|
+
[@deployment, *@history.descriptors].any? do |owner|
|
|
99
|
+
owner.data.fetch("projects").values.any? { |project| project.fetch("worker_uids").include?(@uid) }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def installed?
|
|
104
|
+
!@deployment.nil? && !@history.nil?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def refuse_graph_mutation!(options:)
|
|
108
|
+
selected = options.values_at(:mapping, :attempt).any? { |value| value && !value.to_s.empty? } ||
|
|
109
|
+
%w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ATTEMPT_ID].any? { |key| !@env[key].to_s.empty? }
|
|
110
|
+
if protected_worker? || selected
|
|
111
|
+
raise AttemptErrors::EvidenceUnavailable, "protected queue graph changes require a reviewed new prepared version and attempt"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def resolve(options:, assignment_id:, scope:)
|
|
116
|
+
selected = options.values_at(:mapping, :attempt).any? { |value| value && !value.to_s.empty? } ||
|
|
117
|
+
%w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ATTEMPT_ID].any? { |key| !@env[key].to_s.empty? }
|
|
118
|
+
return nil unless protected_worker? || selected
|
|
119
|
+
unless @deployment && assignment_id && scope && scope.match?(PreparedWork::SCOPE)
|
|
120
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: explicit scoped assignment required"
|
|
121
|
+
end
|
|
122
|
+
attempt = selected_hint(options[:attempt], "ACE_ASSIGN_ATTEMPT_ID")
|
|
123
|
+
hint = @env["ACE_ASSIGN_ASSIGNMENT_ID"].to_s
|
|
124
|
+
unless hint.empty? || hint == assignment_id
|
|
125
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared assignment hint differs"
|
|
126
|
+
end
|
|
127
|
+
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
128
|
+
input = PreparedInput.fetch(client: client(options: options),
|
|
129
|
+
assignment_id: assignment_id, attempt_id: attempt)
|
|
130
|
+
unless input.descriptor.fetch("scope") == scope
|
|
131
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared_input_mismatch: scoped CLI selection"
|
|
132
|
+
end
|
|
133
|
+
input
|
|
134
|
+
rescue KeyError
|
|
135
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original installed mapping is unavailable"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def verify_attempt_hints!(assignment_id:, attempt_id:)
|
|
139
|
+
selected_hint(assignment_id, "ACE_ASSIGN_ASSIGNMENT_ID")
|
|
140
|
+
selected_hint(attempt_id, "ACE_ASSIGN_ATTEMPT_ID")
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def client(options:)
|
|
145
|
+
raise AttemptErrors::EvidenceUnavailable, "protected installed authority is unavailable" unless @deployment
|
|
146
|
+
mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
147
|
+
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
148
|
+
Client.new(mapping_id: mapping, deployment: @deployment, kernel: @kernel)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def mapping_hint?
|
|
152
|
+
!@env["ACE_ASSIGN_LAUNCH_MAPPING"].to_s.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Observer/signer commands reuse the installed selection and held
|
|
156
|
+
# descriptor pair. This grants no effect; endpoints authorize the peer.
|
|
157
|
+
def with_inbox_workflow(options:)
|
|
158
|
+
mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
159
|
+
project, inbox_context = options.values_at(:project, :inbox_context)
|
|
160
|
+
inbox_context_selection(project: project, mapping: mapping, inbox_context: inbox_context) do |_selection|
|
|
161
|
+
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
162
|
+
yield @deployment, @kernel, @deployment.mapping(mapping)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Consumers call only after original PreparedInput admission. These
|
|
167
|
+
# existing owners select transport; they do not confer a new grant.
|
|
168
|
+
def with_installed_selection(options:, input:)
|
|
169
|
+
raise AttemptErrors::EvidenceUnavailable, "protected installed authority is unavailable" unless @deployment
|
|
170
|
+
mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
171
|
+
begin
|
|
172
|
+
map = @deployment.mapping(mapping)
|
|
173
|
+
unless input && input.descriptor.fetch("mapping_id") == mapping &&
|
|
174
|
+
input.descriptor.fetch("project_id") == map.fetch("project_id")
|
|
175
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared installed selection differs"
|
|
176
|
+
end
|
|
177
|
+
rescue KeyError
|
|
178
|
+
raise AttemptErrors::EvidenceUnavailable, "original installed mapping is unavailable"
|
|
179
|
+
end
|
|
180
|
+
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
181
|
+
yield @deployment, @kernel, mapping
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def selected_hint(explicit, key)
|
|
187
|
+
hint = @env[key].to_s
|
|
188
|
+
value = explicit || hint
|
|
189
|
+
unless value.is_a?(String) && value.match?(PreparedWork::TOKEN) && (hint.empty? || hint == value)
|
|
190
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared selector hint differs"
|
|
191
|
+
end
|
|
192
|
+
value
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Each supported operation holds the existing private queue exclusion and
|
|
197
|
+
# consumes its authenticated held snapshot. No graph-changing API exists.
|
|
198
|
+
class PreparedExecutor
|
|
199
|
+
def initialize(input)
|
|
200
|
+
@queue = PreparedQueue.new(work: input.work, descriptor: input.descriptor)
|
|
201
|
+
@scope = input.descriptor.fetch("scope")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def status
|
|
205
|
+
@queue.with_executor(&:status)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
%i[start_step finish_step fail].each do |operation|
|
|
209
|
+
define_method(operation) do |*args, **options|
|
|
210
|
+
unless options[:fork_root] == @scope && (!options[:step_number] ||
|
|
211
|
+
options[:step_number] == @scope || options[:step_number].start_with?(@scope + "."))
|
|
212
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared operation scope differs"
|
|
213
|
+
end
|
|
214
|
+
@queue.with_executor { |executor| executor.public_send(operation, *args, **options) }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def method_missing(*)
|
|
219
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared queue graph changes require a reviewed new prepared version and attempt"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def respond_to_missing?(*); false; end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
|
@@ -17,34 +17,56 @@ module Ace
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def dispatch(request:, peer:, role:, transfer: nil)
|
|
20
|
+
validate_review_namespace!(request)
|
|
20
21
|
handler = @routes.fetch(request.fetch("operation")) { raise ArgumentError, "unknown authority operation" }
|
|
21
22
|
options = {request: request, peer: peer, role: role}
|
|
22
23
|
options[:transfer] = transfer if transfer
|
|
23
24
|
handler.dispatch(**options)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def transfer_binding(
|
|
27
|
+
def transfer_binding(request)
|
|
28
|
+
validate_review_namespace!(request)
|
|
29
|
+
operation = request.fetch("operation")
|
|
27
30
|
handler = @routes.fetch(operation) { raise ArgumentError, "unknown authority operation" }
|
|
28
31
|
return nil unless handler.class.const_defined?(:TRANSFER_OPERATIONS, false)
|
|
29
32
|
binding = handler.class::TRANSFER_OPERATIONS[operation]
|
|
33
|
+
if operation == "evidence_fetch" && %w[prepared_work campaign_candidate].include?(request.dig("params", "kind"))
|
|
34
|
+
binding = {direction: :download, purpose: :candidate, roles: [:worker]}
|
|
35
|
+
end
|
|
30
36
|
return nil unless binding
|
|
31
37
|
unless binding.is_a?(Hash) && binding.keys.sort == %i[direction purpose roles] &&
|
|
32
38
|
%i[upload download].include?(binding[:direction]) && TransferCodec::LIMITS.key?(binding[:purpose]) &&
|
|
33
|
-
binding[:roles].is_a?(Array) && binding[:roles].all? { |role| %i[worker launcher reviewer executor supervisor].include?(role) }
|
|
39
|
+
binding[:roles].is_a?(Array) && binding[:roles].all? { |role| %i[worker launcher reviewer executor supervisor observer signer].include?(role) }
|
|
34
40
|
raise ArgumentError, "invalid source transfer binding"
|
|
35
41
|
end
|
|
36
42
|
binding
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def authorize_transfer!(request:, peer:, role:)
|
|
46
|
+
validate_review_namespace!(request)
|
|
40
47
|
handler = @routes.fetch(request.fetch("operation"))
|
|
41
48
|
handler.authorize_transfer!(request: request, peer: peer, role: role)
|
|
42
49
|
end
|
|
43
50
|
|
|
51
|
+
def validate_review_namespace!(request)
|
|
52
|
+
if request["mutation_id"].is_a?(String) && request["mutation_id"].start_with?("review-reply.")
|
|
53
|
+
raise ArgumentError, "reserved review reply namespace"
|
|
54
|
+
end
|
|
55
|
+
if request["mutation_id"].is_a?(String) && request["mutation_id"].start_with?("review-delegate.") &&
|
|
56
|
+
request["operation"] != "assign_review"
|
|
57
|
+
raise ArgumentError, "reserved review delegation namespace"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
private :validate_review_namespace!
|
|
61
|
+
|
|
44
62
|
def close
|
|
45
63
|
@launch.close
|
|
46
64
|
end
|
|
47
65
|
|
|
66
|
+
def serve_launch_control!(**options)
|
|
67
|
+
@launch.serve_launch_control!(**options)
|
|
68
|
+
end
|
|
69
|
+
|
|
48
70
|
def gate_ready(**options)
|
|
49
71
|
@launch.gate_ready(**options)
|
|
50
72
|
end
|
|
@@ -3,6 +3,7 @@ require "socket"
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "deployment"
|
|
5
5
|
require_relative "transfer_codec"
|
|
6
|
+
require_relative "../molecules/evidence_journal"
|
|
6
7
|
|
|
7
8
|
module Ace
|
|
8
9
|
module Assign
|
|
@@ -11,9 +12,10 @@ module Ace
|
|
|
11
12
|
class Server
|
|
12
13
|
CONNECTIONS = 16
|
|
13
14
|
def initialize(authority_id:, lifecycle:, deployment: Deployment.load,
|
|
14
|
-
kernel: Ace::Runtime::Molecules::ProtectedLinux.new, composition: "launch")
|
|
15
|
+
kernel: Ace::Runtime::Molecules::ProtectedLinux.new, composition: "launch", hitl_service: nil)
|
|
15
16
|
@authority_id, @lifecycle, @deployment, @kernel = authority_id, lifecycle, deployment, kernel
|
|
16
17
|
@composition = composition
|
|
18
|
+
@hitl_service = hitl_service
|
|
17
19
|
deployment.verify_composition!(authority_id, composition: composition)
|
|
18
20
|
@service = deployment.authority(authority_id)
|
|
19
21
|
@mutex = Mutex.new
|
|
@@ -36,6 +38,7 @@ module Ace
|
|
|
36
38
|
unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
37
39
|
raise AttemptErrors::Conflict, "assignment authority already owns its listener"
|
|
38
40
|
end
|
|
41
|
+
start_hitl_service!
|
|
39
42
|
path = @service.fetch("socket_path")
|
|
40
43
|
if File.exist?(path) || File.symlink?(path)
|
|
41
44
|
raise AttemptErrors::Conflict, "existing authority endpoint requires verified installer recovery"
|
|
@@ -44,6 +47,9 @@ module Ace
|
|
|
44
47
|
File.chmod(0o660, path)
|
|
45
48
|
@endpoint = wire.socket_identity(path)
|
|
46
49
|
until @stopped
|
|
50
|
+
if @hitl_thread && !@hitl_thread.alive?
|
|
51
|
+
raise Ace::Runtime::RuntimeUnavailableError, "protected HITL boundary stopped before its authority"
|
|
52
|
+
end
|
|
47
53
|
ready = IO.select([@listener], nil, nil, 0.25)
|
|
48
54
|
next unless ready
|
|
49
55
|
socket = @listener.accept_nonblock(exception: false)
|
|
@@ -68,6 +74,7 @@ module Ace
|
|
|
68
74
|
raise unless @stopped
|
|
69
75
|
ensure
|
|
70
76
|
stop
|
|
77
|
+
stop_hitl_service!
|
|
71
78
|
# Closing a stream does not cancel a handler already inside dispatch.
|
|
72
79
|
# The owner lock and endpoint remain held until every handler ends.
|
|
73
80
|
@mutex.synchronize { @handlers.dup }.each(&:join)
|
|
@@ -90,6 +97,29 @@ module Ace
|
|
|
90
97
|
|
|
91
98
|
private
|
|
92
99
|
|
|
100
|
+
# The existing HITL boundary is a sibling owned by this listener's
|
|
101
|
+
# lifetime. It must be ready before assignment ingress is opened.
|
|
102
|
+
def start_hitl_service!
|
|
103
|
+
return unless @hitl_service
|
|
104
|
+
ready = Queue.new
|
|
105
|
+
@hitl_thread = Thread.new do
|
|
106
|
+
@hitl_service.run(on_ready: -> { ready << :ready })
|
|
107
|
+
rescue Exception => error
|
|
108
|
+
@hitl_failure = error
|
|
109
|
+
ready << error
|
|
110
|
+
end
|
|
111
|
+
@hitl_thread.report_on_exception = false
|
|
112
|
+
result = ready.pop(timeout: 5)
|
|
113
|
+
raise Ace::Runtime::RuntimeUnavailableError, "protected HITL boundary startup timed out" if result.nil?
|
|
114
|
+
raise result if result.is_a?(Exception)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def stop_hitl_service!
|
|
118
|
+
return unless @hitl_thread
|
|
119
|
+
@hitl_service.stop
|
|
120
|
+
@hitl_thread.join
|
|
121
|
+
end
|
|
122
|
+
|
|
93
123
|
def same_endpoint?(path)
|
|
94
124
|
wire.socket_identity(path) == @endpoint
|
|
95
125
|
rescue StandardError
|
|
@@ -102,16 +132,45 @@ module Ace
|
|
|
102
132
|
frame = wire.read(socket, deadline: [deadline, wire.deadline(5)].min, with_size: true)
|
|
103
133
|
request = frame.fetch(:data)
|
|
104
134
|
unless request.is_a?(Hash) && request.keys.sort == %w[mutation_id operation params project_id version] &&
|
|
105
|
-
request["version"] == 1 && request["operation"].is_a?(String) && request["params"].is_a?(Hash)
|
|
135
|
+
request["version"].is_a?(Integer) && request["version"] == 1 && request["operation"].is_a?(String) && request["params"].is_a?(Hash)
|
|
106
136
|
raise ArgumentError, "invalid authority envelope"
|
|
107
137
|
end
|
|
108
|
-
deadline = wire.deadline(90) if %w[reserve_attempt close_execution_scope].include?(request.fetch("operation"))
|
|
138
|
+
deadline = wire.deadline(90) if %w[reserve_attempt close_execution_scope stop_attempt].include?(request.fetch("operation"))
|
|
109
139
|
params = request.fetch("params")
|
|
110
140
|
map = @deployment.verify!(params.fetch("mapping_id"), kernel: @kernel, authority_state: true)
|
|
111
141
|
unless map["authority_id"] == @authority_id && map["project_id"] == request["project_id"]
|
|
112
142
|
raise AttemptErrors::UnauthorizedIdentity, "mapping belongs to another authority or project"
|
|
113
143
|
end
|
|
114
|
-
|
|
144
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
145
|
+
owner_contexts = project.fetch("inbox_contexts", {}).select do |_id, context|
|
|
146
|
+
context.fetch("native_mapping_id") == params.fetch("mapping_id") &&
|
|
147
|
+
context.fetch("owner_credentials").values_at("uid", "gid", "groups") == peer.values_at("uid", "gid", "groups")
|
|
148
|
+
end
|
|
149
|
+
context_query = %w[inbox_context_original].include?(request["operation"])
|
|
150
|
+
if context_query || !owner_contexts.empty?
|
|
151
|
+
unless context_query && @composition == "services" && request["mutation_id"].nil? &&
|
|
152
|
+
owner_contexts.key?(params.fetch("inbox_context_id")) && frame.fetch(:bytesize) <= 16_384
|
|
153
|
+
raise AttemptErrors::UnauthorizedIdentity, "context owner is exclusive to its fixed read-only inbox queries"
|
|
154
|
+
end
|
|
155
|
+
role = :context_owner
|
|
156
|
+
end
|
|
157
|
+
if request["operation"] == "native_readiness"
|
|
158
|
+
unless @composition == "launch" && request["mutation_id"].nil? && params.keys == ["mapping_id"] &&
|
|
159
|
+
frame.fetch(:bytesize) <= 16_384
|
|
160
|
+
raise ArgumentError, "private readiness envelope differs"
|
|
161
|
+
end
|
|
162
|
+
admitted_transfer = @mutex.synchronize do
|
|
163
|
+
if @transfers < 2
|
|
164
|
+
@transfers += 1
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
raise AttemptErrors::Conflict, "readiness transfer capacity is busy" unless admitted_transfer
|
|
169
|
+
@lifecycle.native_readiness!(mapping_id: params.fetch("mapping_id"), peer: peer, socket: socket,
|
|
170
|
+
codec: transfer_codec, deadline: wire.deadline(10))
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
role ||= if principal?(peer, map, "launcher_uid", "launcher_gid", "launcher_groups")
|
|
115
174
|
:launcher
|
|
116
175
|
elsif principal?(peer, map, "worker_uid", "worker_gid", "worker_groups")
|
|
117
176
|
:worker
|
|
@@ -126,55 +185,66 @@ module Ace
|
|
|
126
185
|
end
|
|
127
186
|
end
|
|
128
187
|
raise AttemptErrors::UnauthorizedIdentity, "unmapped kernel peer" unless role
|
|
129
|
-
if
|
|
130
|
-
|
|
188
|
+
if request["operation"] == "launch_control"
|
|
189
|
+
raise AttemptErrors::UnauthorizedIdentity, "Private launcher control requires launcher peer" unless role == :launcher
|
|
190
|
+
raise ArgumentError, "Private launcher header is oversized" unless frame.fetch(:bytesize) <= 16_384
|
|
191
|
+
@lifecycle.serve_launch_control!(request: request, peer: peer, socket: socket, codec: transfer_codec, deadline: deadline)
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
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"]) ||
|
|
195
|
+
@composition == "services" && %w[attempt_status inbox_context_original].include?(request["operation"])
|
|
196
|
+
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
|
|
131
197
|
bodyless_read!(socket, deadline)
|
|
132
198
|
end
|
|
133
199
|
if request["operation"] == "gate_ready"
|
|
134
200
|
raise AttemptErrors::UnauthorizedIdentity, "gate readiness requires worker peer" unless role == :worker
|
|
135
201
|
@lifecycle.gate_ready(request: request, peer: peer, socket: socket, deadline: deadline)
|
|
136
202
|
else
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@transfers
|
|
145
|
-
|
|
203
|
+
Molecules::EvidenceJournal.with_event_read_operation do
|
|
204
|
+
binding = @lifecycle.transfer_binding(request) if @lifecycle.respond_to?(:transfer_binding)
|
|
205
|
+
if binding
|
|
206
|
+
raise AttemptErrors::UnauthorizedIdentity, "transfer role differs" unless binding.fetch(:roles).include?(role)
|
|
207
|
+
raise ArgumentError, "transfer control header is oversized" if frame.fetch(:bytesize) > 16_384
|
|
208
|
+
@lifecycle.authorize_transfer!(request: request, peer: peer, role: role)
|
|
209
|
+
admitted_transfer = @mutex.synchronize do
|
|
210
|
+
if @transfers < 2
|
|
211
|
+
@transfers += 1
|
|
212
|
+
true
|
|
213
|
+
end
|
|
146
214
|
end
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
215
|
+
raise AttemptErrors::Conflict, "transfer capacity is busy" unless admitted_transfer
|
|
216
|
+
codec = transfer_codec
|
|
217
|
+
if binding.fetch(:direction) == :upload
|
|
218
|
+
result = codec.receive(socket, descriptor: params.fetch("transfer"), purpose: binding.fetch(:purpose), deadline: deadline) do |input|
|
|
219
|
+
@lifecycle.dispatch(request: request, peer: peer, role: role, transfer: input)
|
|
220
|
+
end
|
|
221
|
+
else
|
|
222
|
+
result = @lifecycle.dispatch(request: request, peer: peer, role: role)
|
|
223
|
+
parts = result.fetch(:transfer_parts)
|
|
224
|
+
descriptor = codec.descriptor(parts, purpose: binding.fetch(:purpose))
|
|
225
|
+
result = result.merge(data: result.fetch(:data).merge("transfer" => descriptor))
|
|
153
226
|
end
|
|
154
227
|
else
|
|
228
|
+
raise ArgumentError, "unexpected transfer descriptor" if params.key?("transfer")
|
|
155
229
|
result = @lifecycle.dispatch(request: request, peer: peer, role: role)
|
|
156
|
-
parts = result.fetch(:transfer_parts)
|
|
157
|
-
descriptor = codec.descriptor(parts, purpose: binding.fetch(:purpose))
|
|
158
|
-
result = result.merge(data: result.fetch(:data).merge("transfer" => descriptor))
|
|
159
230
|
end
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"transport" => {"replayed" => result.fetch(:replayed, false)}}, deadline: deadline, limit: 16_384)
|
|
166
|
-
if binding && binding.fetch(:direction) == :download
|
|
167
|
-
codec.send(socket, parts: parts, descriptor: descriptor, purpose: binding.fetch(:purpose), deadline: deadline)
|
|
231
|
+
wire.write(socket, {"status" => "ok", "data" => result.fetch(:data),
|
|
232
|
+
"transport" => {"replayed" => result.fetch(:replayed, false)}}, deadline: deadline, limit: 16_384)
|
|
233
|
+
if binding && binding.fetch(:direction) == :download
|
|
234
|
+
codec.send(socket, parts: parts, descriptor: descriptor, purpose: binding.fetch(:purpose), deadline: deadline)
|
|
235
|
+
end
|
|
168
236
|
end
|
|
169
237
|
end
|
|
170
238
|
rescue ArgumentError, KeyError, AttemptErrors::MalformedTransfer
|
|
171
239
|
refusal(socket, "invalid_input")
|
|
172
|
-
rescue AttemptErrors::UnauthorizedIdentity
|
|
240
|
+
rescue AttemptErrors::UnauthorizedIdentity, SecurityError
|
|
173
241
|
refusal(socket, "unauthorized")
|
|
174
242
|
rescue AttemptErrors::Conflict
|
|
175
243
|
refusal(socket, "conflict")
|
|
176
244
|
rescue AttemptErrors::NotFound
|
|
177
245
|
refusal(socket, "missing")
|
|
246
|
+
rescue AttemptErrors::BoundedResultUnavailable
|
|
247
|
+
refusal(socket, "bounded_result")
|
|
178
248
|
rescue StandardError
|
|
179
249
|
refusal(socket, "evidence_unavailable")
|
|
180
250
|
ensure
|