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,59 +0,0 @@
1
- # frozen_string_literal: true
2
- require "fiddle"
3
-
4
- module Ace
5
- module Assign
6
- module Authority
7
- # Linux POSIX access ACL evaluation for a fixed unprivileged principal.
8
- # No credential switching; unsupported or unreadable policy refuses admission.
9
- class PosixAcl
10
- def entries(path, attribute: "system.posix_acl_access")
11
- unless %w[system.posix_acl_access system.posix_acl_default].include?(attribute)
12
- raise ArgumentError, "unsupported POSIX ACL attribute"
13
- end
14
- raise Ace::Runtime::RuntimeUnavailableError, "receiver ACL inspection requires Linux" unless RUBY_PLATFORM.include?("linux")
15
- function = Fiddle::Function.new(Fiddle::Handle::DEFAULT["lgetxattr"],
16
- [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T], Fiddle::TYPE_SSIZE_T)
17
- buffer = Fiddle::Pointer.malloc(8192, Fiddle::RUBY_FREE)
18
- count = function.call(path, attribute, buffer, 8192)
19
- return nil if count == -1 && Fiddle.last_error == Errno::ENODATA::Errno
20
- raise Ace::Runtime::RuntimeUnavailableError, "receiver access ACL is unreadable" if count < 4 || (count - 4) % 8 != 0
21
- bytes = buffer[0, count]
22
- raise Ace::Runtime::RuntimeUnavailableError, "unknown receiver ACL version" unless bytes.unpack1("L<") == 2
23
- rows = bytes.byteslice(4..).unpack("S<S<L<" * ((count - 4) / 8)).each_slice(3).to_a
24
- validate!(rows)
25
- rows
26
- rescue Fiddle::DLError
27
- raise Ace::Runtime::RuntimeUnavailableError, "receiver ACL inspection is unavailable"
28
- end
29
-
30
- def searchable?(path, stat:, uid:, groups:)
31
- rows = entries(path)
32
- unless rows
33
- shift = uid == stat.uid ? 6 : (groups.include?(stat.gid) ? 3 : 0)
34
- return ((stat.mode >> shift) & 1) == 1
35
- end
36
- return (rows.find { |tag, _perm, _id| tag == 1 }[1] & 1) == 1 if uid == stat.uid
37
- mask = rows.find { |tag, _perm, _id| tag == 16 }&.at(1) || 7
38
- named = rows.find { |tag, _perm, id| tag == 2 && id == uid }
39
- return (named[1] & mask & 1) == 1 if named
40
- matched = rows.select { |tag, _perm, id| (tag == 4 && groups.include?(stat.gid)) || (tag == 8 && groups.include?(id)) }
41
- return matched.any? { |_tag, perm, _id| (perm & mask & 1) == 1 } unless matched.empty?
42
- (rows.find { |tag, _perm, _id| tag == 32 }[1] & 1) == 1
43
- end
44
-
45
- private
46
-
47
- def validate!(rows)
48
- valid = rows.all? { |tag, perm, id| [1, 2, 4, 8, 16, 32].include?(tag) && (0..7).cover?(perm) &&
49
- ([2, 8].include?(tag) ? id != 0xffffffff : id == 0xffffffff) }
50
- valid &&= [1, 4, 32].all? { |tag| rows.count { |row| row[0] == tag } == 1 }
51
- valid &&= rows.count { |row| row[0] == 16 } <= 1
52
- valid &&= [2, 8].all? { |tag| ids = rows.select { |row| row[0] == tag }.map(&:last); ids.uniq == ids }
53
- valid &&= !rows.any? { |row| [2, 8].include?(row[0]) } || rows.count { |row| row[0] == 16 } == 1
54
- raise Ace::Runtime::RuntimeUnavailableError, "invalid receiver access ACL" unless valid
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shellwords"
4
- require_relative "client"
5
- require_relative "prepared_work"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # One authenticated original fetch and one immutable verified work graph.
11
- # Caller identifiers select; the maintained Client/Endcap authenticates.
12
- class PreparedInput
13
- attr_reader :descriptor, :work, :working_directory
14
-
15
- def self.fetch(client:, assignment_id:, attempt_id:)
16
- reply = client.call("evidence_fetch", {"assignment_id" => assignment_id, "attempt_id" => attempt_id,
17
- "kind" => "prepared_work", "purpose_id" => "original_prepared_work", "artifact_id" => "prepared_bundle"},
18
- download: true, purpose: :candidate, timeout: 30)
19
- new(reply: reply, candidate_loader: -> {
20
- client.call("evidence_fetch", {"assignment_id" => assignment_id, "attempt_id" => attempt_id,
21
- "kind" => "campaign_candidate", "purpose_id" => "original_campaign_candidate", "artifact_id" => "candidate_bundle"},
22
- download: true, purpose: :candidate, timeout: 30)
23
- })
24
- end
25
-
26
- def initialize(reply:, candidate_reply: nil, candidate_loader: nil)
27
- @descriptor = reply.data.fetch("descriptor")
28
- Molecules::LifecycleExclusion.workspace_reader(projection: descriptor.fetch("workspace_exclusion"))
29
- unless descriptor.dig("workspace_exclusion", "root_resource", "view_path") ==
30
- "/run/ace/lifecycle-exclusion/#{descriptor.fetch('mapping_id')}"
31
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original lifecycle mapping differs"
32
- end
33
- unless reply.parts.is_a?(Array) && reply.parts.one?
34
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: one original bundle required"
35
- end
36
- root = PrivateDirectory.verify!(descriptor.fetch("original_worker_scratch_root"))
37
- @work = PreparedWork.admit(bytes: reply.parts.first, head: descriptor.fetch("prepared_head"),
38
- tree: descriptor.fetch("prepared_tree"), sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), root: root)
39
- expected = work.reference(head: descriptor.fetch("prepared_head"), tree: descriptor.fetch("prepared_tree"))
40
- definition = work.definition_bytes(head: descriptor.fetch("prepared_head"), tree: descriptor.fetch("prepared_tree"))
41
- unless expected == descriptor.slice(*expected.keys) &&
42
- descriptor.values_at("assignment_id", "project_id") == work.manifest.values_at("assignment_id", "project_id") &&
43
- Digest::SHA256.hexdigest(definition) == descriptor.fetch("definition_digest")
44
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_mismatch: original graph association"
45
- end
46
- execution = work.definition["campaign_execution"]
47
- if execution
48
- candidate_reply ||= candidate_loader&.call
49
- candidate = candidate_reply&.data&.fetch("descriptor")
50
- unless candidate.is_a?(Hash) && candidate.keys.sort == %w[head tree bytes sha256 candidate_generation campaign_execution mapping_id assignment_id attempt_id project_id original_worker_identity original_worker_scratch_root original_binding_digest].sort &&
51
- candidate.slice("mapping_id", "assignment_id", "attempt_id", "project_id", "original_worker_identity", "original_worker_scratch_root", "original_binding_digest") ==
52
- descriptor.slice("mapping_id", "assignment_id", "attempt_id", "project_id", "original_worker_identity", "original_worker_scratch_root", "original_binding_digest") &&
53
- candidate.fetch("campaign_execution") == execution && candidate.fetch("head") == execution.fetch("head") &&
54
- candidate.fetch("candidate_generation") == execution.fetch("parent_candidate_generation") &&
55
- candidate_reply.parts.is_a?(Array) && candidate_reply.parts.one?
56
- raise AttemptErrors::EvidenceUnavailable, "campaign original candidate input differs"
57
- end
58
- materialized = CandidateTransfer.new(root: root).materialize_repository(bytes: candidate_reply.parts.first,
59
- sha256: candidate.fetch("sha256"), size: candidate.fetch("bytes"), head: candidate.fetch("head"),
60
- tree: candidate.fetch("tree"), root: root)
61
- @working_directory = materialized.fetch("directory").freeze
62
- elsif candidate_reply
63
- raise AttemptErrors::EvidenceUnavailable, "ordinary prepared input forbids a campaign candidate"
64
- end
65
- @descriptor = JSON.parse(JSON.generate(descriptor))
66
- pending = [@descriptor]
67
- until pending.empty?
68
- value = pending.pop
69
- pending.concat(value.values) if value.is_a?(Hash)
70
- pending.concat(value) if value.is_a?(Array)
71
- value.freeze
72
- end
73
- freeze
74
- end
75
-
76
- def render(text)
77
- values = descriptor.slice("mapping_id", "assignment_id", "scope", "attempt_id")
78
- rendered = +""
79
- cursor = 0
80
- # Only an opening delimiter introduces a token. Closing braces in
81
- # literal JSON are preserved; inserted selector values are not parsed.
82
- while (opening = text.index("{{", cursor))
83
- rendered << text[cursor...opening]
84
- closing = text.index("}}", opening + 2)
85
- unless closing
86
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_invalid: unresolved instruction token"
87
- end
88
- token = text[(opening + 2)...closing]
89
- if token.include?("{") || token.include?("}")
90
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_invalid: unresolved instruction token"
91
- end
92
- field = token.delete_prefix("admitted.")
93
- unless token == "admitted." + field && values.key?(field)
94
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_invalid: unknown instruction token"
95
- end
96
- rendered << Shellwords.escape(values.fetch(field))
97
- cursor = closing + 2
98
- end
99
- rendered << text[cursor..]
100
- rendered.freeze
101
- end
102
-
103
- def drive_prompt
104
- target = Shellwords.escape(descriptor.fetch("assignment_id") + "@" + descriptor.fetch("scope"))
105
- selectors = "--assignment #{target} --mapping #{Shellwords.escape(descriptor.fetch('mapping_id'))} --attempt #{Shellwords.escape(descriptor.fetch('attempt_id'))}"
106
- instructions = ["Drive the authenticated captured assignment subtree. Use ace-assign status #{selectors}, step #{selectors}, start #{selectors}, finish #{selectors} --message REPORT, and fail #{selectors} --message REASON. Do not fork-run, add, retry, renumber, or expand another graph."]
107
- if (execution = work.definition["campaign_execution"])
108
- instructions << "This is a registered campaign #{execution.fetch('phase')} child. The provider working directory is the private authenticated parent candidate repository at #{Shellwords.escape(execution.fetch('head'))}, with original base #{Shellwords.escape(execution.fetch('base'))}. Do not edit or advance that candidate. Execute the captured #{Shellwords.escape(execution.fetch('operation'))} phase and retain its actual #{Shellwords.escape(execution.fetch('check_name'))} check and report artifacts. Submit an ordinary campaign-free result (campaign null), operation #{Shellwords.escape(execution.fetch('operation'))}, exact original head/base, and actual check outcome. The existing independent reviewer must validate those same artifact bytes; an approval child must retain the exact canonical independent review actor/verdict. No session metadata or completed queue step counts as a settled R1 campaign round."
109
- end
110
- original = "--mapping #{Shellwords.escape(descriptor.fetch('mapping_id'))} --assignment #{Shellwords.escape(descriptor.fetch('assignment_id'))} --attempt #{Shellwords.escape(descriptor.fetch('attempt_id'))}"
111
- instructions << "Before the original worker exits, explicitly submit its tested candidate and real execution result through the protected authority. Queue completion is not candidate/result acceptance. Read ace-assign authority status #{original}; persist its authority_generation with the complete next command arguments and ordered byte inputs. For each NEW operation use a new explicit mutation and a newly selected generation; for exact retries preserve the original generation, mutation and bytes."
112
- instructions << "Submit one prepared Git bundle: ace-assign submit-candidate #{original} --head HEAD --candidate-generation CURRENT_CANDIDATE --expected-generation G --mutation CANDIDATE_MUTATION --bundle FILE. Select CURRENT_CANDIDATE from status result_candidate_generation, using 0 when null. Persist the returned candidate_generation and head as REVIEWED_CANDIDATE and HEAD; never derive the next counter by arithmetic or from queue completion."
113
- instructions << "Have an independent installed reviewer run ace-overseer review --project #{Shellwords.escape(descriptor.fetch('project_id'))} --agent #{Shellwords.escape(descriptor.fetch('mapping_id'))} --assignment #{Shellwords.escape(descriptor.fetch('assignment_id'))} --attempt #{Shellwords.escape(descriptor.fetch('attempt_id'))} --head HEAD --candidate-generation REVIEWED_CANDIDATE --expected-generation G --mutation REVIEW_REQUEST --accept-mutation REVIEW_ACCEPT. Do not impersonate that principal. The review consumer owns actual candidate export, independent checks and canonical acceptance. After review, read authority status again before the first result submission."
114
- instructions << "Produce the real receipt with Ace::Assign::Models::ExecutionReceipt.new/to_h: original assignment/attempt/project and scope #{Shellwords.escape(descriptor.fetch('scope'))}, exact tested candidate head, installed worker actor/role worker/runtime herdr, actual operation/verdict/executed checks and ordered artifact path/SHA256 pairs. No invented successful check or queue-to-receipt conversion. Submit JSON bytes and matching ordered artifact files with ace-assign submit-result #{original} --head HEAD --candidate-generation REVIEWED_CANDIDATE --expected-generation G --mutation RESULT_MUTATION --receipt FILE --artifact FILE ... . Failed empty artifacts use no --artifact; succeeded requires evidence. Supervisor finish is separate and revalidates result, independent review and no-writer/settlement proof."
115
- instructions << "For an authorized merge step captured INSIDE this subtree, use the protected delivery rule in wfi://assign/drive. Never execute an outer preset step or expand this graph. Retain the exact approved candidate, structured input bytes, authorization and request ID before the first request; select authority_generation for that new request only."
116
- service = "--project #{Shellwords.escape(descriptor.fetch('project_id'))} #{original} --scope #{Shellwords.escape(descriptor.fetch('scope'))} --candidate-head HEAD --candidate-generation REVIEWED_CANDIDATE"
117
- instructions << "Request the uniquely installed authorized merge receiver: ace-lab service request #{service} --service SERVICE --operation merge --authorization AUTHORIZATION --request-id REQUEST --expected-generation G --input INPUT_FILE. Credentials alone are not authorization. Persist the returned original input_digest and target with all request arguments. Lost/uncertain replies never permit another effect or refreshed-generation retry."
118
- instructions << "Inspect the same request: ace-lab service status #{service} --request REQUEST --input-digest INPUT_DIGEST --target TARGET. Only canonical succeeded evidence permits ace-assign delivery #{original} --scope #{Shellwords.escape(descriptor.fetch('scope'))} --operation merge --service-request REQUEST --candidate-head HEAD --candidate-generation REVIEWED_CANDIDATE --input-digest INPUT_DIGEST --target TARGET. This consumption verifies the imported receipt and never executes merge. Refusal, unavailable or uncertain leaves the active delivery step unfinished; report its exact blocker. Finish the queue step only after succeeded canonical delivery."
119
- work.manifest.fetch("context").each do |entry|
120
- instructions << render(work.files.fetch(entry.fetch("text").fetch("filename")))
121
- end
122
- work.manifest.fetch("steps").each do |entry|
123
- instructions << "Step #{entry.fetch('number')}:\n" + render(work.parse_queue_step!(work.files.fetch("steps/" + entry.fetch("filename"))).fetch(:body))
124
- end
125
- instructions.join("\n\n").freeze
126
- end
127
- end
128
- end
129
- end
130
- end
@@ -1,185 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "securerandom"
5
- require "yaml"
6
- require_relative "prepared_work"
7
- require_relative "private_directory"
8
- require_relative "../molecules/lifecycle_exclusion"
9
- require_relative "../organisms/assignment_executor"
10
-
11
- module Ace
12
- module Assign
13
- module Authority
14
- # Existing assignment/progress files, rooted only in the authenticated
15
- # original worker scratch. This cache never supplies authority or a graph.
16
- class PreparedQueue
17
- attr_reader :cache_base, :directory, :work, :descriptor
18
-
19
- def initialize(work:, descriptor:)
20
- @work, @descriptor = work, descriptor
21
- @scratch = PrivateDirectory.verify!(descriptor.fetch("original_worker_scratch_root"))
22
- %w[mapping_id attempt_id assignment_id].each do |field|
23
- value = descriptor.fetch(field)
24
- unavailable!("queue identity") unless value.is_a?(String) && value.match?(PreparedWork::TOKEN)
25
- end
26
- @cache_base = File.join(@scratch, "prepared-queues", descriptor.fetch("mapping_id"), descriptor.fetch("attempt_id"))
27
- @directory = File.join(cache_base, descriptor.fetch("assignment_id"))
28
- definition = JSON.parse(work.definition_bytes(head: descriptor.fetch("prepared_head"), tree: descriptor.fetch("prepared_tree")))
29
- unavailable!("captured assignment metadata") unless %w[name source_config created_at].all? do |field|
30
- definition[field].is_a?(String) && !definition[field].empty?
31
- end
32
- validate_timestamp!(definition.fetch("created_at"))
33
- validate_timestamp!(definition["updated_at"]) if definition.key?("updated_at")
34
- @metadata = Models::Assignment.from_h(definition, cache_dir: directory).to_h
35
- lock_root = File.join(@scratch, "prepared-queue-exclusion")
36
- private_directory!(lock_root, create: true)
37
- @exclusion = Molecules::LifecycleExclusion.new(root: lock_root)
38
- @key = @exclusion.assignment_key(descriptor.values_at("mapping_id", "attempt_id", "assignment_id").join(":"))
39
- end
40
-
41
- # Called ONLY after the fixed adapter proves actual original self. Later
42
- # CLI consumers use with_executor and cannot recreate a missing queue.
43
- def activate_original!
44
- @exclusion.with_exclusive(@key) do
45
- publish_initial! unless File.exist?(directory) || File.symlink?(directory)
46
- executor = verified_executor
47
- result = executor.status
48
- state = result.fetch(:state)
49
- unavailable!("selected queue has failed progress") unless state.failed.empty?
50
- return nil if state.subtree_complete?(descriptor.fetch("scope"))
51
- unless state.steps.all? { |step| step.status == :pending }
52
- unavailable!("original adapter has already activated queue progress")
53
- end
54
- root = state.find_by_number(descriptor.fetch("scope"))
55
- unavailable!("original selected fork root") unless root && root.fork?
56
- executor.step_writer.mark_active(root.file_path)
57
- # Same held-byte consumer validates the resulting progress before
58
- # returning to the adapter that may now start its provider.
59
- executor.status
60
- root
61
- end
62
- end
63
-
64
- def with_executor
65
- @exclusion.with_exclusive(@key) do
66
- unavailable!("published queue is unavailable; no reconstruction") unless File.directory?(directory)
67
- yield verified_executor
68
- end
69
- end
70
-
71
- private
72
-
73
- def verified_executor
74
- private_directory!(directory)
75
- private_directory!(File.join(directory, "steps"))
76
- private_directory!(File.join(directory, "reports"))
77
- metadata = work.parse_queue_metadata!(held_read(File.join(directory, "assignment.yaml"), PreparedWork::MAX_MANIFEST))
78
- unavailable!("queue assignment metadata differs") unless metadata.is_a?(Hash) &&
79
- metadata.reject { |key, _| key == "updated_at" } == @metadata.reject { |key, _| key == "updated_at" }
80
- validate_timestamp!(metadata["updated_at"]) if metadata.key?("updated_at")
81
- unavailable!("queue job differs") unless held_read(File.join(directory, "job.yaml"), PreparedWork::MAX_TEXT) == work.files.fetch("job.yaml")
82
- owner = self
83
- scanner = Molecules::QueueScanner.new
84
- parser = scanner.method(:scan_captured)
85
- scanner.define_singleton_method(:scan) do |steps_dir, assignment:|
86
- raise AttemptErrors::EvidenceUnavailable, "prepared queue scanner path differs" unless steps_dir == File.join(owner.directory, "steps")
87
- steps, reports = owner.send(:captured_queue)
88
- parser.call(steps: steps, assignment: assignment, reports: reports)
89
- end
90
- Organisms::AssignmentExecutor.new(cache_base: cache_base,
91
- selected_assignment: Models::Assignment.from_h(metadata, cache_dir: directory), queue_scanner: scanner)
92
- end
93
-
94
- def captured_queue
95
- expected = work.manifest.fetch("steps").map { |entry| entry.fetch("filename") }
96
- steps_dir = File.join(directory, "steps")
97
- unavailable!("queue step inventory differs") unless Dir.children(steps_dir).sort == expected.sort
98
- steps = expected.to_h do |filename|
99
- bytes = held_read(File.join(steps_dir, filename), PreparedWork::MAX_TEXT)
100
- parsed = work.parse_queue_step!(bytes)
101
- accepted = work.parse_queue_step!(work.files.fetch("steps/" + filename))
102
- projection = ->(value) { value.fetch(:frontmatter).reject { |key, _| PreparedWork::PROGRESS.include?(key) } }
103
- unless projection.call(parsed) == projection.call(accepted) && parsed.fetch(:body) == accepted.fetch(:body) &&
104
- %w[pending active done failed].include?(parsed.fetch(:frontmatter).fetch("status", "pending"))
105
- unavailable!("queue work-bearing input differs")
106
- end
107
- [filename, bytes]
108
- end
109
- reports_dir = File.join(directory, "reports")
110
- allowed = work.manifest.fetch("steps").map do |entry|
111
- name = work.parse_queue_step!(work.files.fetch("steps/" + entry.fetch("filename"))).fetch(:frontmatter).fetch("name")
112
- Atoms::StepFileParser.generate_report_filename(entry.fetch("number"), name)
113
- end
114
- reports = Dir.children(reports_dir).to_h do |filename|
115
- unavailable!("queue report inventory differs") unless allowed.include?(filename)
116
- bytes = held_read(File.join(reports_dir, filename), PreparedWork::MAX_TEXT)
117
- work.parse_queue_step!(bytes)
118
- [filename, bytes]
119
- end
120
- [steps.freeze, reports.freeze]
121
- end
122
-
123
- def publish_initial!
124
- private_directory!(File.join(@scratch, "prepared-queues"), create: true)
125
- private_directory!(File.dirname(cache_base), create: true)
126
- private_directory!(cache_base, create: true)
127
- stage = File.join(cache_base, ".prepared-#{SecureRandom.hex(12)}")
128
- Dir.mkdir(stage, 0700)
129
- %w[steps reports].each { |name| Dir.mkdir(File.join(stage, name), 0700) }
130
- durable_write(File.join(stage, "assignment.yaml"), @metadata.to_yaml)
131
- durable_write(File.join(stage, "job.yaml"), work.files.fetch("job.yaml"))
132
- work.manifest.fetch("steps").each do |entry|
133
- name = entry.fetch("filename")
134
- durable_write(File.join(stage, "steps", name), work.files.fetch("steps/" + name))
135
- end
136
- %w[steps reports].each { |name| File.open(File.join(stage, name), File::RDONLY) { |file| file.fsync } }
137
- File.open(stage, File::RDONLY) { |file| file.fsync }
138
- unavailable!("queue publication collision") if File.exist?(directory) || File.symlink?(directory)
139
- File.rename(stage, directory)
140
- File.open(cache_base, File::RDONLY) { |file| file.fsync }
141
- ensure
142
- FileUtils.remove_entry(stage) if stage && File.directory?(stage) && !File.symlink?(stage)
143
- end
144
-
145
- def private_directory!(path, create: false)
146
- Dir.mkdir(path, 0700) if create && !File.exist?(path) && !File.symlink?(path)
147
- PrivateDirectory.verify!(path)
148
- end
149
-
150
- def durable_write(path, bytes)
151
- File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) do |file|
152
- file.write(bytes); file.flush; file.fsync
153
- end
154
- end
155
-
156
- def held_read(path, limit)
157
- File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
158
- before = file.stat
159
- unavailable!("queue held file kind/size") unless before.file? && before.uid == Process.uid &&
160
- (before.mode & 0077).zero? && before.size.between?(1, limit)
161
- bytes = file.read(limit + 1).force_encoding(Encoding::UTF_8)
162
- after = file.stat
163
- unavailable!("queue held file changed") unless bytes.valid_encoding? && !bytes.include?("\0") &&
164
- bytes.bytesize == before.size && [before.dev, before.ino, before.size, before.mtime, before.ctime] ==
165
- [after.dev, after.ino, after.size, after.mtime, after.ctime]
166
- bytes.freeze
167
- end
168
- rescue SystemCallError
169
- unavailable!("queue held file unavailable")
170
- end
171
-
172
- def unavailable!(reason)
173
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: #{reason}"
174
- end
175
-
176
- def validate_timestamp!(value)
177
- unavailable!("captured assignment timestamp") unless value.is_a?(String)
178
- Time.iso8601(value)
179
- rescue ArgumentError
180
- unavailable!("captured assignment timestamp")
181
- end
182
- end
183
- end
184
- end
185
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "protected_assignment_context"
4
- require_relative "task_context_entry"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # The selected immutable Lab owner sets the hook before entry evaluation.
10
- # Its presence alone never grants scope; original fetch admission decides.
11
- class PreparedTaskContext
12
- SCHEMA = "ace.assign.prepared-task-context/v1"
13
- MAX_RESPONSE = 6_307_840
14
- PRINCIPAL_SCHEMA = "ace.assign.task-context-principal/v1"
15
- SELECTION_SCHEMA = "ace.assign.task-context-selection/v1"
16
- MAX_PRINCIPAL = 1_024
17
- MAX_SELECTION = 65_536
18
-
19
- def initialize(context: nil)
20
- @context = context
21
- end
22
-
23
- def call(mapping:, assignment:, attempt:, task:)
24
- unavailable!("task-context task selector") unless task.is_a?(String) && task.match?(PreparedWork::TOKEN)
25
- selected_entry = selected_entry!
26
- input = resolved_input(mapping: mapping, assignment: assignment, attempt: attempt)
27
- unavailable!("original protected task-context admission") unless input.descriptor.fetch("task_context_entry") == selected_entry
28
- item = input.work.manifest.fetch("context").find { |entry| entry.fetch("task_id") == task }
29
- unavailable!("task is outside captured original context") unless item
30
- text = input.work.files.fetch(item.fetch("text").fetch("filename"))
31
- record = input.descriptor.slice("mapping_id", "assignment_id", "attempt_id", "scope", "definition_digest", "selection_sha256")
32
- .merge("schema" => SCHEMA, "task_id" => task, "text" => text)
33
- response!(record, MAX_RESPONSE)
34
- rescue ArgumentError, KeyError
35
- unavailable!("task-context input or original entry differs")
36
- end
37
-
38
- def principal
39
- selected_entry!
40
- context = installed_context!
41
- response!({"schema" => PRINCIPAL_SCHEMA, "uid" => context.uid, "protected_worker" => context.protected_worker?}, MAX_PRINCIPAL)
42
- end
43
-
44
- def selection(mapping:, assignment:, attempt:)
45
- selected_entry!
46
- # Current discovery selects a trusted classifier only. Original fetch
47
- # independently selects the retained entry; no text leaves this mode.
48
- input = resolved_input(mapping: mapping, assignment: assignment, attempt: attempt)
49
- response!(input.descriptor.slice("mapping_id", "assignment_id", "attempt_id", "scope", "definition_digest", "selection_sha256", "task_context_entry")
50
- .merge("schema" => SELECTION_SCHEMA), MAX_SELECTION)
51
- rescue ArgumentError, KeyError
52
- unavailable!("task-context original selection differs")
53
- end
54
-
55
- def inbox_principal
56
- selected_entry!
57
- context = installed_context!
58
- response!({"schema" => "ace.assign.inbox-context-principal/v1", "uid" => context.uid,
59
- "protected_participant" => context.protected_participant?}, MAX_PRINCIPAL)
60
- end
61
-
62
- def inbox_selection(project:, mapping:, inbox_context:)
63
- selected_entry!
64
- installed_context!.inbox_context_selection(project: project, mapping: mapping, inbox_context: inbox_context) do |record|
65
- response!(record.merge("schema" => "ace.assign.inbox-context-selection/v1"), 16_384)
66
- end
67
- end
68
-
69
- private
70
-
71
- def selected_entry!
72
- unless Object.const_defined?(:ACE_PROTECTED_TASK_CONTEXT_ENTRY, false)
73
- unavailable!("selected original task-context entry is absent")
74
- end
75
- selected_entry = Object.const_get(:ACE_PROTECTED_TASK_CONTEXT_ENTRY, false)
76
- TaskContextEntry.validate!(selected_entry)
77
- objects = [selected_entry, *selected_entry.keys, *selected_entry.values]
78
- selected_entry.values.each { |reference| objects.concat(reference.keys); objects.concat(reference.values) }
79
- unavailable!("selected task-context entry is mutable") unless objects.all?(&:frozen?)
80
- selected_entry
81
- rescue ArgumentError, KeyError, TypeError
82
- unavailable!("task-context selected entry differs")
83
- end
84
-
85
- def installed_context!
86
- @context ||= ProtectedAssignmentContext.load
87
- unavailable!("published task-context installed owner is absent") unless @context.installed?
88
- @context
89
- end
90
-
91
- def resolved_input(mapping:, assignment:, attempt:)
92
- unless [mapping, attempt].all? { |value| value.is_a?(String) && value.match?(PreparedWork::TOKEN) } && assignment.is_a?(String)
93
- unavailable!("task-context selectors")
94
- end
95
- assignment_id, scope = assignment.split("@", 2)
96
- unless assignment_id&.match?(PreparedWork::TOKEN) && scope&.match?(PreparedWork::SCOPE)
97
- unavailable!("task-context scoped assignment")
98
- end
99
- input = installed_context!.resolve(options: {mapping: mapping, attempt: attempt}, assignment_id: assignment_id, scope: scope)
100
- unavailable!("original protected task-context admission") unless input
101
- input
102
- end
103
-
104
- def response!(record, limit)
105
- response = JSON.generate(record) + "\n"
106
- unavailable!("task-context response bound") if response.bytesize > limit
107
- response.freeze
108
- end
109
-
110
- def unavailable!(reason)
111
- raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: #{reason}"
112
- end
113
- end
114
- end
115
- end
116
- end