ace-assign 0.70.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +3 -2099
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "prepared_input"
4
- require_relative "prepared_queue"
5
- require_relative "../molecules/fork_session_launcher"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Fixed gate target. Environment identifiers are untrusted lookup hints;
11
- # the original issued capability and actual kernel birth decide admission.
12
- class PreparedWorker
13
- def initialize(kernel: Ace::Runtime::Molecules::ProtectedLinux.new, client_factory: nil, launcher: nil, env: ENV,
14
- workspace_reader_factory: nil)
15
- @kernel, @launcher, @env = kernel, launcher, env
16
- @client_factory = client_factory || ->(mapping) { Client.new(mapping_id: mapping, kernel: @kernel) }
17
- @workspace_reader_factory = workspace_reader_factory || ->(projection) {
18
- Molecules::LifecycleExclusion.workspace_reader(projection: projection)
19
- }
20
- @started = false
21
- end
22
-
23
- def run
24
- raise AttemptErrors::EvidenceUnavailable, "original adapter invocation already activated" if @started
25
- mapping, assignment, attempt = %w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ASSIGNMENT_ID ACE_ASSIGN_ATTEMPT_ID].map do |key|
26
- value = @env[key]
27
- unless value.is_a?(String) && value.match?(PreparedWork::TOKEN)
28
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original worker selector"
29
- end
30
- value
31
- end
32
- input = PreparedInput.fetch(client: @client_factory.call(mapping), assignment_id: assignment, attempt_id: attempt)
33
- self_identity = @kernel.capture(Process.pid)
34
- @kernel.live!(self_identity)
35
- original = input.descriptor.fetch("original_worker_identity")
36
- unless @kernel.same?(self_identity, original)
37
- raise AttemptErrors::UnauthorizedIdentity, "prepared adapter requires exact original worker self"
38
- end
39
- input.drive_prompt
40
- @workspace_lease = @workspace_reader_factory.call(input.descriptor.fetch("workspace_exclusion"))
41
- @workspace_lease.acquire!
42
- @started = true
43
- queue = PreparedQueue.new(work: input.work, descriptor: input.descriptor)
44
- root = queue.activate_original!
45
- unless root
46
- @workspace_lease.verify_unchanged!
47
- @workspace_lease.close!
48
- return {"state" => "completed"}
49
- end
50
- launcher = @launcher || Molecules::ForkSessionLauncher.new(config: {})
51
- result = launcher.launch_provider_session(assignment_id: assignment, fork_root: input.descriptor.fetch("scope"),
52
- provider: root.fork_provider || Molecules::ForkSessionLauncher::DEFAULT_PROVIDER, cache_dir: queue.directory,
53
- prepared_input: input)
54
- queue.with_executor do |executor|
55
- state = executor.status.fetch(:state)
56
- unless state.failed.empty? && state.subtree_complete?(input.descriptor.fetch("scope"))
57
- raise AttemptErrors::EvidenceUnavailable, "prepared provider exited before selected queue completed"
58
- end
59
- end
60
- @workspace_lease.verify_unchanged!
61
- @workspace_lease.close!
62
- result
63
- rescue SystemCallError, IOError
64
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: published worker queue or provider output is unavailable"
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- module PrivateDirectory
7
- module_function
8
-
9
- # Source configuration chooses this path. Each ancestor must prevent
10
- # another principal from replacing the private leaf or its contents.
11
- def verify!(root)
12
- path = File.expand_path(root)
13
- cursor = path
14
- loop do
15
- stat = File.lstat(cursor)
16
- unless stat.directory? && !stat.symlink? && [0, Process.uid].include?(stat.uid) && (stat.mode & 0022).zero?
17
- raise AttemptErrors::ReceiptRejected, "Private directory ancestry is not protected"
18
- end
19
- break if cursor == "/"
20
- cursor = File.dirname(cursor)
21
- end
22
- stat = File.stat(path)
23
- unless stat.uid == Process.uid && (stat.mode & 0077).zero?
24
- raise AttemptErrors::ReceiptRejected, "Private directory must be owned by this peer and private"
25
- end
26
- path
27
- rescue SystemCallError, TypeError
28
- raise AttemptErrors::ReceiptRejected, "Private directory is unavailable"
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,226 +0,0 @@
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(uid: Process.uid)
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, uid: uid) 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, uid: uid)
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
@@ -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,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