ace-assign 0.70.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +3 -2099
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,286 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../molecules/evidence_journal"
4
- require_relative "../molecules/canonical_evidence"
5
- require_relative "service_cleanup_evidence"
6
- require_relative "service_cleanup_dispatch"
7
- require_relative "service_publication_evidence"
8
- require_relative "../molecules/execution_scope_lineage"
9
-
10
- module Ace
11
- module Assign
12
- module Authority
13
- # One source-owned context for initial and retained service evidence.
14
- # The journal supplies the record and transient pending view; callers
15
- # never supply an alternate reader, artifact path or trusted flag.
16
- class ServiceEvidence
17
- include ServiceCleanupEvidence
18
- include ServiceCleanupDispatch
19
- include ServicePublicationEvidence
20
- FIELDS = (Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS +
21
- %w[dispatch_ticket_id claim_binding candidate_generation claim_generation policy_digest]).freeze
22
- CHALLENGE_FIELDS = %w[version request_id input_digest claim_binding dispatch_ticket_id no_effect_challenge
23
- challenge_generation failure_event_digest failure_generation].freeze
24
- INSPECTION_FIELDS = %w[version request_id input_digest claim_binding no_effect_challenge challenge_generation
25
- challenge_event_digest failure_event_digest failure_generation target dispatch_phase effect_absent
26
- handler_terminated writers_absent].freeze
27
-
28
- def initialize(journal:, cleanup_artifacts: nil)
29
- @journal = journal
30
- @canonical = Molecules::CanonicalEvidence.new(journal: journal)
31
- @cleanup_artifacts = cleanup_artifacts
32
- end
33
-
34
- # Executor-only projection from the same authenticated original record;
35
- # selectors never come from the receiver or a caller-supplied target.
36
- def settlement_context(record, commit: @journal.ref_value, read_view: nil)
37
- pending = {commit: commit, service_read_view: read_view}
38
- context(record, pending: pending)
39
- challenge = challenge!(record, pending: pending, current: false) if
40
- %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| record.key?(key) }
41
- proof = cleanup_dispatch_context!(record, commit: commit, pending: pending) if
42
- record.values_at("operation", "dispatch_phase") == ["prune-preserved-workspace", "dispatch_started"]
43
- settlement_projection(record, pending, challenge, proof)
44
- rescue KeyError, TypeError
45
- raise AttemptErrors::EvidenceUnavailable, "original service settlement context is incomplete"
46
- end
47
-
48
- def context(record, no_effect: false, pending: nil)
49
- binding = FIELDS.to_h { |key| [key, record.fetch(key)] }
50
- if record.fetch("operation") == "prune-preserved-workspace" && record["dispatch_phase"] == "dispatch_started"
51
- binding["operation_owner_binding"] = record.fetch("operation_owner_binding")
52
- executor = record.fetch("executor_process_binding")
53
- boot = record.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
54
- Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
55
- raise AttemptErrors::EvidenceUnavailable, "original cleanup executor differs" unless executor.fetch("uid") == record.fetch("executor_uid")
56
- binding["executor_process_binding"] = executor
57
- end
58
- if record["operation"] == "publish" && record["dispatch_phase"] != "issued"
59
- executor = record.fetch("executor_process_binding")
60
- boot = record.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
61
- Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
62
- raise AttemptErrors::EvidenceUnavailable, "original publication executor differs" unless executor.fetch("uid") == record.fetch("executor_uid")
63
- binding["executor_process_binding"] = executor
64
- end
65
- unless %w[claim_binding policy_digest].all? { |key| binding[key].is_a?(String) && binding[key].match?(/\A[0-9a-f]{64}\z/) } &&
66
- %w[candidate_generation claim_generation].all? { |key| binding[key].is_a?(Integer) && binding[key].positive? } &&
67
- binding["dispatch_ticket_id"].is_a?(String) && binding["dispatch_ticket_id"].match?(Molecules::JournalMutation::ID)
68
- raise AttemptErrors::EvidenceUnavailable, "canonical dispatch binding is invalid"
69
- end
70
- if no_effect
71
- return context_for_challenge(record, challenge!(record, pending: pending))
72
- end
73
- {kind: "service", project_id: record.fetch("project_id"), assignment_id: record.fetch("assignment_id"),
74
- attempt_id: record.fetch("attempt_id"), peer_uid: record.fetch("executor_uid"), binding: binding,
75
- request_id_or_event_id: record.fetch("request_id"), generation: record.fetch("claim_generation")}
76
- rescue KeyError
77
- raise AttemptErrors::EvidenceUnavailable, "canonical service evidence binding is incomplete"
78
- end
79
-
80
- # Exact original challenge and failure selected from the same immutable
81
- # canonical prefix as its import. Pending events are the journal owner's
82
- # held CAS plan, never caller-supplied alternative history.
83
- def challenge!(record, pending: nil, current: true)
84
- context(record)
85
- selector = record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
86
- unless selector.keys.sort == %w[no_effect_challenge challenge_generation challenge_event_digest].sort &&
87
- selector["no_effect_challenge"].is_a?(String) && selector["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
88
- selector["challenge_generation"].is_a?(Integer) && selector["challenge_generation"].positive? &&
89
- selector["challenge_event_digest"].is_a?(String) && selector["challenge_event_digest"].match?(/\A[0-9a-f]{64}\z/)
90
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge selector is invalid"
91
- end
92
- commit = pending && pending[:commit] || @journal.ref_value
93
- events = if pending && pending[:pending_events]
94
- pending.fetch(:current_events) + pending.fetch(:pending_events)
95
- else
96
- service_read_events(record, pending)
97
- end
98
- unless Models::EvidenceEvent.chain_valid?(events)
99
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge chain is corrupt"
100
- end
101
- selected = events.select { |event| event["digest"] == record.fetch("challenge_event_digest") }
102
- unless selected.size == 1 && selected.first["type"] == "service_no_effect_challenge"
103
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge event is missing"
104
- end
105
- challenge = selected.first
106
- payload = challenge.fetch("payload")
107
- before = events.take_while { |event| event["digest"] != challenge.fetch("digest") }
108
- failures = events.select do |event|
109
- %w[service_claim service_transition].include?(event["type"]) &&
110
- event.dig("payload", "request_id") == record.fetch("request_id") &&
111
- %w[uncertain failed].include?(event.dig("payload", "state"))
112
- end
113
- failure = before.reverse.find { |event| failures.include?(event) }
114
- failure_prefix = before.take_while { |event| event["digest"] != failure&.fetch("digest") }
115
- generation = failure_prefix.count { |event| event["type"] == "authority_mutation" } + 1
116
- unless payload.is_a?(Hash) && payload.keys.sort == CHALLENGE_FIELDS.sort &&
117
- payload["version"].is_a?(Integer) && payload["version"] == 1 &&
118
- %w[challenge_generation failure_generation].all? { |key| payload[key].is_a?(Integer) && payload[key].positive? } &&
119
- payload["no_effect_challenge"].is_a?(String) && payload["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
120
- %w[request_id input_digest claim_binding dispatch_ticket_id no_effect_challenge challenge_generation].all? { |key| payload[key] == record[key] } &&
121
- failure && (!current || failures.last == failure) && before.any? { |event| event["digest"] == failure.fetch("digest") } &&
122
- payload["failure_event_digest"] == failure.fetch("digest") && payload["failure_generation"] == generation &&
123
- payload["challenge_generation"] == before.count { |event| event["type"] == "authority_mutation" } + 1
124
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge or current failure binding differs"
125
- end
126
- position = events.index(challenge)
127
- if pending && pending[:pending_events]&.any? { |event| event["digest"] == challenge.fetch("digest") }
128
- unless pending[:operation] == "claim_service_settlement"
129
- raise AttemptErrors::EvidenceUnavailable, "pending challenge has no fixed authority owner"
130
- end
131
- else
132
- update, accepted = events.values_at(position + 1, position + 2)
133
- expected_params = record.slice("mapping_id", "assignment_id", "attempt_id", "request_id", "candidate_generation").merge(
134
- "head" => record.fetch("candidate_head"), "expected_generation" => payload.fetch("challenge_generation") - 1)
135
- unless update && update["type"] == "service_challenge" && update.dig("payload", "request_id") == record.fetch("request_id") &&
136
- accepted && accepted["type"] == "authority_mutation" && accepted.dig("payload", "operation") == "claim_service_settlement" &&
137
- accepted.dig("payload", "assignment_id") == record.fetch("assignment_id") && accepted.dig("payload", "attempt_id") == record.fetch("attempt_id") &&
138
- accepted.dig("payload", "parameters_digest") == Atoms::EvidenceDigest.digest(expected_params) &&
139
- accepted.dig("payload", "data", "generation").is_a?(Integer) &&
140
- accepted.dig("payload", "data", "generation") == payload.fetch("challenge_generation") &&
141
- accepted.dig("payload", "data", "reconciliation_challenge", "challenge_generation").is_a?(Integer) &&
142
- accepted.dig("payload", "data", "request_id") == record.fetch("request_id") &&
143
- accepted.dig("payload", "data", "reconciliation_challenge") == record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
144
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge lacks its accepted authority mutation"
145
- end
146
- commits = service_read_introductions(record,
147
- [challenge.fetch("digest"), failure.fetch("digest"), update.fetch("digest"), accepted.fetch("digest")], pending)
148
- unless commits.fetch(challenge.fetch("digest")) == commits.fetch(update.fetch("digest")) &&
149
- commits.fetch(challenge.fetch("digest")) == commits.fetch(accepted.fetch("digest"))
150
- raise AttemptErrors::EvidenceUnavailable, "challenge record and authority acceptance were not atomic"
151
- end
152
- original = @journal.service_request(record.fetch("request_id"), commit: commits.fetch(challenge.fetch("digest")))
153
- unless original && %w[uncertain failed].include?(original["state"]) &&
154
- update.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(original) &&
155
- (FIELDS + %w[mapping_id service_id caller_uid authorization worker_process_binding launch_ticket reservation_generation dispatch_phase]).all? { |key| original[key] == record[key] } &&
156
- original.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest") ==
157
- record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
158
- raise AttemptErrors::EvidenceUnavailable, "challenge accepted record differs from original binding"
159
- end
160
- end
161
- challenge
162
- rescue KeyError, TypeError, NoMethodError
163
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge binding is incomplete"
164
- end
165
-
166
- def inspection!(content, record, challenge)
167
- unless content.is_a?(String) && content.bytesize <= Molecules::CanonicalEvidence::MAX_ARTIFACT_BYTES
168
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection exceeds artifact bounds"
169
- end
170
- text = content.dup.force_encoding(Encoding::UTF_8)
171
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is invalid UTF8" unless text.valid_encoding?
172
- lines = text.lines.filter_map { |line| line.delete_prefix("ace-service-no-effect ").chomp if line.start_with?("ace-service-no-effect ") }
173
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is missing or repeated" unless lines.size == 1
174
- inspection = JSON.parse(lines.first, create_additions: false, max_nesting: 8,
175
- allow_duplicate_key: false, allow_comments: false)
176
- inspection_projection!(inspection, record, challenge)
177
- rescue JSON::ParserError, KeyError, TypeError
178
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is malformed"
179
- end
180
-
181
- def inspection_projection!(inspection, record, challenge)
182
- expected = challenge.fetch("payload").slice("request_id", "input_digest", "claim_binding", "no_effect_challenge",
183
- "challenge_generation", "failure_event_digest", "failure_generation").merge("version" => 1,
184
- "challenge_event_digest" => challenge.fetch("digest"), "target" => record.fetch("target"),
185
- "dispatch_phase" => record.fetch("dispatch_phase"), "effect_absent" => true,
186
- "handler_terminated" => true, "writers_absent" => true)
187
- unless inspection.is_a?(Hash) && inspection.keys.sort == INSPECTION_FIELDS.sort &&
188
- %w[version challenge_generation failure_generation].all? { |key| inspection[key].is_a?(Integer) } &&
189
- inspection == expected
190
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection differs from original challenge"
191
- end
192
- true
193
- rescue KeyError, TypeError
194
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is malformed"
195
- end
196
-
197
- def call(reference, record, state, pending)
198
- if reference.is_a?(Array)
199
- return publication_collection!(reference, record, state, pending) if record["operation"] == "publish"
200
- return cleanup_collection!(reference, record, state, pending)
201
- end
202
- no_effect = state == "failed-settled"
203
- expected = context(record, no_effect: no_effect, pending: pending)
204
- bytes = if pending && pending[:pending_events]
205
- @canonical.read_pending(reference, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
206
- else
207
- @canonical.read(reference, **expected, commit: pending && pending[:commit] || @journal.ref_value)
208
- end
209
- if no_effect
210
- challenge = challenge!(record, pending: pending)
211
- inspection!(bytes, record, challenge)
212
- end
213
- bytes
214
- end
215
-
216
- private
217
-
218
- def settlement_projection(record, pending, challenge, proof)
219
- selected = nil
220
- if challenge
221
- latest_failure = service_read_events(record, pending).reverse.find do |event|
222
- event["attempt_id"] == record.fetch("attempt_id") &&
223
- %w[service_claim service_transition].include?(event["type"]) &&
224
- event.dig("payload", "request_id") == record.fetch("request_id") &&
225
- %w[uncertain failed].include?(event.dig("payload", "state"))
226
- end
227
- if latest_failure && latest_failure.fetch("digest") == challenge.dig("payload", "failure_event_digest")
228
- selected = challenge.fetch("payload").slice("request_id", "input_digest", "claim_binding",
229
- "no_effect_challenge", "challenge_generation", "failure_event_digest", "failure_generation")
230
- .merge("challenge_event_digest" => challenge.fetch("digest"))
231
- end
232
- end
233
- execution = record.slice("mapping_id", "candidate_generation", "claim_binding", "dispatch_phase")
234
- .merge("head" => record.fetch("candidate_head"))
235
- if record["operation"] == "prune-preserved-workspace" && record["dispatch_phase"] == "dispatch_started"
236
- execution.merge!(record.slice("operation_owner_binding", "executor_process_binding"))
237
- execution.merge!(proof.slice("request_event_digest", "dispatch_event_digest"))
238
- end
239
- value = {"version" => 1, "request" => record.slice(*Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS),
240
- "execution" => execution, "challenge" => selected}
241
- freeze_projection(JSON.parse(JSON.generate(value)))
242
- end
243
-
244
- def context_for_challenge(record, selection)
245
- expected = context(record)
246
- binding = expected.fetch(:binding)
247
- binding.merge!(record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest"))
248
- binding.merge!(selection.fetch("payload").slice("failure_event_digest", "failure_generation"))
249
- binding["dispatch_phase"] = record.fetch("dispatch_phase")
250
- expected
251
- end
252
-
253
- def service_read_view(pending)
254
- view = pending && pending[:service_read_view]
255
- return nil unless view
256
- unless view.is_a?(Molecules::EvidenceJournal::ServiceReadView) && view.journal.equal?(@journal) &&
257
- view.commit == pending.fetch(:commit) && view.inventory.fetch("commit") == view.commit
258
- raise AttemptErrors::EvidenceUnavailable, "service read view differs from its selected owner"
259
- end
260
- view
261
- end
262
-
263
- def service_read_events(record, pending)
264
- view = service_read_view(pending)
265
- selected = view ? view.inventory.fetch("events").fetch(record.fetch("assignment_id")) :
266
- @journal.read_events(record.fetch("assignment_id"), commit: pending && pending[:commit] || @journal.ref_value)
267
- selected.select { |event| event.fetch("attempt_id") == record.fetch("attempt_id") }
268
- end
269
-
270
- def service_read_introductions(record, selectors, pending)
271
- view = service_read_view(pending)
272
- return @journal.event_commits!(assignment_id: record.fetch("assignment_id"), event_digests: selectors,
273
- commit: pending && pending[:commit] || @journal.ref_value) unless view
274
- facts = view.inventory.fetch("introductions").fetch(record.fetch("assignment_id"))
275
- selectors.to_h { |selector| [selector, facts.fetch(selector)] }.freeze
276
- end
277
-
278
- def freeze_projection(value)
279
- value.each_value { |child| freeze_projection(child) } if value.is_a?(Hash)
280
- value.each { |child| freeze_projection(child) } if value.is_a?(Array)
281
- value.freeze
282
- end
283
- end
284
- end
285
- end
286
- end
@@ -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