ace-assign 0.71.0 → 0.73.1

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