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,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "json"
5
-
6
- module Ace
7
- module Assign
8
- module Atoms
9
- # Pure canonical digest helpers for attempt evidence.
10
- #
11
- # Every accepted evidence record (events, receipts, artifacts) carries a
12
- # SHA-256 digest over a canonical JSON serialization (sorted keys, no
13
- # insignificant whitespace) so records remain comparable across writers
14
- # and reloads. Digests are over non-secret metadata only.
15
- module EvidenceDigest
16
- # Canonical JSON serialization of an object.
17
- #
18
- # Sorts hash keys recursively and strips insignificant whitespace so
19
- # semantically equal payloads produce byte-identical output.
20
- #
21
- # @param object [Object] JSON-serializable object
22
- # @return [String] Canonical JSON string
23
- def self.canonical_json(object)
24
- JSON.generate(sort_keys(object))
25
- end
26
-
27
- # SHA-256 hex digest over the canonical JSON of an object.
28
- #
29
- # @param object [Object] JSON-serializable object
30
- # @return [String] 64-character hex digest
31
- def self.digest(object)
32
- digest_string(canonical_json(object))
33
- end
34
-
35
- # SHA-256 hex digest over raw bytes.
36
- #
37
- # @param bytes [String] Raw byte content
38
- # @return [String] 64-character hex digest
39
- def self.digest_string(bytes)
40
- Digest::SHA256.hexdigest(bytes)
41
- end
42
-
43
- # SHA-256 hex digest of a file's contents.
44
- #
45
- # @param path [String] File path
46
- # @return [String] 64-character hex digest
47
- # @raise [Errno::ENOENT] if the file does not exist
48
- def self.digest_file(path)
49
- digest_string(File.read(path))
50
- end
51
-
52
- # Short digest prefix for file naming (collision-resistant enough for
53
- # append-only event filenames; full digests remain in the payload).
54
- #
55
- # @param object [Object] JSON-serializable object
56
- # @return [String] 12-character hex prefix
57
- def self.short_digest(object)
58
- digest(object)[0, 12]
59
- end
60
-
61
- class << self
62
- private
63
-
64
- def sort_keys(value)
65
- case value
66
- when Hash
67
- value.sort.to_h.transform_values { |v| sort_keys(v) }
68
- when Array
69
- value.map { |item| sort_keys(item) }
70
- else
71
- value
72
- end
73
- end
74
- end
75
- end
76
- end
77
- end
78
- end
@@ -1,398 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "json"
5
- require "time"
6
- require "uri"
7
-
8
- module Ace
9
- module Assign
10
- module Atoms
11
- # Closed wire schemas for the ordinary fixed merge service (spec
12
- # 8x3.t.xz9.4). One owner for the exact request document, the terminal
13
- # fixed-effect receipt, and their semantic joins; both ace-assign
14
- # admission and the ace-lab producer enforce this exact module instead
15
- # of two dialects. Unknown fields are rejected at every nesting level,
16
- # formats (uri, date-time) are enforced, and every pattern/bound below
17
- # is the reviewed spec text — never widened.
18
- module FixedMergeSchemas
19
- REQUEST_SCHEMA_ID = "ace.lab.service.ordinary-merge-request/v1"
20
- RECEIPT_SCHEMA_ID = "ace.assign.fixed-effect-receipt/v1"
21
- ACK_SCHEMA_ID = "ace.lab.service.ordinary-merge-ack/v1"
22
- REFUSAL_SCHEMA_ID = "ace.lab.service.ordinary-merge-refusal/v1"
23
- STATUS_SCHEMA_ID = "ace.lab.service.ordinary-merge-status/v1"
24
- STATUS_ERROR_SCHEMA_ID = "ace.lab.service.ordinary-merge-status-error/v1"
25
-
26
- SHA256 = /\A[0-9a-f]{64}\z/
27
- HEAD = /\A[0-9a-f]{40}\z/
28
- FORGE = /\A[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\z/
29
- TERMINAL_STATUSES = %w[succeeded failed].freeze
30
- EFFECT_OUTCOMES = %w[merged already_merged failed_no_effect].freeze
31
- FAILURE_CODES = [nil, "merge_failed_no_effect"].freeze
32
-
33
- # Every key must be an exact String with no unknown siblings.
34
- class SchemaError < ::StandardError; end
35
-
36
- module_function
37
-
38
- # Canonical request digest — spec ruling r4: lowercase hex SHA-256 over
39
- # the canonical JSON (recursively key-sorted by code point, compact
40
- # encoder, UTF-8) of the FULL eight-field immutable binding. The digest
41
- # covers the binding, never the input document alone: byte-identical
42
- # input under any different selector, authorization or request ID
43
- # yields a different digest. The producer computes it at admission and
44
- # re-derives it for replay and receipt verification; consumers re-derive
45
- # the identical digest from their recorded binding. Both packages call
46
- # this one implementation so a pair can never diverge.
47
- def canonical_request_digest(project:, assignment:, attempt:, queue_entry:,
48
- authorization:, request_id:, input:)
49
- preimage = canonical({
50
- "project" => project, "assignment" => assignment, "attempt" => attempt,
51
- "queue_entry" => queue_entry, "operation" => "merge",
52
- "authorization" => authorization, "request_id" => request_id, "input" => input
53
- })
54
- Digest::SHA256.hexdigest(JSON.generate(preimage))
55
- end
56
-
57
- # Recursively sort object keys by code point; arrays keep order.
58
- def canonical(value)
59
- case value
60
- when Hash then value.keys.sort.each_with_object({}) { |key, result| result[key] = canonical(value[key]) }
61
- when Array then value.map { |item| canonical(item) }
62
- else value
63
- end
64
- end
65
-
66
- # The only admitted ordinary merge request document.
67
- def validate_request!(document)
68
- require_object!(document, "request")
69
- require_exact_keys!(document, "request", %w[schema forge pr_url head_repository head_ref
70
- candidate_head base_repository base_ref expected_base_head method review_evidence test_evidence])
71
- const!(document, "schema", REQUEST_SCHEMA_ID)
72
- string_bound!(document, "forge", 1, 128, pattern: FORGE)
73
- string_bound!(document, "pr_url", 1, 2048)
74
- uri_format!(document.fetch("pr_url"))
75
- string_bound!(document, "head_repository", 1, 1024)
76
- string_bound!(document, "head_ref", 1, 255)
77
- pattern!(document, "candidate_head", HEAD)
78
- string_bound!(document, "base_repository", 1, 1024)
79
- string_bound!(document, "base_ref", 1, 255)
80
- pattern!(document, "expected_base_head", HEAD)
81
- const!(document, "method", "merge")
82
- validate_review_evidence!(document.fetch("review_evidence"))
83
- validate_test_evidence!(document.fetch("test_evidence"))
84
- true
85
- end
86
-
87
- # The semantic join schema validity alone never waives: the reviewed
88
- # head and every test receipt head must equal the exact candidate.
89
- def require_request_joins!(document)
90
- candidate = document.fetch("candidate_head")
91
- unless document.dig("review_evidence", "reviewed_head") == candidate
92
- raise SchemaError, "review_evidence.reviewed_head does not equal candidate_head"
93
- end
94
- document.fetch("test_evidence").each_with_index do |item, index|
95
- unless item.fetch("candidate_head") == candidate
96
- raise SchemaError, "test_evidence[#{index}].candidate_head does not equal candidate_head"
97
- end
98
- end
99
- true
100
- end
101
-
102
- def validate_review_evidence!(evidence)
103
- require_object!(evidence, "review_evidence")
104
- require_exact_keys!(evidence, "review_evidence", %w[schema reference reviewer verdict
105
- reviewed_head recorded_at record_sha256])
106
- const!(evidence, "schema", "ace.review.acceptance-reference/v1")
107
- string_bound!(evidence, "reference", 1, 256)
108
- string_bound!(evidence, "reviewer", 1, 128)
109
- const!(evidence, "verdict", "approved")
110
- pattern!(evidence, "reviewed_head", HEAD)
111
- date_time!(evidence.fetch("recorded_at"))
112
- pattern!(evidence, "record_sha256", SHA256)
113
- true
114
- end
115
-
116
- def validate_test_evidence!(items)
117
- unless items.is_a?(Array) && !items.empty?
118
- raise SchemaError, "test_evidence must be a non-empty array"
119
- end
120
- items.each_with_index do |item, index|
121
- require_object!(item, "test_evidence[#{index}]")
122
- require_exact_keys!(item, "test_evidence[#{index}]", %w[schema reference candidate_head
123
- command result recorded_at receipt_sha256])
124
- const!(item, "schema", "ace.test.execution-reference/v1")
125
- string_bound!(item, "reference", 1, 256)
126
- pattern!(item, "candidate_head", HEAD)
127
- string_bound!(item, "command", 1, 2048)
128
- const!(item, "result", "passed")
129
- date_time!(item.fetch("recorded_at"))
130
- pattern!(item, "receipt_sha256", SHA256)
131
- end
132
- # uniqueItems: deep equality over the whole closed document
133
- unless items.map { |item| JSON.generate(item) }.uniq.size == items.size
134
- raise SchemaError, "test_evidence items are not unique"
135
- end
136
- true
137
- end
138
-
139
- # The exact terminal receipt document. Structural pass only; binding
140
- # against the canonical admitted record is validate_receipt_against!
141
- def validate_receipt!(document)
142
- require_object!(document, "receipt")
143
- require_exact_keys!(document, "receipt", %w[schema reference request_id input_digest
144
- terminal_status request producer effect remote_observation])
145
- const!(document, "schema", RECEIPT_SCHEMA_ID)
146
- string_bound!(document, "reference", 1, 256)
147
- string_bound!(document, "request_id", 1, 256)
148
- pattern!(document, "input_digest", SHA256)
149
- unless TERMINAL_STATUSES.include?(document.fetch("terminal_status"))
150
- raise SchemaError, "terminal_status must be succeeded or failed"
151
- end
152
- validate_receipt_request!(document.fetch("request"))
153
- validate_receipt_producer!(document.fetch("producer"))
154
- validate_receipt_effect!(document.fetch("effect"))
155
- validate_receipt_observation!(document.fetch("remote_observation"))
156
- require_terminal_invariant!(document)
157
- true
158
- end
159
-
160
- # Embedded, never flattened: all selectors and the complete validated
161
- # input document live under the single request object.
162
- def validate_receipt_request!(request)
163
- require_object!(request, "request")
164
- require_exact_keys!(request, "request", %w[project assignment attempt queue_entry
165
- operation authorization input])
166
- string_bound!(request, "project", 1, 128)
167
- string_bound!(request, "assignment", 1, 128)
168
- string_bound!(request, "attempt", 1, 128)
169
- string_bound!(request, "queue_entry", 1, 128)
170
- const!(request, "operation", "merge")
171
- string_bound!(request, "authorization", 1, 256)
172
- validate_request!(request.fetch("input"))
173
- true
174
- end
175
-
176
- def validate_receipt_producer!(producer)
177
- require_object!(producer, "producer")
178
- require_exact_keys!(producer, "producer", %w[receiver_id principal configuration_sha256])
179
- string_bound!(producer, "receiver_id", 1, 128)
180
- string_bound!(producer, "principal", 1, 256)
181
- pattern!(producer, "configuration_sha256", SHA256)
182
- true
183
- end
184
-
185
- def validate_receipt_effect!(effect)
186
- require_object!(effect, "effect")
187
- require_exact_keys!(effect, "effect", %w[started_at finished_at outcome failure_code no_effect_evidence])
188
- unless effect.fetch("started_at").nil? || effect.fetch("started_at").is_a?(String)
189
- raise SchemaError, "effect.started_at must be a date-time or null"
190
- end
191
- date_time!(effect.fetch("started_at")) unless effect.fetch("started_at").nil?
192
- date_time!(effect.fetch("finished_at"))
193
- unless EFFECT_OUTCOMES.include?(effect.fetch("outcome"))
194
- raise SchemaError, "effect.outcome must be merged, already_merged or failed_no_effect"
195
- end
196
- unless FAILURE_CODES.include?(effect.fetch("failure_code"))
197
- raise SchemaError, "effect.failure_code must be null or merge_failed_no_effect"
198
- end
199
- evidence = effect.fetch("no_effect_evidence")
200
- return true if evidence.nil?
201
- require_object!(evidence, "no_effect_evidence")
202
- require_exact_keys!(evidence, "no_effect_evidence", %w[reference sha256])
203
- string_bound!(evidence, "reference", 1, 256)
204
- pattern!(evidence, "sha256", SHA256)
205
- true
206
- end
207
-
208
- def validate_receipt_observation!(observation)
209
- return true if observation.nil?
210
- require_object!(observation, "remote_observation")
211
- require_exact_keys!(observation, "remote_observation", %w[forge_instance pr_identity_url
212
- target_repository_identity target_ref_observed merged_candidate_head admission_target_head
213
- resulting_target_head merge_commit checkout_id worktree_id branch])
214
- pattern!(observation, "forge_instance", FORGE)
215
- string_bound!(observation, "pr_identity_url", 1, 2048)
216
- uri_format!(observation.fetch("pr_identity_url"))
217
- string_bound!(observation, "target_repository_identity", 1, 1024)
218
- string_bound!(observation, "target_ref_observed", 1, 255)
219
- %w[merged_candidate_head admission_target_head resulting_target_head merge_commit].each do |key|
220
- pattern!(observation, key, HEAD)
221
- end
222
- string_bound!(observation, "checkout_id", 1, 256)
223
- string_bound!(observation, "worktree_id", 1, 256)
224
- string_bound!(observation, "branch", 1, 255)
225
- true
226
- end
227
-
228
- # The closed terminal oneOf: exactly one branch may hold.
229
- def require_terminal_invariant!(document)
230
- succeeded = document.fetch("terminal_status") == "succeeded"
231
- effect = document.fetch("effect")
232
- if succeeded
233
- raise SchemaError, "succeeded receipt needs effect.started_at" if effect.fetch("started_at").nil?
234
- unless %w[merged already_merged].include?(effect.fetch("outcome"))
235
- raise SchemaError, "succeeded receipt outcome must be merged or already_merged"
236
- end
237
- unless effect.fetch("failure_code").nil? && effect.fetch("no_effect_evidence").nil?
238
- raise SchemaError, "succeeded receipt cannot carry a failure code or no-effect evidence"
239
- end
240
- unless document.fetch("remote_observation").is_a?(Hash)
241
- raise SchemaError, "succeeded receipt requires remote_observation"
242
- end
243
- else
244
- unless effect.fetch("outcome") == "failed_no_effect"
245
- raise SchemaError, "failed receipt outcome must be failed_no_effect"
246
- end
247
- unless effect.fetch("failure_code") == "merge_failed_no_effect"
248
- raise SchemaError, "failed receipt requires the immutable failure code"
249
- end
250
- unless effect.fetch("no_effect_evidence").is_a?(Hash)
251
- raise SchemaError, "failed receipt is terminal only with immutable no_effect_evidence"
252
- end
253
- unless document.fetch("remote_observation").nil?
254
- raise SchemaError, "failed receipt cannot observe a remote merge"
255
- end
256
- end
257
- true
258
- end
259
-
260
- # The producer-side binding: every embedded request value and top-level
261
- # ID/digest must match the canonical admitted record, and the remote
262
- # observation must join back to the embedded request and the
263
- # registered execution identity.
264
- def validate_receipt_against!(receipt, record)
265
- validate_receipt!(receipt)
266
- request = receipt.fetch("request")
267
- embedded = request.fetch("input")
268
- unless receipt.fetch("request_id") == record.fetch("request_id") &&
269
- receipt.fetch("input_digest") == record.fetch("input_digest") &&
270
- receipt.fetch("reference") == record.fetch("reference")
271
- raise SchemaError, "receipt identity does not match the canonical admitted record"
272
- end
273
- # Ruling r4: receipt verification RE-DERIVES the digest from the
274
- # embedded request and the record selectors — an echoed digest that
275
- # merely matches a stored value is not sufficient.
276
- embedded_binding = request
277
- rederived = canonical_request_digest(
278
- project: record.fetch("project_id"), assignment: record.fetch("assignment_id"),
279
- attempt: record.fetch("attempt_id"), queue_entry: record.fetch("queue_entry"),
280
- authorization: record.fetch("authorization"), request_id: record.fetch("request_id"),
281
- input: embedded_binding.fetch("input")
282
- )
283
- unless rederived == record.fetch("input_digest") && rederived == receipt.fetch("input_digest")
284
- raise SchemaError, "receipt digest does not re-derive from the embedded request binding"
285
- end
286
- selector_map = {
287
- "project" => "project_id", "assignment" => "assignment_id", "attempt" => "attempt_id",
288
- "queue_entry" => "queue_entry", "operation" => "operation", "authorization" => "authorization"
289
- }
290
- joined = selector_map.all? { |request_key, record_key|
291
- request.fetch(request_key) == record.fetch(record_key)
292
- }
293
- unless joined
294
- raise SchemaError, "embedded request selectors do not match the canonical admitted record"
295
- end
296
- unless embedded == record.fetch("request")
297
- raise SchemaError, "embedded input does not equal the canonical admitted request document"
298
- end
299
- producer = receipt.fetch("producer")
300
- unless producer.fetch("receiver_id") == record.fetch("receiver_id") &&
301
- producer.fetch("principal") == record.fetch("principal") &&
302
- producer.fetch("configuration_sha256") == record.fetch("configuration_sha256")
303
- raise SchemaError, "receipt producer does not match the canonical admitted record"
304
- end
305
- observation = receipt.fetch("remote_observation")
306
- if observation
307
- unless observation.fetch("forge_instance") == embedded.fetch("forge") &&
308
- observation.fetch("pr_identity_url") == embedded.fetch("pr_url") &&
309
- observation.fetch("target_repository_identity") == embedded.fetch("base_repository") &&
310
- observation.fetch("target_ref_observed") == embedded.fetch("base_ref") &&
311
- observation.fetch("merged_candidate_head") == embedded.fetch("candidate_head") &&
312
- observation.fetch("admission_target_head") == embedded.fetch("expected_base_head")
313
- raise SchemaError, "remote_observation does not join back to the embedded request"
314
- end
315
- identity = record.fetch("execution_identity")
316
- unless identity && observation.fetch("checkout_id") == identity.fetch("checkout_id") &&
317
- observation.fetch("worktree_id") == identity.fetch("worktree_id") &&
318
- observation.fetch("branch") == identity.fetch("branch")
319
- raise SchemaError, "remote_observation does not match the registered execution identity"
320
- end
321
- unless observation.fetch("merge_commit") == observation.fetch("resulting_target_head")
322
- raise SchemaError, "merge_commit must equal resulting_target_head for the fixed merge method"
323
- end
324
- end
325
- true
326
- end
327
-
328
- # ---- closed primitive validators ------------------------------------
329
-
330
- def require_object!(value, name)
331
- raise SchemaError, "#{name} must be an object" unless value.is_a?(Hash)
332
- true
333
- end
334
-
335
- def require_exact_keys!(object, name, keys)
336
- unknown = object.keys - keys
337
- unless unknown.empty?
338
- raise SchemaError, "#{name} has unknown fields: #{unknown.sort.join(", ")}"
339
- end
340
- missing = keys - object.keys
341
- unless missing.empty?
342
- raise SchemaError, "#{name} is missing fields: #{missing.sort.join(", ")}"
343
- end
344
- true
345
- end
346
-
347
- def const!(object, key, value)
348
- unless object.fetch(key) == value
349
- raise SchemaError, "#{key} must be #{value.to_s.inspect}"
350
- end
351
- true
352
- end
353
-
354
- def pattern!(object, key, pattern)
355
- value = object.fetch(key)
356
- unless value.is_a?(String) && value.match?(pattern)
357
- raise SchemaError, "#{key} has an invalid format"
358
- end
359
- true
360
- end
361
-
362
- def string_bound!(object, key, min, max, pattern: nil)
363
- value = object.fetch(key)
364
- unless value.is_a?(String) && value.length.between?(min, max)
365
- raise SchemaError, "#{key} must be a string of #{min}..#{max} characters"
366
- end
367
- return true if pattern.nil? || value.match?(pattern)
368
- raise SchemaError, "#{key} has an invalid format"
369
- end
370
-
371
- def date_time!(value)
372
- unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})\z/)
373
- raise SchemaError, "date-time must be an RFC 3339 timestamp"
374
- end
375
- begin
376
- Time.iso8601(value)
377
- rescue ArgumentError
378
- raise SchemaError, "date-time is not a valid timestamp"
379
- end
380
- true
381
- end
382
-
383
- def uri_format!(value)
384
- begin
385
- uri = URI.parse(value)
386
- rescue URI::Error
387
- raise SchemaError, "uri format is invalid"
388
- end
389
- unless uri.is_a?(URI::Generic) && !uri.scheme.to_s.empty? && !uri.host.to_s.empty? &&
390
- value.match?(/\A\p{ASCII}+\z/) && !value.match?(/[\\\s<>"]|\A\//)
391
- raise SchemaError, "uri format is invalid"
392
- end
393
- true
394
- end
395
- end
396
- end
397
- end
398
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
-
5
- module Ace
6
- module Assign
7
- module Atoms
8
- # Git's fixed cat-file batch framing. Owners select process, immutable
9
- # repository, deadline and byte/count limits before invoking this codec.
10
- module GitBlobBatch
11
- OID = /\A[0-9a-f]{40}\z/
12
- class Invalid < StandardError
13
- attr_reader :reason
14
- def initialize(reason)
15
- @reason = reason
16
- super("invalid Git blob batch #{reason}")
17
- end
18
- end
19
-
20
- def self.sizes(oids, output)
21
- unless oids.is_a?(Array) && oids.all? { |oid| oid.is_a?(String) && OID.match?(oid) } && output.is_a?(String)
22
- raise Invalid, :selector
23
- end
24
- lines = output.b.lines
25
- raise Invalid, :count unless lines.size == oids.size
26
- lines.zip(oids).map do |line, oid|
27
- match = /\A([0-9a-f]{40}) blob (0|[1-9][0-9]*)\n\z/.match(line)
28
- raise Invalid, :metadata unless match && match[1] == oid
29
- match[2].to_i
30
- end
31
- end
32
-
33
- def self.decode(entries, output)
34
- unless entries.is_a?(Array) && entries.all? { |entry|
35
- entry.is_a?(Array) && entry.size == 2 && entry[0].is_a?(String) && OID.match?(entry[0]) &&
36
- entry[1].is_a?(Integer) && entry[1] >= 0 } && output.is_a?(String)
37
- raise Invalid, :selector
38
- end
39
- output = output.b
40
- offset = 0
41
- entries.map do |oid, size|
42
- ending = output.index("\n", offset)
43
- unless ending && ending - offset <= 127 && output.byteslice(offset, ending - offset) == "#{oid} blob #{size}"
44
- raise Invalid, :header
45
- end
46
- offset = ending + 1
47
- content = output.byteslice(offset, size)
48
- unless content&.bytesize == size && output.byteslice(offset + size, 1) == "\n" &&
49
- Digest::SHA1.new.update("blob #{size}\0").update(content).hexdigest == oid
50
- raise Invalid, :bytes
51
- end
52
- offset += size + 1
53
- content
54
- end.tap { raise Invalid, :trailing unless offset == output.bytesize }
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "json"
5
- require_relative "campaign_execution"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Shared byte/schema validation; each lifecycle separately authenticates
11
- # the registration's owner, campaign provenance and original selection.
12
- module AssignmentDefinition
13
- def self.parse(bytes:, digest:, assignment_id:, project_id:)
14
- unless bytes.is_a?(String) && bytes.bytesize <= 32_768 && digest.is_a?(String) &&
15
- Digest::SHA256.hexdigest(bytes) == digest
16
- raise ArgumentError, "invalid assignment definition digest or size"
17
- end
18
- value = JSON.parse(bytes)
19
- allowed = %w[session_id name description created_at updated_at source_config parent task_id project_id prepared_work review_campaign campaign_execution]
20
- unless value.is_a?(Hash) && (value.keys - allowed).empty? &&
21
- %w[session_id name created_at source_config task_id project_id].all? { |key| value[key].is_a?(String) && !value[key].empty? } &&
22
- value["session_id"] == assignment_id && value["project_id"] == project_id
23
- raise ArgumentError, "invalid managed assignment definition"
24
- end
25
- CampaignExecution.validate_parent!(value.fetch("review_campaign")) if value.key?("review_campaign")
26
- assignment = Models::Assignment.from_h(value)
27
- unless assignment.managed? && assignment.task_id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/)
28
- raise ArgumentError, "definition is not managed or task identity is invalid"
29
- end
30
- assignment
31
- end
32
- end
33
- end
34
- end
35
- end