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,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- # Publication-specific facts remain artifacts of the existing service
7
- # record and canonical import owner, not a second publication journal.
8
- module ServicePublicationEvidence
9
- PUBLICATION_NEED_FIELDS = %w[schema classification request_id input_digest claim_binding
10
- candidate_generation head registry gem_name version artifact_digest dispatch_event_digest previous_challenge_digest].freeze
11
- PUBLICATION_FIELDS = %w[publication_challenge_digest publication_challenge_ref publication_challenge_event_digest
12
- publication_challenge_generation publication_issuing_event_digest].freeze
13
-
14
- def publication_result!(bytes, record, state:)
15
- value = JSON.parse(bytes, allow_duplicate_key: false, allow_nan: false, max_nesting: 16)
16
- fields = %w[schema publication target classification code request_id input_digest claim_binding]
17
- publication = value.fetch("publication")
18
- allowed = state == "succeeded" ? ["registry_exact_artifact"] :
19
- %w[provider_rejected_non_otp executor_credentials_unavailable registry_artifact_conflict]
20
- unless bytes.bytesize.between?(1, 65536) && value.is_a?(Hash) && value.keys.sort == fields.sort &&
21
- value["schema"] == "ace.publication-result/v1" && value["classification"] == state && allowed.include?(value["code"]) &&
22
- %w[request_id input_digest claim_binding target].all? { |key| value[key] == record[key] } &&
23
- publication.is_a?(Hash) && publication.keys.sort == %w[gem_name version head registry artifact_relative_path].sort &&
24
- publication["head"] == record.fetch("candidate_head") && publication["registry"] == "https://rubygems.org" &&
25
- publication["gem_name"].is_a?(String) && publication["gem_name"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
26
- publication["version"].is_a?(String) && publication["version"].bytesize.between?(1, 128) &&
27
- publication["version"].match?(/\A[0-9]+(?:\.[0-9A-Za-z]+)*(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?\z/) &&
28
- "rubygems:#{publication['gem_name']}:#{publication['version']}" == record.dig("target", "resource") &&
29
- publication["artifact_relative_path"].is_a?(String) && publication["artifact_relative_path"].bytesize.between?(1, 4096) &&
30
- publication["artifact_relative_path"].end_with?(".gem") &&
31
- publication["artifact_relative_path"].split("/", -1).all? { |part| part.match?(/\A[a-zA-Z0-9_.-]+\z/) && !%w[. ..].include?(part) }
32
- raise AttemptErrors::ReceiptRejected, "publication result differs from original artifact"
33
- end
34
- value
35
- rescue JSON::ParserError, KeyError, TypeError, NoMethodError
36
- raise AttemptErrors::ReceiptRejected, "publication result is invalid"
37
- end
38
-
39
- def publication_collection!(references, record, state, pending)
40
- unless %w[succeeded failed].include?(state) && references.size == 1
41
- raise AttemptErrors::ReceiptRejected, "publication requires one exact result artifact"
42
- end
43
- expected = context(record, pending: pending)
44
- bytes = if pending && pending[:pending_events]
45
- @canonical.read_pending(references.first, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
46
- else
47
- @canonical.read(references.first, **expected, commit: pending && pending[:commit] || @journal.ref_value)
48
- end
49
- publication_result!(bytes, record, state: state)
50
- [bytes].freeze
51
- end
52
-
53
- def publication_need!(bytes, record, previous:)
54
- value = JSON.parse(bytes, allow_duplicate_key: false, allow_comments: false,
55
- allow_nan: false, create_additions: false, max_nesting: 16)
56
- unless bytes.bytesize.between?(1, 65536) && value.is_a?(Hash) &&
57
- value.keys.sort == PUBLICATION_NEED_FIELDS.sort && value["schema"] == "ace.publication-otp-need/v1" &&
58
- %w[otp-required otp-rejected otp-expired].include?(value["classification"]) &&
59
- %w[request_id input_digest claim_binding candidate_generation].all? { |key| value[key] == record[key] } &&
60
- value["head"] == record["candidate_head"] && value["registry"] == "https://rubygems.org" &&
61
- value["previous_challenge_digest"] == previous && value["artifact_digest"] == record.dig("target", "artifact_digest") &&
62
- "rubygems:#{value['gem_name']}:#{value['version']}" == record.dig("target", "resource") &&
63
- value["dispatch_event_digest"].is_a?(String) && value["dispatch_event_digest"].match?(/\A[0-9a-f]{64}\z/)
64
- raise AttemptErrors::ReceiptRejected, "publication OTP need differs from original dispatch"
65
- end
66
- value
67
- rescue JSON::ParserError, KeyError, TypeError
68
- raise AttemptErrors::ReceiptRejected, "publication OTP need is invalid"
69
- end
70
-
71
- def publication_challenge!(record, pending: nil)
72
- context(record)
73
- commit = pending && pending[:commit] || @journal.ref_value
74
- events = pending && pending[:pending_events] ? pending.fetch(:current_events) + pending.fetch(:pending_events) :
75
- @journal.read_events(record.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == record.fetch("attempt_id") }
76
- unless Models::EvidenceEvent.chain_valid?(events)
77
- raise AttemptErrors::EvidenceUnavailable, "publication challenge chain is corrupt"
78
- end
79
- event = events.find { |row| row["digest"] == record["publication_challenge_event_digest"] }
80
- unless event && event["type"] == "service_publication_challenge" &&
81
- event.dig("payload", "challenge_digest") == record["publication_challenge_digest"] &&
82
- event.dig("payload", "request_id") == record["request_id"] &&
83
- event.dig("payload", "generation") == record["publication_challenge_generation"]
84
- raise AttemptErrors::EvidenceUnavailable, "publication challenge introduction is unavailable"
85
- end
86
- reference = record.fetch("publication_challenge_ref")
87
- bytes = if pending && pending[:pending_events]
88
- @canonical.read_pending(reference, **context(record), current_events: pending.fetch(:current_events),
89
- pending_events: pending.fetch(:pending_events), blobs: pending.fetch(:blobs), commit: commit)
90
- else
91
- @canonical.read(reference, **context(record), commit: commit)
92
- end
93
- unless Digest::SHA256.hexdigest(bytes) == record.fetch("publication_challenge_digest")
94
- raise AttemptErrors::EvidenceUnavailable, "publication challenge digest differs"
95
- end
96
- need = publication_need!(bytes, record, previous: event.dig("payload", "previous_challenge_digest"))
97
- dispatch = events.find { |row| row["digest"] == need.fetch("dispatch_event_digest") }
98
- unless dispatch && dispatch["type"] == "authority_mutation" && dispatch.dig("payload", "operation") == "begin_dispatch" &&
99
- dispatch.dig("payload", "data", "request_id") == record.fetch("request_id") &&
100
- dispatch.dig("payload", "data", "executor_process_binding") == record.fetch("executor_process_binding")
101
- raise AttemptErrors::EvidenceUnavailable, "publication need has no original executor dispatch"
102
- end
103
- need
104
- rescue KeyError, TypeError
105
- raise AttemptErrors::EvidenceUnavailable, "publication challenge evidence is incomplete"
106
- end
107
- end
108
- end
109
- end
110
- end
@@ -1,255 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
- require "fileutils"
5
- require_relative "transfer_codec"
6
- require_relative "../molecules/evidence_journal"
7
-
8
- module Ace
9
- module Assign
10
- module Authority
11
- # Shared endpoint lifetime and bounded wire protocol. Concrete source
12
- # compositions authenticate their own installed principals before dispatch.
13
- # Protected deployment/kernel requirements remain in Server.
14
- class SocketServer
15
- CONNECTIONS = 16
16
-
17
- def initialize(service:, lifecycle:, services: [])
18
- unless services.is_a?(Array) && services.uniq.size == services.size &&
19
- services.all? { |owned| owned.respond_to?(:run) && owned.respond_to?(:stop) }
20
- raise ArgumentError, "authority services require distinct source-owned run/stop lifetimes"
21
- end
22
- @service, @lifecycle, @services = service, lifecycle, services.dup.freeze
23
- @service_threads = []
24
- @activation_gates = []
25
- @mutex = Mutex.new
26
- @connections = []
27
- @handlers = []
28
- @transfers = 0
29
- @stopped = false
30
- end
31
-
32
- def serve
33
- verify_listener!
34
- directory = File.dirname(@service.fetch("socket_path"))
35
- wire.root_path!(directory, directory: true, owner: @service.fetch("uid"))
36
- lock = File.open(File.join(directory, "listener.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
37
- unless lock.flock(File::LOCK_EX | File::LOCK_NB)
38
- raise AttemptErrors::Conflict, "assignment authority already owns its listener"
39
- end
40
- path = @service.fetch("socket_path")
41
- if File.exist?(path) || File.symlink?(path)
42
- raise AttemptErrors::Conflict, "existing authority endpoint requires verified installer recovery"
43
- end
44
- @listener = UNIXServer.new(path)
45
- @endpoint = wire.socket_identity(path)
46
- File.chmod(0o660, path)
47
- start_services!
48
- verify_services!
49
- @activation_gates.each { |gate| gate << :activate }
50
- until @stopped
51
- verify_services!
52
- ready = IO.select([@listener], nil, nil, 0.25)
53
- next unless ready
54
- socket = @listener.accept_nonblock(exception: false)
55
- next if socket == :wait_readable
56
- admitted = @mutex.synchronize do
57
- if @connections.size < CONNECTIONS
58
- @connections << socket
59
- true
60
- end
61
- end
62
- unless admitted
63
- refusal(socket, "busy")
64
- socket.close
65
- next
66
- end
67
- @mutex.synchronize do
68
- @handlers.reject! { |handler| !handler.alive? }
69
- @handlers << Thread.new(socket) { |connection| receive(connection) }
70
- end
71
- end
72
- rescue IOError, Errno::EBADF
73
- raise unless @stopped
74
- ensure
75
- begin
76
- stop
77
- stop_services!
78
- ensure
79
- # Closing a stream does not cancel an admitted transaction. Hold
80
- # the owner lock and endpoint until every handler has ended.
81
- begin
82
- @mutex.synchronize { @handlers.dup }.each(&:join)
83
- @lifecycle.close if @endpoint && @lifecycle.respond_to?(:close)
84
- ensure
85
- begin
86
- File.unlink(path) if @endpoint && same_endpoint?(path)
87
- ensure
88
- lock&.close
89
- end
90
- end
91
- end
92
- end
93
-
94
- def request_stop
95
- @stopped = true
96
- end
97
-
98
- def stop
99
- @stopped = true
100
- @listener&.close unless @listener&.closed?
101
- @mutex.synchronize { @connections.each { |socket| socket.close unless socket.closed? } }
102
- end
103
-
104
- private
105
-
106
- # Installed HITL and merge listeners share the assignment owner lock.
107
- # Every selected boundary must be ready before assignment ingress opens;
108
- # a stopped boundary closes admission until the complete owner restarts.
109
- def start_services!
110
- @services.each do |owned|
111
- ready = Queue.new
112
- activation = Queue.new
113
- @activation_gates << activation
114
- thread = Thread.new do
115
- owned.run(on_ready: lambda do
116
- ready << :ready
117
- unless activation.pop == :activate
118
- raise Ace::Runtime::RuntimeUnavailableError, "authority startup was cancelled"
119
- end
120
- end)
121
- rescue Exception => error
122
- ready << error
123
- ensure
124
- ready << :stopped
125
- end
126
- thread.report_on_exception = false
127
- @service_threads << [owned, thread]
128
- result = ready.pop(timeout: 5)
129
- raise Ace::Runtime::RuntimeUnavailableError, "authority service startup did not become ready" unless result == :ready || result.is_a?(Exception)
130
- raise result if result.is_a?(Exception)
131
- end
132
- end
133
-
134
- def verify_services!
135
- unless @service_threads.all? { |_, thread| thread.alive? }
136
- raise Ace::Runtime::RuntimeUnavailableError, "an owned service stopped before its authority"
137
- end
138
- end
139
-
140
- def stop_services!
141
- errors = []
142
- @activation_gates.each { |gate| gate << :cancel }
143
- @service_threads.reverse_each do |owned, _|
144
- owned.stop
145
- rescue Exception => error
146
- errors << error
147
- end
148
- @service_threads.each do |_, thread|
149
- thread.join
150
- rescue Exception => error
151
- errors << error
152
- end
153
- raise errors.first unless errors.empty?
154
- end
155
-
156
- def same_endpoint?(path)
157
- wire.socket_identity(path) == @endpoint
158
- rescue StandardError
159
- false
160
- end
161
-
162
- def verify_listener!
163
- raise NotImplementedError, "source composition must verify its installed listener"
164
- end
165
-
166
- def dispatch_request(socket, request, peer, role, deadline, frame_bytes:)
167
- params = request.fetch("params")
168
- admitted_transfer = false
169
- Molecules::EvidenceJournal.with_event_read_operation do
170
- binding = @lifecycle.transfer_binding(request) if @lifecycle.respond_to?(:transfer_binding)
171
- if binding
172
- raise AttemptErrors::UnauthorizedIdentity, "transfer role differs" unless binding.fetch(:roles).include?(role)
173
- raise ArgumentError, "transfer control header is oversized" if frame_bytes > 16_384
174
- authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
175
- admitted_transfer = @mutex.synchronize do
176
- if @transfers < 2
177
- @transfers += 1
178
- true
179
- end
180
- end
181
- raise AttemptErrors::Conflict, "transfer capacity is busy" unless admitted_transfer
182
- codec = transfer_codec
183
- if binding.fetch(:direction) == :upload
184
- result = codec.receive(socket, descriptor: params.fetch("transfer"), purpose: binding.fetch(:purpose), deadline: deadline) do |input|
185
- dispatch_lifecycle(request: request, peer: peer, role: role, transfer: input, deadline: deadline)
186
- end
187
- else
188
- result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
189
- parts = result.fetch(:transfer_parts)
190
- descriptor = codec.descriptor(parts, purpose: binding.fetch(:purpose))
191
- result = result.merge(data: result.fetch(:data).merge("transfer" => descriptor))
192
- end
193
- else
194
- raise ArgumentError, "unexpected transfer descriptor" if params.key?("transfer")
195
- result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
196
- end
197
- wire.write(socket, {"status" => "ok", "data" => result.fetch(:data),
198
- "transport" => {"replayed" => result.fetch(:replayed, false)}}, deadline: deadline, limit: 16_384)
199
- if binding && binding.fetch(:direction) == :download
200
- codec.send(socket, parts: parts, descriptor: descriptor, purpose: binding.fetch(:purpose), deadline: deadline)
201
- end
202
- end
203
- ensure
204
- @mutex.synchronize { @transfers -= 1 } if admitted_transfer
205
- end
206
-
207
- def authorize_transfer!(request:, peer:, role:, deadline:)
208
- @lifecycle.authorize_transfer!(request: request, peer: peer, role: role)
209
- end
210
-
211
- def dispatch_lifecycle(request:, peer:, role:, deadline:, transfer: nil)
212
- options = {request: request, peer: peer, role: role}
213
- options[:transfer] = transfer if transfer
214
- @lifecycle.dispatch(**options)
215
- end
216
-
217
- # These closed read operations have no request body. Half-close proves
218
- # framing completion before status or artifact data can be exposed.
219
- def bodyless_read!(socket, deadline)
220
- loop do
221
- remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
222
- raise ArgumentError, "read request framing is incomplete" unless remaining.positive? && IO.select([socket], nil, nil, remaining)
223
- bytes = socket.read_nonblock(1, exception: false)
224
- next if bytes == :wait_readable
225
- raise ArgumentError, "unexpected read request body" unless bytes.nil?
226
- return
227
- end
228
- end
229
-
230
- def transfer_codec
231
- @mutex.synchronize do
232
- @transfer_codec ||= begin
233
- root = @service.fetch("state_root")
234
- PrivateDirectory.verify!(root)
235
- spool = File.join(root, "transfers")
236
- Dir.mkdir(spool, 0o700) unless File.exist?(spool) || File.symlink?(spool)
237
- TransferCodec.new(root: spool)
238
- end
239
- end
240
- end
241
-
242
- def refusal(socket, code)
243
- wire.write(socket, {"status" => "error", "error" => {"code" => code, "message" => "Protected authority refused #{code}"}},
244
- deadline: wire.deadline(0.1))
245
- rescue StandardError
246
- nil
247
- end
248
-
249
- def wire
250
- Ace::Runtime::Molecules::ProtectedSocket
251
- end
252
- end
253
- end
254
- end
255
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- # Structural admission of source-owned pins. Artifact authentication belongs
7
- # to the selected entry owner; these references are never an execution grant.
8
- module TaskContextEntry
9
- LIMITS = {"manifest" => 65_536, "wrapper" => 33_554_432}.freeze
10
-
11
- def self.validate!(value)
12
- unless value.is_a?(Hash) && value.keys.sort == LIMITS.keys.sort
13
- raise ArgumentError, "task context entry fields differ"
14
- end
15
- LIMITS.each do |key, limit|
16
- reference = value[key]
17
- unless reference.is_a?(Hash) && reference.keys.sort == %w[bytes path sha256] &&
18
- reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, limit) &&
19
- reference["sha256"].is_a?(String) && reference["sha256"].match?(/\A[0-9a-f]{64}\z/) &&
20
- reference["path"].is_a?(String) && reference["path"].encoding == Encoding::UTF_8 &&
21
- reference["path"].valid_encoding? && reference["path"].bytesize.between?(2, 4096) &&
22
- !reference["path"].include?("\0") && reference["path"].start_with?("/") &&
23
- File.expand_path(reference["path"]) == reference["path"]
24
- raise ArgumentError, "task context entry reference differs"
25
- end
26
- end
27
- value
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,189 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "tempfile"
5
- require "timeout"
6
- require "ace/runtime/molecules/protected_socket"
7
- require_relative "private_directory"
8
- require_relative "../atoms/candidate_limits"
9
-
10
- module Ace
11
- module Assign
12
- module Authority
13
- # Binary framing after the existing authenticated control header. The
14
- # Server selects purpose from a source handler's fixed operation table,
15
- # admits a transfer slot before calling, and holds no journal lock here.
16
- class TransferCodec
17
- LIMITS = {prompt_text: [16_384, 1, 16_384], scope_boundary_observation: [65_536, 1, 65_536], candidate: [Atoms::CandidateLimits::BYTES, 1, Atoms::CandidateLimits::BYTES],
18
- release_artifact: [64 * 1024 * 1024, 1, 64 * 1024 * 1024],
19
- artifacts: [256 * 1024, 16, 64 * 1024],
20
- receipt_artifacts: [272 * 1024, 17, 64 * 1024],
21
- service_input: [64 * 1024, 1, 64 * 1024], assignment_snapshot: [Atoms::CandidateLimits::BYTES, 1, Atoms::CandidateLimits::BYTES]}.freeze
22
- SHA256 = /\A[0-9a-f]{64}\z/
23
-
24
- class Input
25
- def initialize(file, descriptor)
26
- @file, @descriptor = file, descriptor
27
- end
28
-
29
- def bytes(index: 0)
30
- unless index.is_a?(Integer) && index.between?(0, count - 1)
31
- raise ArgumentError, "Invalid transferred part index"
32
- end
33
- part = @descriptor.fetch("parts").fetch(index)
34
- offset = @descriptor.fetch("parts").take(index).sum { |entry| entry.fetch("bytes") }
35
- @file.seek(offset)
36
- bytes = (@file.read(part.fetch("bytes")) || "").b
37
- unless bytes.bytesize == part["bytes"] && Digest::SHA256.hexdigest(bytes) == part["sha256"]
38
- raise AttemptErrors::ReceiptRejected, "Transferred bytes no longer match accepted binding"
39
- end
40
- bytes
41
- end
42
-
43
- def count
44
- @descriptor.fetch("parts").length
45
- end
46
- end
47
-
48
- def initialize(root: nil)
49
- @root = root && PrivateDirectory.verify!(root)
50
- end
51
-
52
- # Upload write-EOF is required before invoking the mutation consumer.
53
- # Persistent launch prompt streams use their fixed framed entry point below.
54
- def receive(socket, descriptor:, purpose:, deadline:)
55
- receive_parts(socket, descriptor: descriptor, purpose: purpose, deadline: deadline, eof: true) { |input| yield input }
56
- end
57
-
58
- # Fixed authenticated launch_control stream only. A declared exact
59
- # prompt body is followed by the next bounded control frame; unlike
60
- # public uploads, this private duplex connection remains writable.
61
- def receive_launch_prompt(socket, descriptor:, transfer_id:, deadline:)
62
- deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
63
- marker = launch_prompt_marker(transfer_id)
64
- receive_parts(socket, descriptor: descriptor, purpose: :prompt_text, deadline: deadline, eof: false) do |input|
65
- boundary = Ace::Runtime::Molecules::ProtectedSocket.read(socket, deadline: deadline, limit: 16_384, with_size: true)
66
- unless boundary.fetch(:data) == marker && boundary.fetch(:bytesize) == (JSON.generate(marker) + "\n").bytesize
67
- reject!("Private prompt boundary differs")
68
- end
69
- yield input
70
- end
71
- end
72
-
73
- def send_launch_prompt(socket, bytes:, descriptor:, transfer_id:, deadline:)
74
- deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
75
- marker = launch_prompt_marker(transfer_id)
76
- send(socket, parts: [bytes], descriptor: descriptor, purpose: :prompt_text, deadline: deadline)
77
- Ace::Runtime::Molecules::ProtectedSocket.write(socket, marker, deadline: deadline, limit: 16_384)
78
- end
79
-
80
- def launch_prompt_marker(transfer_id)
81
- unless transfer_id.is_a?(String) && transfer_id.match?(/\A[0-9a-f]{32}\z/)
82
- raise ArgumentError, "invalid private prompt transfer identity"
83
- end
84
- {"type" => "launch_prompt_end", "transfer_id" => transfer_id}
85
- end
86
- private :launch_prompt_marker
87
-
88
- def receive_parts(socket, descriptor:, purpose:, deadline:, eof:)
89
- validate!(descriptor, purpose)
90
- descriptor = descriptor.merge("parts" => descriptor.fetch("parts").map { |part| part.dup.freeze }.freeze).freeze
91
- deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
92
- raise ArgumentError, "receive requires protected source scratch" unless @root
93
- PrivateDirectory.verify!(@root)
94
- Tempfile.create(["transfer-", ".bytes"], @root, binmode: true) do |file|
95
- digest = Digest::SHA256.new
96
- descriptor.fetch("parts").each do |part|
97
- part_digest = Digest::SHA256.new
98
- remaining = part.fetch("bytes")
99
- while remaining.positive?
100
- chunk = read(socket, [remaining, 65_536].min, deadline)
101
- reject!("Transferred bytes ended early") if chunk.nil?
102
- file.write(chunk)
103
- digest.update(chunk)
104
- part_digest.update(chunk)
105
- remaining -= chunk.bytesize
106
- end
107
- reject!("Transferred part digest differs") unless part_digest.hexdigest == part["sha256"]
108
- end
109
- reject!("Transferred aggregate digest differs") unless digest.hexdigest == descriptor["sha256"]
110
- reject!("Transferred bytes exceed declared size") if eof && !read(socket, 1, deadline).nil?
111
- file.flush
112
- file.rewind
113
- yield Input.new(file, descriptor)
114
- end
115
- end
116
- private :receive_parts
117
-
118
- def descriptor(parts, purpose:)
119
- reject!("Invalid transfer parts") unless parts.is_a?(Array) && parts.all? { |part| part.is_a?(String) }
120
- digest = Digest::SHA256.new
121
- parts.each { |part| digest.update(part.b) }
122
- result = {"version" => 1, "bytes" => parts.sum(&:bytesize), "sha256" => digest.hexdigest,
123
- "parts" => parts.map { |part| {"bytes" => part.bytesize, "sha256" => Digest::SHA256.hexdigest(part.b)} }}
124
- validate!(result, purpose)
125
- result
126
- end
127
-
128
- # The caller writes the bounded JSON descriptor first, then these raw
129
- # bytes. Both directions share the Server's original monotonic deadline.
130
- def send(socket, parts:, descriptor:, purpose:, deadline:)
131
- expected = self.descriptor(parts, purpose: purpose)
132
- reject!("Export descriptor differs from bytes") unless expected == descriptor
133
- deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
134
- parts.each do |part|
135
- offset = 0
136
- while offset < part.bytesize
137
- wait(socket, deadline, write: true)
138
- written = socket.write_nonblock(part.byteslice(offset, [65_536, part.bytesize - offset].min), exception: false)
139
- offset += written unless written == :wait_writable
140
- end
141
- end
142
- end
143
-
144
- def validate!(descriptor, purpose)
145
- total, count, each = LIMITS.fetch(purpose) { reject!("Unknown source transfer purpose") }
146
- unless descriptor.is_a?(Hash) && descriptor.keys.sort == %w[bytes parts sha256 version] &&
147
- descriptor["version"] == 1 && descriptor["bytes"].is_a?(Integer) &&
148
- descriptor["bytes"].between?(0, total) && descriptor["sha256"].is_a?(String) && descriptor["sha256"].match?(SHA256) &&
149
- descriptor["parts"].is_a?(Array) && descriptor["parts"].length.between?(1, count) &&
150
- descriptor["parts"].all? { |part| part.is_a?(Hash) && part.keys.sort == %w[bytes sha256] &&
151
- part["bytes"].is_a?(Integer) && part["bytes"].between?(0, each) &&
152
- part["sha256"].is_a?(String) && part["sha256"].match?(SHA256) } &&
153
- descriptor["bytes"] == descriptor["parts"].sum { |part| part["bytes"] } &&
154
- (!%i[candidate service_input prompt_text assignment_snapshot release_artifact].include?(purpose) || descriptor["bytes"].positive?)
155
- reject!("Invalid or oversized transfer descriptor")
156
- end
157
- if purpose == :receipt_artifacts
158
- receipt, *evidence = descriptor.fetch("parts")
159
- unless receipt.fetch("bytes").between?(1, 16 * 1024) && evidence.sum { |part| part.fetch("bytes") } <= 256 * 1024
160
- reject!("Receipt or evidence exceeds its fixed transfer limit")
161
- end
162
- end
163
- true
164
- end
165
-
166
- private
167
-
168
- def read(socket, count, deadline)
169
- loop do
170
- wait(socket, deadline)
171
- bytes = socket.read_nonblock(count, exception: false)
172
- return bytes unless bytes == :wait_readable
173
- end
174
- end
175
-
176
- def wait(socket, deadline, write: false)
177
- remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
178
- raise Timeout::Error, "Transfer deadline exceeded" unless remaining.positive?
179
- ready = IO.select(write ? nil : [socket], write ? [socket] : nil, nil, remaining)
180
- raise Timeout::Error, "Transfer deadline exceeded" unless ready
181
- end
182
-
183
- def reject!(message)
184
- raise AttemptErrors::MalformedTransfer, message
185
- end
186
- end
187
- end
188
- end
189
- end
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require_relative "../../../authority/protected_assignment_context"
5
-
6
- module Ace
7
- module Assign
8
- module CLI
9
- module Commands
10
- # Public `ace-assign attempt` command namespace (interface contract:
11
- # start, status, finish, reconcile).
12
- module Attempt
13
- # Shared coordinator construction, option validation, and JSON output.
14
- module Base
15
- private
16
-
17
- def protected_context(options)
18
- keys = %i[mapping assignment result head candidate_generation mutation expected_generation inbox_context registration signature]
19
- keys.each do |key|
20
- if options.key?(key) && options[key].to_s.strip.empty?
21
- raise Ace::Support::Cli::Error, "Empty protected --#{key.to_s.tr('_', '-')} selector"
22
- end
23
- end
24
- context = @protected_assignment_context ||= Ace::Assign::Authority::ProtectedAssignmentContext.load
25
- selected = keys.any? do |key|
26
- options.key?(key)
27
- end
28
- context if context.protected_participant? || selected || context.mapping_hint?
29
- rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
30
- protected_transport_unavailable!
31
- end
32
-
33
- def protected_attempt_request(context, options, usage)
34
- params = {"assignment_id" => require_option(options, :assignment, usage),
35
- "attempt_id" => require_option(options, :attempt, usage),
36
- "expected_generation" => integer_option(options, :expected_generation, usage)}
37
- context.verify_attempt_hints!(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"))
38
- [context.client(options: options), params, require_option(options, :mutation, usage)]
39
- rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
40
- protected_transport_unavailable!
41
- end
42
-
43
- def protected_call(client, operation, params, mutation)
44
- client.call(operation, params, mutation_id: mutation, timeout: 30).data
45
- rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
46
- protected_transport_unavailable!
47
- end
48
-
49
- def protected_transport_unavailable!
50
- raise AttemptErrors::EvidenceUnavailable, "Protected authority outcome unavailable; retain original selectors and mutation for reconciliation"
51
- end
52
-
53
- def integer_option(options, key, usage, positive: false)
54
- value = options[key]
55
- unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A(?:0|[1-9][0-9]*)\z/))
56
- raise Ace::Support::Cli::Error, "--#{key.to_s.tr('_', '-')} requires an exact integer: #{usage}"
57
- end
58
- number = value.is_a?(Integer) ? value : Integer(value, 10)
59
- unless positive ? number.positive? : number >= 0
60
- raise Ace::Support::Cli::Error, "--#{key.to_s.tr('_', '-')} is out of range"
61
- end
62
- number
63
- end
64
-
65
- def build_coordinator
66
- Organisms::AttemptCoordinator.new
67
- end
68
-
69
- def require_option(options, key, usage)
70
- value = options[key].to_s.strip
71
- raise Ace::Support::Cli::Error, "Missing --#{key}: usage: ace-assign attempt #{usage}" if value.empty?
72
-
73
- value
74
- end
75
-
76
- def emit_json(payload)
77
- puts JSON.generate(payload)
78
- end
79
- end
80
- end
81
- end
82
- end
83
- end
84
- end