ace-assign 0.71.0 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +5 -2116
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,151 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "execution_scope_lineage"
4
- require_relative "../atoms/attempt_state_machine"
5
-
6
- module Ace
7
- module Assign
8
- module Molecules
9
- # Canonical state projection, not live closure or settlement authority.
10
- # The stopped producer and terminal consumer must additionally obtain the
11
- # existing owners' authenticated original-prefix evidence and fresh proof.
12
- module CanonicalAttemptState
13
- STOPPED_FIELDS = %w[project_id mapping_id assignment_id attempt_id descriptor_sha256
14
- scope_binding_event_id seal_event_id closed_proof_event_id service_settlement_event_digests].freeze
15
- SELECTION_FIELDS = (STOPPED_FIELDS - %w[service_settlement_event_digests]).freeze
16
-
17
- def self.derive(events)
18
- return nil unless events.any? { |event| event["type"] == "intent" }
19
- state = "reserved"
20
- stopped = false
21
- events.each do |event|
22
- if stopped && %w[process_start receipt_accepted transition reconciliation attempt_stopped].include?(event["type"])
23
- unavailable!("Accepted stopped attempt cannot be resurrected or replaced")
24
- end
25
- case event["type"]
26
- when "process_start" then state = "running"
27
- when "receipt_accepted" then state = event.dig("payload", "receipt", "verdict") || state
28
- when "transition" then state = event.dig("payload", "to") || state
29
- when "reconciliation" then state = event.dig("payload", "resolution") || state
30
- when "attempt_stopped"
31
- verify_stopped_acceptance!(events, event, prior_state: state)
32
- state = "stopped"
33
- stopped = true
34
- end
35
- end
36
- state
37
- end
38
-
39
- def self.stopped_payload!(events:, selection:, service_evidence:, commit:)
40
- unless selection.is_a?(Hash) && selection.keys.sort == SELECTION_FIELDS.sort &&
41
- service_evidence.is_a?(Hash) && service_evidence.keys.sort == %w[commit services] &&
42
- commit.is_a?(String) && commit.match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
43
- service_evidence["commit"] == commit
44
- unavailable!("Stopped plan evidence prefix differs")
45
- end
46
- services = service_evidence.fetch("services")
47
- unless services.is_a?(Array) &&
48
- services.all? { |item| item.is_a?(Hash) && item.keys.sort == %w[event_digest evidence_refs receipt_digest record_digest request_id state] }
49
- unavailable!("Stopped settlement projections are malformed")
50
- end
51
- service_ids = services.map { |item| item.fetch("request_id") }
52
- unless service_ids == service_ids.sort && service_ids.uniq == service_ids
53
- unavailable!("Stopped settlement projection is not ordered unique owner output")
54
- end
55
- services.each do |item|
56
- event = events.find { |entry| entry["digest"] == item.fetch("event_digest") && entry["type"] == "service_transition" }
57
- unless event && event.dig("payload", "request_id") == item.fetch("request_id") &&
58
- event.dig("payload", "state") == item.fetch("state") && %w[succeeded failed-settled].include?(item.fetch("state")) &&
59
- event.dig("payload", "record_digest") == item.fetch("record_digest") &&
60
- event.dig("payload", "receipt_digest") == item.fetch("receipt_digest")
61
- unavailable!("Stopped service projection differs from canonical terminal event")
62
- end
63
- end
64
- payload = selection.merge("service_settlement_event_digests" => services.map { |item| item.fetch("event_digest") }.sort)
65
- validate_stopped_payload!(events, payload)
66
- payload
67
- rescue KeyError, TypeError, ArgumentError, NoMethodError
68
- unavailable!("Stopped plan evidence is malformed")
69
- end
70
-
71
- def self.verify_stopped_acceptance!(events, event, prior_state:)
72
- Atoms::AttemptStateMachine.proof_stopped_transition!(prior_state)
73
- unless events.count { |entry| entry["type"] == "attempt_stopped" } == 1 && Models::EvidenceEvent.chain_valid?(events)
74
- unavailable!("Stopped canonical chain is ambiguous or corrupt")
75
- end
76
- payload = event.fetch("payload")
77
- prefix = events.take_while { |entry| entry["digest"] != event.fetch("digest") }
78
- validate_stopped_payload!(prefix, payload)
79
- acceptance = events.find { |entry| entry["type"] == "authority_mutation" && entry["previous_digest"] == event.fetch("digest") }
80
- data = acceptance&.dig("payload", "data")
81
- generation = acceptance && events.take_while { |entry| entry["digest"] != acceptance["digest"] }.count { |entry| entry["type"] == "authority_mutation" } + 1
82
- unless acceptance && data.is_a?(Hash) && data.keys.sort == %w[attempt_id generation proof_id required_action state] &&
83
- data["generation"].is_a?(Integer) && data["generation"] == generation &&
84
- acceptance.dig("payload", "operation") == "stop_attempt" &&
85
- acceptance.dig("payload", "assignment_id") == payload.fetch("assignment_id") &&
86
- acceptance.dig("payload", "attempt_id") == payload.fetch("attempt_id") &&
87
- acceptance.dig("payload", "data", "attempt_id") == payload.fetch("attempt_id") &&
88
- acceptance.dig("payload", "data", "state") == "stopped" &&
89
- acceptance.dig("payload", "data", "proof_id") == payload.fetch("closed_proof_event_id") &&
90
- acceptance.dig("payload", "data", "required_action").nil?
91
- unavailable!("Stopped transition has no adjacent accepted owner mutation")
92
- end
93
- seal = prefix.find { |entry| entry["digest"] == payload.fetch("seal_event_id") }
94
- accepted_seal = seal && prefix.find { |entry| entry["type"] == "authority_mutation" && entry["previous_digest"] == seal["digest"] &&
95
- %w[close_execution_scope stop_attempt].include?(entry.dig("payload", "operation")) }
96
- proof = prefix.find { |entry| entry["digest"] == payload.fetch("closed_proof_event_id") }
97
- accepted_proof = proof && prefix.find { |entry| entry["type"] == "authority_mutation" && entry["previous_digest"] == proof["digest"] &&
98
- %w[close_execution_scope stop_attempt].include?(entry.dig("payload", "operation")) &&
99
- entry.dig("payload", "data", "proof_id") == proof["digest"] &&
100
- (entry.dig("payload", "operation") == "close_execution_scope" ?
101
- entry.dig("payload", "data", "state") == "closed_no_writers" :
102
- entry.dig("payload", "data").is_a?(Hash) && entry.dig("payload", "data").keys.sort == %w[attempt_id generation proof_id required_action state] &&
103
- entry.dig("payload", "data", "attempt_id") == payload.fetch("attempt_id") &&
104
- entry.dig("payload", "data", "generation").is_a?(Integer) &&
105
- entry.dig("payload", "data", "generation") == prefix.take_while { |item| item["digest"] != entry["digest"] }.count { |item| item["type"] == "authority_mutation" } + 1 &&
106
- entry.dig("payload", "data", "state") == "uncertain" &&
107
- entry.dig("payload", "data", "required_action") == "reconcile_scope") }
108
- unless accepted_seal && (accepted_proof || event["previous_digest"] == proof&.fetch("digest"))
109
- unavailable!("Stopped canonical seal or proof has no accepted owner")
110
- end
111
- event
112
- rescue KeyError, TypeError, ArgumentError, NoMethodError, AttemptErrors::InvalidTransition
113
- unavailable!("Stopped canonical acceptance is malformed")
114
- end
115
-
116
- def self.validate_stopped_payload!(events, payload)
117
- unless payload.is_a?(Hash) && payload.keys.sort == STOPPED_FIELDS.sort &&
118
- %w[project_id mapping_id assignment_id attempt_id].all? { |key| payload[key].is_a?(String) && ExecutionScopeLineage::ID.match?(payload[key]) } &&
119
- %w[descriptor_sha256 scope_binding_event_id seal_event_id closed_proof_event_id].all? { |key| digest?(payload[key]) } &&
120
- %w[service_settlement_event_digests].all? { |key|
121
- values = payload[key]
122
- values.is_a?(Array) && values.all? { |value| digest?(value) } && values == values.sort && values.uniq == values }
123
- unavailable!("Stopped canonical payload is not closed")
124
- end
125
- lineage = ExecutionScopeLineage.new(events: events, **payload.slice("project_id", "mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
126
- provisioning = events.select { |event| event["type"] == "scope_provisioning" }
127
- unless provisioning.one? && provisioning.first.dig("payload", "descriptor_sha256") == payload.fetch("descriptor_sha256") &&
128
- lineage.native_event && lineage.child_event &&
129
- lineage.binding_event&.fetch("digest") == payload.fetch("scope_binding_event_id") &&
130
- lineage.seal_event&.fetch("digest") == payload.fetch("seal_event_id") &&
131
- lineage.proof_event&.fetch("digest") == payload.fetch("closed_proof_event_id")
132
- unavailable!("Stopped canonical original scope selectors differ")
133
- end
134
- payload.fetch("service_settlement_event_digests").each do |digest|
135
- unless events.any? { |event| event["digest"] == digest && event["type"] == "service_transition" &&
136
- %w[succeeded failed-settled].include?(event.dig("payload", "state")) }
137
- unavailable!("Stopped canonical service selector is not terminal evidence")
138
- end
139
- end
140
- payload
141
- rescue KeyError, TypeError, ArgumentError, NoMethodError
142
- unavailable!("Stopped canonical scope is malformed")
143
- end
144
-
145
- def self.digest?(value) = value.is_a?(String) && ExecutionScopeLineage::DIGEST.match?(value)
146
- def self.unavailable!(message) = raise AttemptErrors::EvidenceUnavailable, message
147
- private_class_method :digest?, :unavailable!
148
- end
149
- end
150
- end
151
- end
@@ -1,170 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "securerandom"
5
- require "time"
6
-
7
- module Ace
8
- module Assign
9
- module Molecules
10
- # One import/verification contract for protected review, service and
11
- # inbox evidence. Filesystem projections never supply accepted bytes.
12
- class CanonicalEvidence
13
- MAX_ARTIFACT_BYTES = 64 * 1024
14
- MAX_TOTAL_BYTES = 256 * 1024
15
- MAX_ARTIFACTS = 16
16
- ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
17
- MAX_RELEASE_ARTIFACT_BYTES = 64 * 1024 * 1024
18
- ROLES = {"service" => "executor", "review" => "reviewer",
19
- "result" => "worker", "release-artifact" => "worker"}.freeze
20
- REPORT_LIMITS = [MAX_ARTIFACT_BYTES, MAX_TOTAL_BYTES, MAX_ARTIFACTS].freeze
21
- LIMITS = {"service" => REPORT_LIMITS, "review" => REPORT_LIMITS, "result" => REPORT_LIMITS,
22
- "release-artifact" => [MAX_RELEASE_ARTIFACT_BYTES, MAX_RELEASE_ARTIFACT_BYTES, 1].freeze}.freeze
23
- DESCRIPTOR_FIELDS = %w[version artifact_id kind project_id assignment_id attempt_id peer_uid role
24
- binding_digest sha256 bytes admitted_at admitted_after_event_digest request_id_or_event_id
25
- candidate_generation_or_claim_generation].freeze
26
-
27
- # Shared closed descriptor validation before clients accept any bytes.
28
- def self.valid_descriptor?(descriptor)
29
- return false unless descriptor.is_a?(Hash) && descriptor.keys.sort == DESCRIPTOR_FIELDS.sort &&
30
- descriptor["version"] == 1 && ROLES.key?(descriptor["kind"]) && ROLES[descriptor["kind"]] == descriptor["role"] &&
31
- %w[artifact_id project_id assignment_id attempt_id request_id_or_event_id].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(ID) } &&
32
- %w[binding_digest sha256 admitted_after_event_digest].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{64}\z/) } &&
33
- descriptor["peer_uid"].is_a?(Integer) && descriptor["peer_uid"] >= 0 &&
34
- descriptor["bytes"].is_a?(Integer) && descriptor["bytes"].between?(descriptor["kind"] == "release-artifact" ? 1 : 0,
35
- LIMITS.fetch(descriptor["kind"]).first) &&
36
- descriptor["candidate_generation_or_claim_generation"].is_a?(Integer) && descriptor["candidate_generation_or_claim_generation"] >= 0 &&
37
- descriptor["admitted_at"].is_a?(String)
38
- Time.iso8601(descriptor.fetch("admitted_at"))
39
- true
40
- rescue ArgumentError, TypeError
41
- false
42
- end
43
-
44
- def initialize(journal:)
45
- @journal = journal
46
- end
47
-
48
- # Construct the immutable bytes/events for EvidenceJournal#mutate.
49
- # peer_uid comes from the accepted kernel-authenticated connection;
50
- # callers of the wire API cannot supply provenance or artifact IDs.
51
- def import_plan(kind:, project_id:, assignment_id:, attempt_id:, peer_uid:, binding:,
52
- request_id_or_event_id:, generation:, admitted_after_event_digest:, artifacts:)
53
- role = ROLES.fetch(kind) { raise ArgumentError, "invalid evidence kind" }
54
- max_each, max_total, max_count = LIMITS.fetch(kind)
55
- minimum = kind == "release-artifact" ? 1 : 0
56
- ids = [project_id, assignment_id, attempt_id, request_id_or_event_id]
57
- unless ids.all? { |id| id.is_a?(String) && id.match?(ID) } && binding.is_a?(Hash) &&
58
- admitted_after_event_digest.is_a?(String) && admitted_after_event_digest.match?(/\A[0-9a-f]{64}\z/) &&
59
- peer_uid.is_a?(Integer) && peer_uid >= 0 && generation.is_a?(Integer) && generation >= 0 &&
60
- artifacts.is_a?(Array) && !artifacts.empty? && artifacts.length <= max_count &&
61
- artifacts.all? { |bytes| bytes.is_a?(String) && bytes.bytesize.between?(minimum, max_each) } &&
62
- artifacts.sum(&:bytesize) <= max_total
63
- raise ArgumentError, "invalid or oversized canonical evidence import"
64
- end
65
- events = []
66
- blobs = {}
67
- references = artifacts.map do |bytes|
68
- artifact_id = SecureRandom.hex(16)
69
- artifact_id += "-no-effect" if kind == "service" && binding["no_effect_challenge"]
70
- digest = Digest::SHA256.hexdigest(bytes.b)
71
- path = "evidence/imports/#{artifact_id}"
72
- descriptor = {"version" => 1, "artifact_id" => artifact_id, "kind" => kind,
73
- "project_id" => project_id, "assignment_id" => assignment_id,
74
- "attempt_id" => attempt_id, "peer_uid" => peer_uid, "role" => role,
75
- "binding_digest" => Atoms::EvidenceDigest.digest(binding), "sha256" => digest,
76
- "bytes" => bytes.bytesize, "admitted_at" => Time.now.utc.iso8601(9),
77
- "admitted_after_event_digest" => admitted_after_event_digest,
78
- "request_id_or_event_id" => request_id_or_event_id,
79
- "candidate_generation_or_claim_generation" => generation}
80
- events << {type: "evidence_import", payload: descriptor}
81
- blobs[path] = bytes.b
82
- {"ref" => path, "sha256" => digest}
83
- end
84
- {events: events, blobs: blobs, references: references}
85
- end
86
-
87
- def read(reference, kind:, project_id:, assignment_id:, attempt_id:, peer_uid:, binding:,
88
- request_id_or_event_id:, generation:, commit: @journal.ref_value)
89
- # One immutable commit supplies both provenance events and bytes.
90
- events = @journal.read_events(assignment_id, commit: commit)
91
- .select { |event| event["attempt_id"] == attempt_id }
92
- verified_bytes(reference, events: events, kind: kind, project_id: project_id,
93
- assignment_id: assignment_id, attempt_id: attempt_id, peer_uid: peer_uid, binding: binding,
94
- request_id_or_event_id: request_id_or_event_id, generation: generation) do |path|
95
- if kind == "release-artifact"
96
- @journal.bounded_blob(path, commit: commit, max_bytes: MAX_RELEASE_ARTIFACT_BYTES)
97
- else
98
- @journal.blob(path, commit: commit)
99
- end
100
- end
101
- end
102
-
103
- # Only the journal writer supplies this transient pre-CAS view. It
104
- # validates the exact same provenance/chain/bytes as committed reads;
105
- # it does not accept a receipt or advance the canonical ref itself.
106
- def read_pending(reference, current_events:, pending_events:, blobs:, commit:,
107
- kind:, project_id:, assignment_id:, attempt_id:, peer_uid:, binding:,
108
- request_id_or_event_id:, generation:)
109
- events = current_events + pending_events
110
- unless events.all? { |event| event["attempt_id"] == attempt_id } && blobs.is_a?(Hash)
111
- unavailable!
112
- end
113
- verified_bytes(reference, events: events, kind: kind, project_id: project_id,
114
- assignment_id: assignment_id, attempt_id: attempt_id, peer_uid: peer_uid, binding: binding,
115
- request_id_or_event_id: request_id_or_event_id, generation: generation) do |path|
116
- if blobs.key?(path)
117
- blobs.fetch(path)
118
- elsif kind == "release-artifact"
119
- @journal.bounded_blob(path, commit: commit, max_bytes: MAX_RELEASE_ARTIFACT_BYTES)
120
- else
121
- @journal.blob(path, commit: commit)
122
- end
123
- end
124
- end
125
-
126
- private
127
-
128
- def verified_bytes(reference, events:, kind:, project_id:, assignment_id:, attempt_id:, peer_uid:, binding:,
129
- request_id_or_event_id:, generation:)
130
- unless reference.is_a?(Hash) && reference.keys.sort == %w[ref sha256] &&
131
- reference["ref"].is_a?(String) && reference["ref"].match?(%r{\Aevidence/imports/[a-z0-9-]+\z})
132
- unavailable!
133
- end
134
- unavailable! unless Models::EvidenceEvent.chain_valid?(events)
135
- artifact_id = reference.fetch("ref").delete_prefix("evidence/imports/")
136
- imports = events.select do |event|
137
- event["type"] == "evidence_import" && event.dig("payload", "artifact_id") == artifact_id
138
- end
139
- unavailable! unless imports.length == 1
140
- event = imports.first
141
- descriptor = event.fetch("payload")
142
- expected = {"version" => 1, "artifact_id" => artifact_id, "kind" => kind,
143
- "project_id" => project_id, "assignment_id" => assignment_id,
144
- "attempt_id" => attempt_id, "peer_uid" => peer_uid, "role" => ROLES.fetch(kind),
145
- "binding_digest" => Atoms::EvidenceDigest.digest(binding),
146
- "sha256" => reference.fetch("sha256"), "request_id_or_event_id" => request_id_or_event_id,
147
- "candidate_generation_or_claim_generation" => generation}
148
- unavailable! unless self.class.valid_descriptor?(descriptor) &&
149
- expected.all? { |key, value| descriptor[key] == value }
150
- before = events.take_while { |entry| entry["digest"] != event["digest"] }
151
- unavailable! unless before.any? { |entry| entry["digest"] == descriptor["admitted_after_event_digest"] }
152
- if kind == "service" && binding["no_effect_challenge"]
153
- unavailable! unless descriptor["admitted_after_event_digest"] == binding.fetch("challenge_event_digest")
154
- end
155
- Time.iso8601(descriptor.fetch("admitted_at"))
156
- bytes = yield(reference.fetch("ref"))
157
- unavailable! unless bytes.is_a?(String) && bytes.bytesize <= LIMITS.fetch(kind).first && bytes.bytesize == descriptor["bytes"] &&
158
- Digest::SHA256.hexdigest(bytes) == descriptor["sha256"]
159
- bytes
160
- rescue ArgumentError, KeyError, TypeError
161
- unavailable!
162
- end
163
-
164
- def unavailable!
165
- raise AttemptErrors::EvidenceUnavailable, "Canonical evidence import or binding is unverifiable"
166
- end
167
- end
168
- end
169
- end
170
- end
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Molecules
6
- # Closed bounded read dialect shared by isolated protected snapshots and
7
- # trusted installed journals. Neither boundary can dispatch a Git write.
8
- module CanonicalReadCommands
9
- BYTE_LIMIT = 256 * 1_048_576
10
- OID = /\A[0-9a-f]{40}\z/
11
-
12
- attr_reader :commit
13
-
14
- # EvidenceJournal uses this only as its existing read command boundary.
15
- def call(argv, stdin_data: "", output_limit: BYTE_LIMIT)
16
- refuse!("read_outside_snapshot") unless @view && @commit
17
- unless read_command?(argv, stdin_data)
18
- refuse!("write_or_unsupported_command")
19
- end
20
- result = read_result(argv, stdin_data: stdin_data, limit: [output_limit, BYTE_LIMIT].min)
21
- [result.stdout.to_s.strip, result.stderr.to_s.strip, result.status]
22
- end
23
-
24
- def blob_batch(entries, output_limit:)
25
- refuse!("read_outside_snapshot") unless @view && @commit
26
- unless entries.is_a?(Array) && entries.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
27
- refuse!("unsupported_command")
28
- end
29
- result = run!(["cat-file", "--batch"], stdin_data: entries.join("\n") + "\n",
30
- limit: [output_limit, BYTE_LIMIT].min)
31
- result.stdout.to_s.b
32
- end
33
-
34
- def history_diff(commits, paths, output_limit:)
35
- refuse!("read_outside_snapshot") unless @view && @commit
36
- unless commits.is_a?(Array) && commits.size.between?(1, 64) && commits.uniq.size == commits.size &&
37
- commits.all? { |commit| commit.is_a?(String) && commit.match?(OID) } &&
38
- paths.is_a?(Array) && !paths.empty? && paths.uniq.size == paths.size &&
39
- (paths == ["execution/"] || paths.all? { |path| path.is_a?(String) && path.match?(%r{\Aexecution/[a-zA-Z0-9_.:-]+/events/\z}) }) &&
40
- output_limit.is_a?(Integer) && output_limit.positive?
41
- refuse!("unsupported_command")
42
- end
43
- result = run!(%w[diff-tree --stdin --always --root -r --raw -z --no-renames --no-ext-diff
44
- --no-textconv --no-abbrev --] + paths, stdin_data: commits.join("\n") + "\n",
45
- limit: [output_limit, BYTE_LIMIT].min)
46
- result.stdout.to_s.b
47
- end
48
-
49
- def blob_sizes(oids)
50
- refuse!("read_outside_snapshot") unless @view && @commit
51
- unless oids.is_a?(Array) && oids.size.between?(1, 64) && oids.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
52
- refuse!("unsupported_command")
53
- end
54
- run!(%w[cat-file --batch-check], stdin_data: oids.join("\n") + "\n", limit: oids.size * 128).stdout.to_s.b
55
- end
56
-
57
- def blob(path, commit:, max_bytes:)
58
- refuse!("read_outside_snapshot") unless @view && @commit
59
- unless commit.is_a?(String) && commit.match?(OID) && max_bytes.is_a?(Integer) && max_bytes.positive?
60
- refuse!("unsupported_command")
61
- end
62
- result = run!(["cat-file", "blob", "#{commit}:#{path}"], limit: [max_bytes, BYTE_LIMIT].min)
63
- result.stdout.to_s.b
64
- end
65
-
66
- private
67
-
68
- def read_result(argv, **options)
69
- run!(argv, **options)
70
- end
71
-
72
- def read_command?(argv, stdin)
73
- return false unless argv.is_a?(Array) && argv.all? { |item| item.is_a?(String) && !item.include?("\0") } && stdin.empty?
74
- allowed = {"rev-parse" => %w[--verify --quiet --git-dir], "cat-file" => %w[-t -s blob],
75
- "rev-list" => %w[--first-parent --parents], "ls-tree" => %w[-r -l -d --name-only --],
76
- "show" => [], "log" => %w[--diff-filter=A -1 --format=%H --]}
77
- flags = allowed[argv.first]
78
- flags && argv.drop(1).all? { |item| !item.start_with?("-") || flags.include?(item) || item == "--max-count=100001" }
79
- end
80
-
81
- end
82
- end
83
- end
84
- end