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,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