ace-assign 0.71.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 (195) 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 -2119
  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/organisms/assignment_executor.rb +0 -6
  35. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  36. data/lib/ace/assign/version.rb +1 -1
  37. data/lib/ace/assign.rb +0 -6
  38. metadata +4 -176
  39. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  40. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  41. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  42. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  43. data/docs/protected-authority.md +0 -282
  44. data/exe/ace-scope-ready +0 -55
  45. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  46. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  47. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  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/atoms/git_blob_batch.rb +0 -59
  52. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  53. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  54. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  55. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  56. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  57. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  58. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  59. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  60. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  61. data/lib/ace/assign/authority/client.rb +0 -257
  62. data/lib/ace/assign/authority/command_execution.rb +0 -180
  63. data/lib/ace/assign/authority/deployment.rb +0 -627
  64. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  65. data/lib/ace/assign/authority/endcap.rb +0 -457
  66. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  67. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  68. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  69. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  70. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  71. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  72. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  73. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  74. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  75. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  76. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  77. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  78. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  79. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  80. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  81. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  82. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  83. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  84. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  85. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  86. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  87. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  88. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  89. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  90. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  91. data/lib/ace/assign/authority/launch_review.rb +0 -91
  92. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  93. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  94. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  95. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  96. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  97. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  98. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  99. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  100. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  101. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  102. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  103. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  104. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  105. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  106. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  107. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  108. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  109. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  110. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  111. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  112. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  113. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  114. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  115. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  116. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  117. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  118. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  119. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  120. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  121. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  122. data/lib/ace/assign/authority/private_directory.rb +0 -33
  123. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  124. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  125. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  126. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  127. data/lib/ace/assign/authority/retained_input.rb +0 -53
  128. data/lib/ace/assign/authority/router.rb +0 -81
  129. data/lib/ace/assign/authority/server.rb +0 -131
  130. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  131. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  132. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  133. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  134. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  135. data/lib/ace/assign/authority/socket_server.rb +0 -255
  136. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  137. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  138. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  139. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  140. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  141. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  142. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  143. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  144. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  145. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  146. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  147. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  148. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  149. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  151. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  152. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  153. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  154. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  155. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  156. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  157. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  158. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  159. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  160. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  161. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  162. data/lib/ace/assign/models/attempt.rb +0 -178
  163. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  164. data/lib/ace/assign/models/evidence_event.rb +0 -86
  165. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  166. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  167. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  168. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  169. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  170. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  171. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  172. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  173. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  174. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  175. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  176. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  177. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  178. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  179. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  180. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  181. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  182. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  183. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  184. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  185. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  186. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  187. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  188. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  189. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  190. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  191. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  192. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  193. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  194. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  195. 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