ace-assign 0.63.1 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  7. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  8. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  9. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  10. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  11. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  12. data/CHANGELOG.md +62 -0
  13. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  14. data/docs/protected-authority.md +282 -0
  15. data/docs/usage.md +205 -3
  16. data/exe/ace-scope-ready +55 -0
  17. data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
  18. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  19. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  20. data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
  21. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  22. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  23. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  24. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  25. data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
  26. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  27. data/lib/ace/assign/authority/client.rb +257 -0
  28. data/lib/ace/assign/authority/deployment.rb +627 -0
  29. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  30. data/lib/ace/assign/authority/endcap.rb +455 -0
  31. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  32. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  33. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  34. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  35. data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
  36. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  37. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  38. data/lib/ace/assign/authority/endcap_results.rb +398 -0
  39. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  40. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  41. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  42. data/lib/ace/assign/authority/endcap_services.rb +598 -0
  43. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  44. data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
  45. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  46. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  47. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  48. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  49. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  50. data/lib/ace/assign/authority/launch_driver.rb +393 -0
  51. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  52. data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
  53. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  54. data/lib/ace/assign/authority/launch_review.rb +91 -0
  55. data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
  56. data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
  57. data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
  58. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  59. data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
  60. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  61. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  62. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  63. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  64. data/lib/ace/assign/authority/posix_acl.rb +59 -0
  65. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  66. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  67. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  68. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  69. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  70. data/lib/ace/assign/authority/private_directory.rb +33 -0
  71. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  72. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  73. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  74. data/lib/ace/assign/authority/router.rb +76 -0
  75. data/lib/ace/assign/authority/server.rb +300 -0
  76. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  77. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  78. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  79. data/lib/ace/assign/authority/service_evidence.rb +286 -0
  80. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  81. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  82. data/lib/ace/assign/authority/transfer_codec.rb +187 -0
  83. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  84. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  85. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  86. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  87. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  88. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  89. data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
  90. data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
  91. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  92. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  93. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  94. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  95. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  96. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  97. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  98. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  99. data/lib/ace/assign/cli/commands/create.rb +11 -1
  100. data/lib/ace/assign/cli/commands/delivery.rb +107 -0
  101. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  102. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  103. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  104. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  105. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  106. data/lib/ace/assign/cli/commands/resume.rb +38 -0
  107. data/lib/ace/assign/cli/commands/select.rb +2 -0
  108. data/lib/ace/assign/cli/commands/start.rb +2 -0
  109. data/lib/ace/assign/cli/commands/status.rb +4 -2
  110. data/lib/ace/assign/cli/commands/step.rb +3 -1
  111. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  112. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  113. data/lib/ace/assign/cli.rb +56 -4
  114. data/lib/ace/assign/errors.rb +139 -0
  115. data/lib/ace/assign/models/assignment.rb +26 -4
  116. data/lib/ace/assign/models/evidence_event.rb +4 -2
  117. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  118. data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
  119. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  120. data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
  121. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  122. data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
  123. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  124. data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
  125. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  126. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  127. data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
  128. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  129. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
  130. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  131. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  132. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  133. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  134. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  135. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  136. data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
  137. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  138. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  139. data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
  140. data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
  141. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  142. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  143. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  144. data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
  145. data/lib/ace/assign/version.rb +1 -1
  146. data/lib/ace/assign.rb +4 -102
  147. metadata +186 -8
@@ -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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,76 @@
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:, handlers: [])
8
+ @launch = launch
9
+ @routes = {}
10
+ ([launch] + 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)
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
+ handler.dispatch(**options)
25
+ end
26
+
27
+ def transfer_binding(request)
28
+ validate_review_namespace!(request)
29
+ operation = request.fetch("operation")
30
+ handler = @routes.fetch(operation) { raise ArgumentError, "unknown authority operation" }
31
+ return nil unless handler.class.const_defined?(:TRANSFER_OPERATIONS, false)
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
36
+ return nil unless binding
37
+ unless binding.is_a?(Hash) && binding.keys.sort == %i[direction purpose roles] &&
38
+ %i[upload download].include?(binding[:direction]) && TransferCodec::LIMITS.key?(binding[:purpose]) &&
39
+ binding[:roles].is_a?(Array) && binding[:roles].all? { |role| %i[worker launcher reviewer executor supervisor observer signer].include?(role) }
40
+ raise ArgumentError, "invalid source transfer binding"
41
+ end
42
+ binding
43
+ end
44
+
45
+ def authorize_transfer!(request:, peer:, role:)
46
+ validate_review_namespace!(request)
47
+ handler = @routes.fetch(request.fetch("operation"))
48
+ handler.authorize_transfer!(request: request, peer: peer, role: role)
49
+ end
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
+
62
+ def close
63
+ @launch.close
64
+ end
65
+
66
+ def serve_launch_control!(**options)
67
+ @launch.serve_launch_control!(**options)
68
+ end
69
+
70
+ def gate_ready(**options)
71
+ @launch.gate_ready(**options)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end