ace-assign 0.71.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +3 -2119
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  35. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  36. data/lib/ace/assign/version.rb +1 -1
  37. data/lib/ace/assign.rb +0 -6
  38. metadata +4 -176
  39. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  40. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  41. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  42. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  43. data/docs/protected-authority.md +0 -282
  44. data/exe/ace-scope-ready +0 -55
  45. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  46. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  47. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  52. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  53. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  54. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  55. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  56. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  57. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  58. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  59. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  60. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  61. data/lib/ace/assign/authority/client.rb +0 -257
  62. data/lib/ace/assign/authority/command_execution.rb +0 -180
  63. data/lib/ace/assign/authority/deployment.rb +0 -627
  64. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  65. data/lib/ace/assign/authority/endcap.rb +0 -457
  66. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  67. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  68. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  69. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  70. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  71. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  72. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  73. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  74. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  75. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  76. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  77. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  78. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  79. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  80. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  81. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  82. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  83. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  84. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  85. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  86. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  87. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  88. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  89. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  90. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  91. data/lib/ace/assign/authority/launch_review.rb +0 -91
  92. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  93. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  94. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  95. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  96. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  97. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  98. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  99. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  100. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  101. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  102. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  103. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  104. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  105. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  106. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  107. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  108. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  109. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  110. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  111. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  112. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  113. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  114. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  115. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  116. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  117. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  118. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  119. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  120. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  121. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  122. data/lib/ace/assign/authority/private_directory.rb +0 -33
  123. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  124. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  125. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  126. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  127. data/lib/ace/assign/authority/retained_input.rb +0 -53
  128. data/lib/ace/assign/authority/router.rb +0 -81
  129. data/lib/ace/assign/authority/server.rb +0 -131
  130. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  131. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  132. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  133. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  134. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  135. data/lib/ace/assign/authority/socket_server.rb +0 -255
  136. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  137. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  138. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  139. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  140. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  141. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  142. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  143. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  144. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  145. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  146. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  147. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  148. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  149. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  151. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  152. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  153. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  154. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  155. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  156. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  157. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  158. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  159. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  160. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  161. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  162. data/lib/ace/assign/models/attempt.rb +0 -178
  163. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  164. data/lib/ace/assign/models/evidence_event.rb +0 -86
  165. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  166. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  167. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  168. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  169. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  170. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  171. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  172. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  173. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  174. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  175. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  176. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  177. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  178. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  179. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  180. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  181. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  182. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  183. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  184. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  185. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  186. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  187. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  188. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  189. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  190. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  191. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  192. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  193. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  194. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  195. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,309 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../atoms/candidate_limits"
4
-
5
- require "json"
6
- require "digest"
7
- require "yaml"
8
- require "date"
9
- require_relative "../atoms/step_file_parser"
10
- require_relative "candidate_transfer"
11
-
12
- module Ace
13
- module Assign
14
- module Authority
15
- # Closed prepared-input tree. Git transport and canonical publication are
16
- # owned by CandidateTransfer/LaunchLifecycle; this object owns their shared
17
- # byte contract, never a registration, attempt or progress ledger.
18
- class PreparedWork
19
- MAX_TEXT = 1024 * 1024
20
- MAX_TOTAL = Atoms::CandidateLimits::PREPARED_BYTES
21
- MAX_MANIFEST = 32_768
22
- TOKEN = /\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}\z/
23
- SCOPE = /\A[0-9]+(?:\.[0-9]+)*\z/
24
- SHA = /\A[0-9a-f]{64}\z/
25
- TOP = %w[version assignment_id project_id task_id scope job steps context].freeze
26
- RECORD = %w[bytes filename sha256].freeze
27
- PROGRESS = %w[status started_at completed_at error stall_reason fork_launch_pid fork_tracked_pids fork_pid_updated_at fork_pid_file].freeze
28
- attr_reader :manifest, :files, :definition, :selection_sha256, :manifest_sha256
29
-
30
- def initialize(files:)
31
- invalid!("file inventory") unless files.is_a?(Hash) && files.size.between?(4, 4096)
32
- @files = files.to_h do |path, bytes|
33
- invalid!("file path") unless path.is_a?(String) && !path.start_with?("/") &&
34
- path.bytesize <= Atoms::CandidateLimits::PATH_BYTES &&
35
- path.split("/", -1).all? { |part| !part.empty? && part.bytesize <= Atoms::CandidateLimits::PATH_COMPONENT_BYTES &&
36
- !%w[. .. .git].include?(part.downcase) }
37
- invalid!("text size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
38
- text = bytes.dup.force_encoding(Encoding::UTF_8)
39
- invalid!("UTF-8") unless text.valid_encoding? && !text.include?("\0")
40
- [path.dup.freeze, text.freeze]
41
- end.freeze
42
- invalid!("expanded size") if @files.values.sum(&:bytesize) > MAX_TOTAL
43
- manifest_bytes = @files.fetch("manifest.json")
44
- invalid!("manifest size") if manifest_bytes.bytesize > MAX_MANIFEST
45
- @manifest = parse_json(manifest_bytes)
46
- closed!(@manifest, TOP)
47
- invalid!("version") unless @manifest["version"].is_a?(Integer) && @manifest["version"] == 1
48
- %w[assignment_id project_id task_id].each { |key| token!(@manifest[key]) }
49
- scope!(@manifest["scope"])
50
- canonical = self.class.canonical_manifest(@manifest)
51
- invalid!("manifest serialization") unless manifest_bytes == canonical + "\n"
52
- @selection_sha256 = Digest::SHA256.hexdigest(canonical).freeze
53
- @manifest_sha256 = Digest::SHA256.hexdigest(manifest_bytes).freeze
54
- @used = %w[manifest.json definition.json]
55
- record!(@manifest["job"], "job.yaml")
56
- @job = yaml!(@files.fetch("job.yaml"))
57
- steps!
58
- context!
59
- task_closure!
60
- selected_root!
61
- invalid!("undeclared file") unless @used.sort == @files.keys.sort
62
- definition_bytes = @files.fetch("definition.json")
63
- invalid!("definition size") if definition_bytes.bytesize > MAX_MANIFEST
64
- @definition = parse_json(definition_bytes)
65
- invalid!("base definition") unless @definition.is_a?(Hash) && !@definition.key?("prepared_work") &&
66
- @definition["session_id"] == @manifest["assignment_id"] && @definition["project_id"] == @manifest["project_id"] && @definition["task_id"] == @manifest["task_id"]
67
- deep_freeze(@definition)
68
- deep_freeze(@manifest)
69
- freeze
70
- rescue KeyError, JSON::ParserError, Psych::Exception => error
71
- invalid!(error.message)
72
- end
73
-
74
- # The existing transport verifies complete Git objects before this
75
- # shared byte contract parses anything. Preserve the original input for
76
- # the eventual registration journal; CandidateTransfer may reexport it.
77
- def self.admit(bytes:, head:, tree:, sha256:, size:, root:, deadline: nil)
78
- transfer = CandidateTransfer.new(root: root)
79
- transfer.admit(bytes: bytes, head: head, sha256: sha256, size: size, deadline: deadline) do |repository, deadline, result|
80
- raise ArgumentError, "prepared_input_invalid: Git tree differs" unless result.fetch("tree") == tree
81
- new(files: transfer.prepared_files(repository: repository, deadline: deadline, head: head))
82
- end
83
- end
84
-
85
- def reference(head:, tree:)
86
- invalid!("Git identity") unless [head, tree].all? { |id| id.is_a?(String) && id.match?(/\A[0-9a-f]{40}\z/) }
87
- {"version" => 1, "task_id" => manifest.fetch("task_id"), "scope" => manifest.fetch("scope"),
88
- "prepared_head" => head, "prepared_tree" => tree, "manifest_bytes" => files.fetch("manifest.json").bytesize,
89
- "manifest_sha256" => manifest_sha256, "selection_sha256" => selection_sha256}.freeze
90
- end
91
-
92
- def definition_bytes(head:, tree:)
93
- bytes = JSON.generate(definition.merge("prepared_work" => reference(head: head, tree: tree)))
94
- invalid!("final definition size") if bytes.bytesize > MAX_MANIFEST
95
- bytes.freeze
96
- end
97
-
98
- # Mutable queue progress still uses the accepted YAML grammar/bounds.
99
- # Return the same parsed bytes that the queue decision will consume.
100
- def parse_queue_step!(bytes)
101
- invalid!("queue step size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
102
- match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(bytes)
103
- invalid!("queue step frontmatter") unless match
104
- frontmatter = yaml!(match[1])
105
- invalid!("queue step frontmatter") unless frontmatter.is_a?(Hash)
106
- {frontmatter: frontmatter, body: bytes[match.end(0)..].strip}
107
- rescue Psych::Exception => error
108
- invalid!(error.message)
109
- end
110
-
111
- def parse_queue_metadata!(bytes)
112
- invalid!("queue metadata size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_MANIFEST)
113
- yaml!(bytes)
114
- rescue Psych::Exception => error
115
- invalid!(error.message)
116
- end
117
-
118
- # StepFileParser owns body/frontmatter semantics; only these progress
119
- # fields differ from accepted work. Reports are separate mutable output.
120
- def self.work_projection(bytes)
121
- parsed = Atoms::StepFileParser.parse(bytes)
122
- {"frontmatter" => parsed.fetch(:frontmatter).reject { |key, _| PROGRESS.include?(key) }, "body" => parsed.fetch(:body)}
123
- end
124
-
125
- def self.canonical_manifest(value)
126
- sorted = lambda do |item|
127
- case item
128
- when Hash then item.keys.sort.to_h { |key| [key, sorted.call(item.fetch(key))] }
129
- when Array then item.map { |entry| sorted.call(entry) }
130
- else item
131
- end
132
- end
133
- JSON.generate(TOP.to_h { |key| [key, sorted.call(value.fetch(key))] })
134
- end
135
-
136
- private
137
-
138
- def steps!
139
- steps = @manifest["steps"]
140
- invalid!("steps") unless steps.is_a?(Array) && steps.size.between?(1, 256)
141
- @step_frontmatter = {}
142
- numbers = steps.map do |step|
143
- closed!(step, %w[number filename bytes sha256])
144
- number = step.fetch("number"); scope!(number)
145
- invalid!("step scope") unless number == @manifest["scope"] || number.start_with?(@manifest["scope"] + ".")
146
- filename = step.fetch("filename")
147
- record!(step.reject { |key, _| key == "number" }, "steps/" + filename)
148
- # Step record filenames are basenames; inventory paths add steps/.
149
- content = @files.fetch("steps/" + filename)
150
- match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(content)
151
- invalid!("step frontmatter") unless match
152
- fm = yaml!(match[1])
153
- invalid!("initial status") unless fm.is_a?(Hash) && fm.fetch("status", "pending") == "pending"
154
- @step_frontmatter[number] = fm
155
- invalid!("step name") unless fm["name"].is_a?(String) && !fm["name"].empty?
156
- invalid!("step filename") unless filename == Atoms::StepFileParser.generate_filename(number, fm["name"])
157
- number
158
- end
159
- invalid!("step ordering/root") unless numbers.uniq == numbers && numbers == numbers.sort_by { |n| n.split(".").map(&:to_i) } && numbers.first == @manifest["scope"]
160
- end
161
-
162
- def context!
163
- context = @manifest["context"]
164
- invalid!("context") unless context.is_a?(Array) && context.size.between?(1, 256)
165
- uris = context.map do |entry|
166
- closed!(entry, %w[uri task_id spec text reports]); token!(entry["task_id"])
167
- uri = "task://" + entry.fetch("task_id")
168
- invalid!("context URI") unless entry["uri"] == uri
169
- prefix = "context/" + entry.fetch("task_id")
170
- record!(entry["spec"], prefix + "/spec.md")
171
- record!(entry["text"], prefix + "/bundle.txt")
172
- reports = entry["reports"]
173
- invalid!("reports") unless reports.is_a?(Array)
174
- identities = reports.map do |report|
175
- closed!(report, %w[assignment_id number file]); token!(report["assignment_id"]); scope!(report["number"])
176
- record!(report["file"], prefix + "/reports/" + report.fetch("assignment_id") + "/" + report.fetch("number") + ".r.md")
177
- [report["assignment_id"], report["number"].split(".").map(&:to_i)]
178
- end
179
- invalid!("report ordering") unless identities.uniq == identities && identities == identities.sort
180
- uri
181
- end
182
- invalid!("context ordering/selected task") unless uris.uniq == uris && uris == uris.sort && uris.include?("task://" + @manifest["task_id"])
183
- end
184
-
185
- def selected_root!
186
- scope = @manifest.fetch("scope"); task = @manifest.fetch("task_id")
187
- root = @step_frontmatter.fetch(scope)
188
- invalid!("selected fork root") unless root["context"] == "fork" && root["taskref"] == task
189
- @step_frontmatter.each do |number, fm|
190
- invalid!("selected taskref") if fm.key?("taskref") && fm["taskref"] != task
191
- next if number == scope
192
- parent = number.split(".")[0...-1].join(".")
193
- invalid!("selected parent association") unless fm["parent"] == parent && @step_frontmatter.key?(parent)
194
- end
195
- invalid!("job steps") unless @job.is_a?(Hash) && @job["steps"].is_a?(Array)
196
- roots = @job.fetch("steps").select { |step| step.is_a?(Hash) && step["context"] == "fork" && step["taskref"] == task }
197
- invalid!("job selected root") unless roots.one? && roots.first.values_at("number", "taskref") == [scope, task]
198
- end
199
-
200
- def task_closure!
201
- specs = @manifest.fetch("context").to_h do |entry|
202
- content = @files.fetch(entry.fetch("spec").fetch("filename"))
203
- match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(content)
204
- invalid!("task frontmatter") unless match
205
- task = yaml!(match[1])
206
- invalid!("task association") unless task.is_a?(Hash) && task["id"] == entry["task_id"] && task.fetch("needs_review", false) == false
207
- dependencies = task.fetch("dependencies", [])
208
- invalid!("task dependencies") unless dependencies.is_a?(Array) && dependencies.uniq == dependencies
209
- dependencies.each { |dependency| token!(dependency) }
210
- [entry.fetch("task_id"), task]
211
- end
212
- selected = specs.fetch(@manifest.fetch("task_id"))
213
- invalid!("selected reviewed status") unless %w[pending in-progress].include?(selected["status"])
214
- visiting = []; visited = []
215
- walk = lambda do |id|
216
- invalid!("dependency cycle: " + (visiting + [id]).join(" -> ")) if visiting.include?(id)
217
- return if visited.include?(id)
218
- visiting << id
219
- specs.fetch(id).fetch("dependencies", []).each { |dependency| walk.call(dependency) }
220
- visiting.pop; visited << id
221
- end
222
- walk.call(@manifest.fetch("task_id"))
223
- invalid!("undeclared context") unless visited.sort == specs.keys.sort
224
- end
225
-
226
- def record!(record, path)
227
- closed!(record, RECORD)
228
- # Selected step record uses an exact basename; other records use the
229
- # exact canonical relative artifact path.
230
- expected = path.start_with?("steps/") ? path.delete_prefix("steps/") : path
231
- invalid!("content reference") unless record["filename"] == expected && record["bytes"].is_a?(Integer) && record["bytes"].positive? && record["sha256"].is_a?(String) && record["sha256"].match?(SHA)
232
- bytes = @files.fetch(path)
233
- invalid!("content digest") unless record["bytes"] == bytes.bytesize && record["sha256"] == Digest::SHA256.hexdigest(bytes)
234
- invalid!("duplicate file") if @used.include?(path)
235
- @used << path
236
- end
237
-
238
- def yaml!(bytes)
239
- ast = Psych.parse_stream(bytes)
240
- invalid!("YAML requires one document") unless ast.children.size == 1
241
- scanner = Psych::ScalarScanner.new(Psych::ClassLoader::Restricted.new([], []))
242
- pending = [[ast, 0]]
243
- until pending.empty?
244
- node, depth = pending.pop
245
- invalid!("YAML nesting exceeds 32") if depth > 32
246
- invalid!("YAML alias") if node.is_a?(Psych::Nodes::Alias)
247
- if node.is_a?(Psych::Nodes::Mapping)
248
- keys = node.children.each_slice(2).map(&:first)
249
- decoded = keys.map do |key|
250
- invalid!("YAML mapping keys") unless key.is_a?(Psych::Nodes::Scalar) && [nil, "tag:yaml.org,2002:str"].include?(key.tag)
251
- value = key.plain && key.tag.nil? ? scanner.tokenize(key.value) : key.value
252
- invalid!("YAML mapping keys must be strings") unless value.is_a?(String)
253
- value
254
- end
255
- invalid!("duplicate YAML key") unless decoded.uniq.size == decoded.size
256
- end
257
- (node.children || []).each { |child| pending << [child, depth + 1] }
258
- end
259
- value = YAML.safe_load(bytes, permitted_classes: [Time, Date], aliases: false)
260
- values = [value]
261
- until values.empty?
262
- item = values.pop
263
- case item
264
- when Hash
265
- invalid!("YAML mapping keys must be strings") unless item.keys.all? { |key| key.is_a?(String) }
266
- values.concat(item.values)
267
- when Array then values.concat(item)
268
- when String
269
- invalid!("YAML string encoding") unless item.valid_encoding? && !item.include?("\0")
270
- when Float then invalid!("YAML nonfinite number") unless item.finite?
271
- when Integer, TrueClass, FalseClass, NilClass, Time, Date
272
- # Managed files retain StepFileParser's standard timestamp types.
273
- else invalid!("unsupported YAML value")
274
- end
275
- end
276
- value
277
- end
278
-
279
- def parse_json(bytes)
280
- JSON.parse(bytes, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
281
- end
282
-
283
- def closed!(value, keys)
284
- invalid!("closed fields") unless value.is_a?(Hash) && value.keys.sort == keys.sort
285
- end
286
-
287
- def token!(value)
288
- invalid!("identity") unless value.is_a?(String) && value.match?(TOKEN)
289
- end
290
-
291
- def scope!(value)
292
- invalid!("scope") unless value.is_a?(String) && value.match?(SCOPE)
293
- end
294
-
295
- def deep_freeze(value)
296
- case value
297
- when Hash then value.each { |key, item| deep_freeze(key); deep_freeze(item) }
298
- when Array then value.each { |item| deep_freeze(item) }
299
- end
300
- value.freeze
301
- end
302
-
303
- def invalid!(reason)
304
- raise ArgumentError, "prepared_input_invalid: #{reason}"
305
- end
306
- end
307
- end
308
- end
309
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "prepared_input"
4
- require_relative "prepared_queue"
5
- require_relative "../molecules/fork_session_launcher"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Fixed gate target. Environment identifiers are untrusted lookup hints;
11
- # the original issued capability and actual kernel birth decide admission.
12
- class PreparedWorker
13
- def initialize(kernel: Ace::Runtime::Molecules::ProtectedLinux.new, client_factory: nil, launcher: nil, env: ENV,
14
- workspace_reader_factory: nil)
15
- @kernel, @launcher, @env = kernel, launcher, env
16
- @client_factory = client_factory || ->(mapping) { Client.new(mapping_id: mapping, kernel: @kernel) }
17
- @workspace_reader_factory = workspace_reader_factory || ->(projection) {
18
- Molecules::LifecycleExclusion.workspace_reader(projection: projection)
19
- }
20
- @started = false
21
- end
22
-
23
- def run
24
- raise AttemptErrors::EvidenceUnavailable, "original adapter invocation already activated" if @started
25
- mapping, assignment, attempt = %w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ASSIGNMENT_ID ACE_ASSIGN_ATTEMPT_ID].map do |key|
26
- value = @env[key]
27
- unless value.is_a?(String) && value.match?(PreparedWork::TOKEN)
28
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original worker selector"
29
- end
30
- value
31
- end
32
- input = PreparedInput.fetch(client: @client_factory.call(mapping), assignment_id: assignment, attempt_id: attempt)
33
- self_identity = @kernel.capture(Process.pid)
34
- @kernel.live!(self_identity)
35
- original = input.descriptor.fetch("original_worker_identity")
36
- unless @kernel.same?(self_identity, original)
37
- raise AttemptErrors::UnauthorizedIdentity, "prepared adapter requires exact original worker self"
38
- end
39
- input.drive_prompt
40
- @workspace_lease = @workspace_reader_factory.call(input.descriptor.fetch("workspace_exclusion"))
41
- @workspace_lease.acquire!
42
- @started = true
43
- queue = PreparedQueue.new(work: input.work, descriptor: input.descriptor)
44
- root = queue.activate_original!
45
- unless root
46
- @workspace_lease.verify_unchanged!
47
- @workspace_lease.close!
48
- return {"state" => "completed"}
49
- end
50
- launcher = @launcher || Molecules::ForkSessionLauncher.new(config: {})
51
- result = launcher.launch_provider_session(assignment_id: assignment, fork_root: input.descriptor.fetch("scope"),
52
- provider: root.fork_provider || Molecules::ForkSessionLauncher::DEFAULT_PROVIDER, cache_dir: queue.directory,
53
- prepared_input: input)
54
- queue.with_executor do |executor|
55
- state = executor.status.fetch(:state)
56
- unless state.failed.empty? && state.subtree_complete?(input.descriptor.fetch("scope"))
57
- raise AttemptErrors::EvidenceUnavailable, "prepared provider exited before selected queue completed"
58
- end
59
- end
60
- @workspace_lease.verify_unchanged!
61
- @workspace_lease.close!
62
- result
63
- rescue SystemCallError, IOError
64
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: published worker queue or provider output is unavailable"
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- module PrivateDirectory
7
- module_function
8
-
9
- # Source configuration chooses this path. Each ancestor must prevent
10
- # another principal from replacing the private leaf or its contents.
11
- def verify!(root)
12
- path = File.expand_path(root)
13
- cursor = path
14
- loop do
15
- stat = File.lstat(cursor)
16
- unless stat.directory? && !stat.symlink? && [0, Process.uid].include?(stat.uid) && (stat.mode & 0022).zero?
17
- raise AttemptErrors::ReceiptRejected, "Private directory ancestry is not protected"
18
- end
19
- break if cursor == "/"
20
- cursor = File.dirname(cursor)
21
- end
22
- stat = File.stat(path)
23
- unless stat.uid == Process.uid && (stat.mode & 0077).zero?
24
- raise AttemptErrors::ReceiptRejected, "Private directory must be owned by this peer and private"
25
- end
26
- path
27
- rescue SystemCallError, TypeError
28
- raise AttemptErrors::ReceiptRejected, "Private directory is unavailable"
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,226 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "deployment_history"
4
- require_relative "prepared_input"
5
- require_relative "prepared_queue"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Installed and retained deployment owners determine protected principals.
11
- # A removed original mapping never silently restores ordinary local mode.
12
- class ProtectedAssignmentContext
13
- attr_reader :uid
14
-
15
- def self.load(uid: Process.uid)
16
- paths = [Deployment::PATH, DeploymentHistory::PATH]
17
- installed = paths.any? do |path|
18
- begin
19
- File.lstat(path)
20
- true
21
- rescue Errno::ENOENT
22
- false
23
- end
24
- end
25
- return new(deployment: nil, history: nil, uid: uid) unless installed
26
- deployment = Deployment.load
27
- history = DeploymentHistory.load
28
- unless history.selects?(deployment)
29
- raise AttemptErrors::EvidenceUnavailable, "installed protected descriptor is not retained"
30
- end
31
- new(deployment: deployment, history: history, uid: uid)
32
- end
33
-
34
- def initialize(deployment:, history:, uid: Process.uid, kernel: nil, env: ENV,
35
- artifacts_factory: -> { Ace::Runtime::Molecules::ProtectedArtifactSet.new })
36
- @deployment, @history, @uid, @kernel, @env = deployment, history, uid, kernel, env
37
- @artifacts_factory = artifacts_factory
38
- end
39
-
40
- # A declared installed account never falls through to local authority.
41
- # This is classification only; each wire operation still authenticates
42
- # the current kernel peer and its exact permitted role.
43
- def protected_participant?
44
- return false unless @deployment
45
- raise AttemptErrors::EvidenceUnavailable, "protected retained owners are unavailable" unless @history
46
- [@deployment, *@history.descriptors].any? do |owner|
47
- data = owner.data
48
- principals = data.fetch("authorities").values.map { |authority| authority.fetch("uid") }
49
- principals.concat(data.fetch("launch_mappings").values.flat_map { |map| map.values_at("launcher_uid", "worker_uid") })
50
- data.fetch("projects").each_value do |project|
51
- %w[launcher_uids reviewer_uids worker_uids service_executor_uids supervisor_uids].each do |key|
52
- principals.concat(project.fetch(key, []))
53
- end
54
- principals.concat(project.fetch("service_receivers", {}).values.map { |receiver| receiver.fetch("executor_uid") })
55
- principals.concat(project.fetch("inbox_contexts", {}).values.map { |context| context.fetch("owner_credentials").fetch("uid") })
56
- end
57
- principals.include?(@uid)
58
- end
59
- rescue KeyError, TypeError, NoMethodError
60
- raise AttemptErrors::EvidenceUnavailable, "protected installed principal inventory is malformed"
61
- end
62
-
63
- # Read-only current selection; no role grant or private Inbox access.
64
- def inbox_context_selection(project:, mapping:, inbox_context:)
65
- unless installed? && [project, mapping, inbox_context].all? { |id| id.is_a?(String) && Deployment::TOKEN.match?(id) }
66
- raise AttemptErrors::EvidenceUnavailable, "installed inbox selectors differ"
67
- end
68
- descriptor = @deployment.artifact_reference
69
- history = @history.artifact_reference
70
- unless descriptor && history && @history.selects?(@deployment)
71
- raise AttemptErrors::EvidenceUnavailable, "installed inbox references are unavailable"
72
- end
73
- @artifacts_factory.call.with do |held|
74
- held.read!(descriptor)
75
- held.read!(history)
76
- map = @deployment.mapping(mapping)
77
- unless map.fetch("project_id") == project
78
- raise AttemptErrors::EvidenceUnavailable, "installed inbox project differs"
79
- end
80
- selected = @deployment.inbox_context(mapping, inbox_context)
81
- authority = @deployment.authority(map.fetch("authority_id"))
82
- result = {"uid" => @uid, "project_id" => project, "mapping_id" => mapping,
83
- "inbox_context_id" => inbox_context, "descriptor" => descriptor, "history" => history,
84
- "context" => selected.slice("control_socket_path", "owner_credentials", "native_mapping_id"),
85
- "authority" => authority.slice("socket_path", "uid", "gid", "groups")
86
- .merge("authority_id" => map.fetch("authority_id"))}
87
- held.verify_unchanged!
88
- value = yield result
89
- held.verify_unchanged!
90
- value
91
- end
92
- rescue ArgumentError, KeyError, TypeError, Ace::Runtime::Error
93
- raise AttemptErrors::EvidenceUnavailable, "installed inbox selection is unavailable"
94
- end
95
-
96
- def protected_worker?
97
- return false unless @deployment
98
- [@deployment, *@history.descriptors].any? do |owner|
99
- owner.data.fetch("projects").values.any? { |project| project.fetch("worker_uids").include?(@uid) }
100
- end
101
- end
102
-
103
- def installed?
104
- !@deployment.nil? && !@history.nil?
105
- end
106
-
107
- def refuse_graph_mutation!(options:)
108
- selected = options.values_at(:mapping, :attempt).any? { |value| value && !value.to_s.empty? } ||
109
- %w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ATTEMPT_ID].any? { |key| !@env[key].to_s.empty? }
110
- if protected_worker? || selected
111
- raise AttemptErrors::EvidenceUnavailable, "protected queue graph changes require a reviewed new prepared version and attempt"
112
- end
113
- end
114
-
115
- def resolve(options:, assignment_id:, scope:)
116
- selected = options.values_at(:mapping, :attempt).any? { |value| value && !value.to_s.empty? } ||
117
- %w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ATTEMPT_ID].any? { |key| !@env[key].to_s.empty? }
118
- return nil unless protected_worker? || selected
119
- unless @deployment && assignment_id && scope && scope.match?(PreparedWork::SCOPE)
120
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: explicit scoped assignment required"
121
- end
122
- attempt = selected_hint(options[:attempt], "ACE_ASSIGN_ATTEMPT_ID")
123
- hint = @env["ACE_ASSIGN_ASSIGNMENT_ID"].to_s
124
- unless hint.empty? || hint == assignment_id
125
- raise AttemptErrors::EvidenceUnavailable, "prepared assignment hint differs"
126
- end
127
- @kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
128
- input = PreparedInput.fetch(client: client(options: options),
129
- assignment_id: assignment_id, attempt_id: attempt)
130
- unless input.descriptor.fetch("scope") == scope
131
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_mismatch: scoped CLI selection"
132
- end
133
- input
134
- rescue KeyError
135
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original installed mapping is unavailable"
136
- end
137
-
138
- def verify_attempt_hints!(assignment_id:, attempt_id:)
139
- selected_hint(assignment_id, "ACE_ASSIGN_ASSIGNMENT_ID")
140
- selected_hint(attempt_id, "ACE_ASSIGN_ATTEMPT_ID")
141
- true
142
- end
143
-
144
- def client(options:)
145
- raise AttemptErrors::EvidenceUnavailable, "protected installed authority is unavailable" unless @deployment
146
- mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
147
- @kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
148
- Client.new(mapping_id: mapping, deployment: @deployment, kernel: @kernel)
149
- end
150
-
151
- def mapping_hint?
152
- !@env["ACE_ASSIGN_LAUNCH_MAPPING"].to_s.empty?
153
- end
154
-
155
- # Observer/signer commands reuse the installed selection and held
156
- # descriptor pair. This grants no effect; endpoints authorize the peer.
157
- def with_inbox_workflow(options:)
158
- mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
159
- project, inbox_context = options.values_at(:project, :inbox_context)
160
- inbox_context_selection(project: project, mapping: mapping, inbox_context: inbox_context) do |_selection|
161
- @kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
162
- yield @deployment, @kernel, @deployment.mapping(mapping)
163
- end
164
- end
165
-
166
- # Consumers call only after original PreparedInput admission. These
167
- # existing owners select transport; they do not confer a new grant.
168
- def with_installed_selection(options:, input:)
169
- raise AttemptErrors::EvidenceUnavailable, "protected installed authority is unavailable" unless @deployment
170
- mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
171
- begin
172
- map = @deployment.mapping(mapping)
173
- unless input && input.descriptor.fetch("mapping_id") == mapping &&
174
- input.descriptor.fetch("project_id") == map.fetch("project_id")
175
- raise AttemptErrors::EvidenceUnavailable, "original prepared installed selection differs"
176
- end
177
- rescue KeyError
178
- raise AttemptErrors::EvidenceUnavailable, "original installed mapping is unavailable"
179
- end
180
- @kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
181
- yield @deployment, @kernel, mapping
182
- end
183
-
184
- private
185
-
186
- def selected_hint(explicit, key)
187
- hint = @env[key].to_s
188
- value = explicit || hint
189
- unless value.is_a?(String) && value.match?(PreparedWork::TOKEN) && (hint.empty? || hint == value)
190
- raise AttemptErrors::EvidenceUnavailable, "prepared selector hint differs"
191
- end
192
- value
193
- end
194
- end
195
-
196
- # Each supported operation holds the existing private queue exclusion and
197
- # consumes its authenticated held snapshot. No graph-changing API exists.
198
- class PreparedExecutor
199
- def initialize(input)
200
- @queue = PreparedQueue.new(work: input.work, descriptor: input.descriptor)
201
- @scope = input.descriptor.fetch("scope")
202
- end
203
-
204
- def status
205
- @queue.with_executor(&:status)
206
- end
207
-
208
- %i[start_step finish_step fail].each do |operation|
209
- define_method(operation) do |*args, **options|
210
- unless options[:fork_root] == @scope && (!options[:step_number] ||
211
- options[:step_number] == @scope || options[:step_number].start_with?(@scope + "."))
212
- raise AttemptErrors::EvidenceUnavailable, "prepared operation scope differs"
213
- end
214
- @queue.with_executor { |executor| executor.public_send(operation, *args, **options) }
215
- end
216
- end
217
-
218
- def method_missing(*)
219
- raise AttemptErrors::EvidenceUnavailable, "prepared queue graph changes require a reviewed new prepared version and attempt"
220
- end
221
-
222
- def respond_to_missing?(*); false; end
223
- end
224
- end
225
- end
226
- end