ace-assign 0.70.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 (181) 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 -2099
  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 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  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 +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  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/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,196 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "receipt_transfer"
4
- require_relative "../molecules/canonical_evidence"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # A worker submits an attributable result. This never accepts a terminal
10
- # outcome, an independent review, or an external delivery effect.
11
- module OrdinaryResults
12
- RESULT_OPERATIONS = %w[submit_result result_status result_snapshot].freeze
13
- RESULT_TRANSFERS = {
14
- "submit_result" => {direction: :upload, purpose: :receipt_artifacts, roles: %i[worker supervisor]},
15
- "result_snapshot" => {direction: :download, purpose: :receipt_artifacts, roles: %i[worker supervisor]}
16
- }.freeze
17
- RESULT_KEYS = %w[assignment_id attempt_id expected_generation pane head receipt_sha256 transfer].freeze
18
-
19
- private
20
-
21
- def validate_result!(request)
22
- params = request.fetch("params")
23
- if request.fetch("operation") != "submit_result"
24
- snapshot = request.fetch("operation") == "result_snapshot"
25
- strict!(params, %w[assignment_id attempt_id] + (snapshot ? ["result_id"] : []))
26
- token!(params.fetch("result_id")) if snapshot
27
- raise ArgumentError, "ordinary result status mutation must be null" unless request.fetch("mutation_id").nil?
28
- else
29
- strict!(params, RESULT_KEYS)
30
- token!(request.fetch("mutation_id"))
31
- unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation").positive? &&
32
- params.fetch("head").is_a?(String) && params.fetch("head").match?(/\A[0-9a-f]{40}\z/) &&
33
- params.fetch("receipt_sha256").is_a?(String) && params.fetch("receipt_sha256").match?(/\A[0-9a-f]{64}\z/)
34
- raise ArgumentError, "ordinary result selection is invalid"
35
- end
36
- end
37
- %w[assignment_id attempt_id].each { |key| token!(params.fetch(key)) }
38
- end
39
-
40
- def ordinary_result(request, peer, role, deadline, transfer)
41
- validate_result!(request)
42
- params = request.fetch("params")
43
- commit = @journal.ref_value
44
- registered = registration(params.fetch("assignment_id"), commit: commit)
45
- raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
46
- if request.fetch("operation") != "submit_result"
47
- events = result_events(params, commit)
48
- result = retained_ordinary_result(params, events, commit)
49
- if request.fetch("operation") == "result_snapshot"
50
- unless result && result.fetch("result_id") == params.fetch("result_id")
51
- raise AttemptErrors::NotFound, "ordinary exact result is unavailable"
52
- end
53
- receipt = result.fetch("receipt")
54
- canonical = Molecules::CanonicalEvidence.new(journal: @journal)
55
- parts = [JSON.generate(receipt)] + receipt.fetch("artifacts").map { |artifact|
56
- canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
57
- **ordinary_result_context(result.fetch("binding")), commit: commit)
58
- }
59
- authorize!(request, peer, role, deadline)
60
- return {data: ordinary_result_projection(result).merge("journal_commit" => commit), transfer_parts: parts, replayed: false}
61
- end
62
- authorize!(request, peer, role, deadline)
63
- return {data: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
64
- "journal_commit" => commit, "generation" => @journal.authority_generation(events),
65
- "state" => Molecules::CanonicalAttemptState.derive(events), "submitted_result" => result && ordinary_result_projection(result)}, replayed: false}
66
- end
67
- unless %i[worker supervisor].include?(role)
68
- raise AttemptErrors::UnauthorizedIdentity, "ordinary result requires its original worker"
69
- end
70
- authorize_start_replay!(request, peer, role, deadline)
71
- admitted = ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
72
- artifact_field: "artifacts", reference_key: "path")
73
- digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
74
- keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
75
- @exclusion.with_shared_multi(keys, deadline: deadline) do
76
- @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
77
- mutation_id: request.fetch("mutation_id"), operation: "submit_result", parameters_digest: digest,
78
- expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
79
- replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, generation|
80
- authorize_start_replay!(request, peer, role, deadline)
81
- unless Molecules::CanonicalAttemptState.derive(events) == "running"
82
- raise AttemptErrors::Conflict, "ordinary result requires the original running attempt"
83
- end
84
- raise AttemptErrors::Conflict, "ordinary attempt already submitted its result" if retained_ordinary_result(params, events, prefix)
85
- observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: params.fetch("head"), deadline: deadline)
86
- start_replay_owner!(params, observed.fetch(:identity))
87
- ordinary_result_plan(params, peer, admitted, observed, events, prefix, generation + 1)
88
- end
89
- end
90
- end
91
-
92
- def result_events(params, commit)
93
- events = @journal.read_events(params.fetch("assignment_id"), commit: commit)
94
- .select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
95
- unless Models::EvidenceEvent.chain_valid?(events) && events.one? { |event| event["type"] == "process_start" }
96
- raise AttemptErrors::EvidenceUnavailable, "ordinary result attempt evidence is unavailable"
97
- end
98
- events
99
- end
100
-
101
- def ordinary_result_plan(params, peer, admitted, observed, events, commit, generation)
102
- original = admitted.fetch(:receipt)
103
- original_receipt = Models::ExecutionReceipt.from_h(original)
104
- unless original_receipt.digest == Atoms::EvidenceDigest.digest(original_receipt.digest_payload)
105
- raise AttemptErrors::ReceiptRejected, "ordinary original result digest differs"
106
- end
107
- identity = observed.fetch(:identity)
108
- expected_producer = {"actor" => identity.actor, "role" => identity.role, "runtime" => identity.runtime}
109
- validate_ordinary_result_receipt!(original, expected_producer)
110
- id = SecureRandom.hex(16)
111
- binding = params.slice("assignment_id", "attempt_id", "head").merge("authority" => "ordinary", "project_id" => @project,
112
- "result_id" => id, "worker_uid" => peer.fetch("uid"), "producer" => expected_producer,
113
- "runtime_binding" => identity.runtime_binding, "checkout" => observed.fetch(:checkout),
114
- "generation" => generation, "uploaded_receipt_sha256" => admitted.fetch(:receipt_sha256),
115
- "original_receipt_digest" => original_receipt.digest)
116
- canonical = Molecules::CanonicalEvidence.new(journal: @journal)
117
- plan = admitted.fetch(:artifacts).empty? ? {events: [], blobs: {}, references: []} :
118
- canonical.import_plan(**ordinary_result_context(binding), artifacts: admitted.fetch(:artifacts),
119
- admitted_after_event_digest: events.last.fetch("digest"))
120
- receipt = original.except("digest", "recorded_at").merge("artifacts" => plan.fetch(:references).map { |reference|
121
- {"path" => reference.fetch("ref"), "sha256" => reference.fetch("sha256")}
122
- })
123
- pending = @journal.send(:chain_mutation_events, params.fetch("attempt_id"), events.last.fetch("digest"), plan.fetch(:events))
124
- reader = ->(_data, artifact) { canonical.read_pending({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
125
- **ordinary_result_context(binding), current_events: events, pending_events: pending, blobs: plan.fetch(:blobs), commit: commit) }
126
- verified = verify_ordinary_result_receipt(params, receipt, binding, commit, reader)
127
- if JSON.generate(verified.to_h).bytesize > 16_384
128
- raise AttemptErrors::ReceiptRejected, "canonical ordinary receipt exceeds its transfer limit"
129
- end
130
- payload = {"authority" => "ordinary", "result_id" => id, "binding" => binding, "receipt" => verified.to_h}
131
- plan.merge(events: plan.fetch(:events) + [{type: "result_submitted", payload: payload}],
132
- data: ordinary_result_projection(payload))
133
- end
134
-
135
- def ordinary_result_projection(payload)
136
- payload.slice("result_id").merge(payload.fetch("binding").slice("head", "generation"),
137
- "receipt_digest" => payload.fetch("receipt").fetch("digest"), "verdict" => payload.fetch("receipt").fetch("verdict"),
138
- "state" => "running", "authority" => "ordinary")
139
- end
140
-
141
- def ordinary_result_context(binding)
142
- {kind: "result", project_id: @project, assignment_id: binding.fetch("assignment_id"), attempt_id: binding.fetch("attempt_id"),
143
- peer_uid: binding.fetch("worker_uid"), binding: binding, request_id_or_event_id: binding.fetch("result_id"), generation: binding.fetch("generation")}
144
- end
145
-
146
- def validate_ordinary_result_receipt!(receipt, producer)
147
- unless receipt["producer"] == producer && (receipt.keys - Models::ExecutionReceipt.from_h(receipt).to_h.keys).empty? &&
148
- !receipt["campaign"] && !receipt["review"] && receipt["operation"] != "review" &&
149
- !Molecules::ReceiptVerifier::EXTERNAL_EFFECT_OPERATIONS.include?(receipt["operation"])
150
- raise AttemptErrors::ReceiptRejected, "ordinary result producer or operation requires another evidence owner"
151
- end
152
- end
153
-
154
- def verify_ordinary_result_receipt(params, receipt, binding, commit, reader)
155
- validate_ordinary_result_receipt!(receipt, binding.fetch("producer"))
156
- attempt = @journal.derived_attempts(params.fetch("assignment_id"), commit: commit).find { |item| item.attempt_id == params.fetch("attempt_id") }
157
- raise AttemptErrors::EvidenceUnavailable, "ordinary result attempt is unavailable" unless attempt
158
- Array(receipt["artifacts"]).each { |artifact| reader.call(receipt, artifact) }
159
- Molecules::ReceiptVerifier.new(artifact_reader: reader).verify_result!(receipt,
160
- attempt: attempt, live_head: binding.fetch("head"), repo_root: @journal.repo_root)
161
- end
162
-
163
- def retained_ordinary_result(params, events, commit)
164
- raise AttemptErrors::EvidenceUnavailable, "ordinary result chain is corrupt" unless Models::EvidenceEvent.chain_valid?(events)
165
- records = events.select { |event| event["type"] == "result_submitted" }
166
- replies = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "submit_result" }
167
- return nil if records.empty? && replies.empty?
168
- unless records.one? && replies.one?
169
- raise AttemptErrors::EvidenceUnavailable, "ordinary result provenance is missing or duplicated"
170
- end
171
- payload = records.first.fetch("payload")
172
- binding = payload.fetch("binding")
173
- start = events.find { |event| event["type"] == "process_start" }.fetch("payload")
174
- unless payload.keys.sort == %w[authority binding receipt result_id] && payload["authority"] == "ordinary" &&
175
- payload["result_id"].is_a?(String) && payload["result_id"].match?(/\A[0-9a-f]{32}\z/) &&
176
- binding.keys.sort == %w[assignment_id attempt_id authority checkout generation head original_receipt_digest producer project_id result_id runtime_binding uploaded_receipt_sha256 worker_uid] &&
177
- binding["authority"] == "ordinary" && binding["result_id"] == payload["result_id"] &&
178
- binding["head"] == binding.dig("checkout", "head") &&
179
- binding.values_at("project_id", "assignment_id", "attempt_id", "runtime_binding") ==
180
- [@project, params.fetch("assignment_id"), params.fetch("attempt_id"), start.fetch("runtime_binding")] &&
181
- binding.fetch("producer") == start.slice("actor", "role", "runtime") &&
182
- binding.fetch("worker_uid") == start.dig("runtime_binding", "process_identity", "uid") &&
183
- payload.fetch("receipt").fetch("producer") == binding.fetch("producer") &&
184
- replies.first.dig("payload", "data").slice(*ordinary_result_projection(payload).keys) == ordinary_result_projection(payload)
185
- raise AttemptErrors::EvidenceUnavailable, "ordinary result owner differs"
186
- end
187
- canonical = Molecules::CanonicalEvidence.new(journal: @journal)
188
- reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
189
- **ordinary_result_context(binding), commit: commit) }
190
- verify_ordinary_result_receipt(params, payload.fetch("receipt"), binding, commit, reader)
191
- payload
192
- end
193
- end
194
- end
195
- end
196
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "socket_server"
4
-
5
- module Ace
6
- module Assign
7
- module Authority
8
- # Explicit ordinary admission over Assign's existing wire/transfer owner.
9
- # The Lab composition supplies a trusted installed admission object and
10
- # closed handlers. No protected Deployment or kernel adapter is replaced.
11
- class OrdinaryServer < SocketServer
12
- def initialize(admission:, lifecycle:, services: [])
13
- @admission = admission
14
- super(service: admission.service, lifecycle: lifecycle, services: services)
15
- end
16
-
17
- private
18
-
19
- def verify_listener!
20
- @admission.verify_listener!
21
- end
22
-
23
- def authorize_transfer!(request:, peer:, role:, deadline:)
24
- @lifecycle.authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
25
- end
26
-
27
- def dispatch_lifecycle(request:, peer:, role:, deadline:, transfer: nil)
28
- @lifecycle.dispatch(request: request, peer: peer, role: role, deadline: deadline, transfer: transfer)
29
- end
30
-
31
- def receive(socket)
32
- deadline = wire.deadline(30)
33
- peer = @admission.peer(socket, deadline: deadline)
34
- ingress = Ace::Runtime::Molecules::ProtectedSocket::Ingress.new(socket)
35
- frame = wire.read(ingress, deadline: [deadline, wire.deadline(5)].min, limit: 16_384, with_size: true)
36
- request = frame.fetch(:data)
37
- unless request.is_a?(Hash) && request.keys.sort == %w[mutation_id operation params project_id version] &&
38
- request["version"].is_a?(Integer) && request["version"] == 1 &&
39
- request["operation"].is_a?(String) && request["params"].is_a?(Hash)
40
- raise ArgumentError, "invalid ordinary authority envelope"
41
- end
42
- role = @admission.authorize!(request: request, peer: peer, deadline: deadline)
43
- binding = @lifecycle.transfer_binding(request)
44
- bodyless_read!(ingress, deadline) unless binding && binding.fetch(:direction) == :upload
45
- # Both binary bodies and EOF use ancillary-rejecting reads. Replies
46
- # use the original duplex socket through the wrapper below.
47
- dispatch_request(Duplex.new(socket, ingress), request, peer, role, deadline, frame_bytes: frame.fetch(:bytesize))
48
- rescue ArgumentError, KeyError, AttemptErrors::MalformedTransfer
49
- refusal(socket, "invalid_input")
50
- rescue AttemptErrors::UnauthorizedIdentity, SecurityError
51
- refusal(socket, "unauthorized")
52
- rescue AttemptErrors::Conflict
53
- refusal(socket, "conflict")
54
- rescue AttemptErrors::NotFound
55
- refusal(socket, "missing")
56
- rescue AttemptErrors::BoundedResultUnavailable
57
- refusal(socket, "bounded_result")
58
- rescue StandardError
59
- refusal(socket, "evidence_unavailable")
60
- ensure
61
- socket.close unless socket.closed?
62
- @mutex.synchronize { @connections.delete(socket) }
63
- end
64
-
65
- class Duplex
66
- def initialize(socket, ingress) = (@socket, @ingress = socket, ingress)
67
- def to_io = @socket
68
- def read_nonblock(*args, **options) = @ingress.read_nonblock(*args, **options)
69
- def write_nonblock(*args, **options) = @socket.write_nonblock(*args, **options)
70
- end
71
- private_constant :Duplex
72
- end
73
- end
74
- end
75
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- # Original descriptor selection shared by canonical Inbox consumers.
7
- # The held historical manifest/key owner supplies the exact artifact.
8
- class OriginalInboxSelection
9
- def initialize(history:, authority_id:)
10
- @history, @authority_id = history, authority_id
11
- end
12
-
13
- def load!(events:, params:, map:, journal:, commit:)
14
- provisioning = events.select { |event| event["type"] == "scope_provisioning" }
15
- reservations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
16
- unless provisioning.one? && reservations.one?
17
- raise AttemptErrors::EvidenceUnavailable, "context completion original selection is ambiguous"
18
- end
19
- identity = provisioning.first.fetch("payload")
20
- reserved = reservations.first.fetch("payload").fetch("data")
21
- original = @history.descriptor!(sha256: identity.fetch("descriptor_sha256"))
22
- selected = original.mapping(params.fetch("mapping_id"))
23
- unless identity.keys.sort == %w[deployment_digest descriptor_sha256 reservation_generation slot_id] &&
24
- reserved.values_at("project_id", "assignment_id", "attempt_id", "mapping_id") ==
25
- [map.fetch("project_id"), params.fetch("assignment_id"), params.fetch("attempt_id"), params.fetch("mapping_id")] &&
26
- identity.fetch("reservation_generation") == reserved.fetch("reservation_generation") &&
27
- identity.fetch("deployment_digest") == original.mapping_digest(params.fetch("mapping_id")) &&
28
- selected.fetch("project_id") == map.fetch("project_id") && selected.fetch("authority_id") == @authority_id &&
29
- identity.fetch("slot_id") == selected.fetch("execution_scope").fetch("slot_id") &&
30
- original.project(selected.fetch("project_id")).values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
31
- [journal.repo_root, journal.ref, journal.checkout_root]
32
- raise AttemptErrors::EvidenceUnavailable, "context completion original association differs"
33
- end
34
- journal.event_commit!(assignment_id: params.fetch("assignment_id"), event_digest: provisioning.first.fetch("digest"), commit: commit)
35
- [original, selected]
36
- end
37
-
38
- end
39
- end
40
- end
41
- end
@@ -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