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,78 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative "execution_scope_lineage"
3
-
4
- module Ace
5
- module Assign
6
- module Molecules
7
- module JournalInputInhibition
8
- INPUT_INHIBITION_FIELDS = %w[assignment_id attempt_id mapping_id original_binding_digest project_id seal_event_id].freeze
9
-
10
- def observe_input_inhibition(selection:, evidence:, &authentication)
11
- raise ArgumentError, "Original inhibition authentication is required" unless authentication
12
- input = selection.merge("guarded_evidence" => evidence)
13
- digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(input)))
14
- mutate(assignment_id: selection.fetch("assignment_id"), attempt_id: selection.fetch("attempt_id"),
15
- mutation_id: "input-inhibit.#{selection.fetch('seal_event_id')}", operation: "input_inhibition_observation",
16
- parameters_digest: digest, expected_generation: nil, generation_mode: :input_inhibition,
17
- input_inhibition: input, with_replay: true) do |events, commit, generation|
18
- original = authentication.call(events, commit, generation)
19
- unless original.is_a?(Hash) && original.keys.sort == %w[binding_digest origin] && original["binding_digest"] == selection.fetch("original_binding_digest")
20
- raise AttemptErrors::EvidenceUnavailable, "Original input inhibition record differs"
21
- end
22
- verify_input_drained_evidence!(evidence, original.fetch("origin"))
23
- {events: [{type: "input_inhibited", payload: selection.merge("guarded_evidence" => evidence)}], blobs: {},
24
- data: selection.slice("original_binding_digest", "seal_event_id")}
25
- end
26
- end
27
-
28
- def verify_input_drained_evidence!(evidence, original)
29
- unless original.is_a?(Hash) && evidence.is_a?(Hash) && evidence.keys.sort == %w[input_state origin outcome pending_input] &&
30
- evidence["outcome"] == "inhibited" && evidence["input_state"] == "inhibited" &&
31
- evidence["pending_input"].is_a?(Integer) && evidence["pending_input"].zero?
32
- raise AttemptErrors::EvidenceUnavailable, "Native input drainage is unconfirmed"
33
- end
34
- received = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(evidence.fetch("origin"),
35
- terminal_id: original.fetch("terminal_id"), child: original.fetch("child"))
36
- raise AttemptErrors::EvidenceUnavailable, "Native input drainage origin differs" unless received == original
37
- true
38
- rescue Ace::Runtime::RuntimeUnavailableError, KeyError, TypeError
39
- raise AttemptErrors::EvidenceUnavailable, "Native input drainage is unverifiable"
40
- end
41
-
42
- private
43
-
44
- def verify_input_inhibition!(input, mutation_id:, digest:, assignment_id:, attempt_id:, commit:)
45
- unless input.is_a?(Hash) && input.keys.sort == (INPUT_INHIBITION_FIELDS + ["guarded_evidence"]).sort &&
46
- INPUT_INHIBITION_FIELDS.all? { |key| input[key].is_a?(String) } &&
47
- %w[original_binding_digest seal_event_id].all? { |key| input.fetch(key).match?(/\A[0-9a-f]{64}\z/) } &&
48
- input["assignment_id"] == assignment_id && input["attempt_id"] == attempt_id &&
49
- mutation_id == "input-inhibit.#{input.fetch('seal_event_id')}" &&
50
- digest == Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(input)))
51
- raise ArgumentError, "Invalid fixed input inhibition selector"
52
- end
53
- events = read_events(assignment_id, commit: commit).select { |event| event["attempt_id"] == attempt_id }
54
- authority_generation(events)
55
- record = events.find { |event| event["digest"] == input.fetch("original_binding_digest") && event["type"] == "authority_mutation" &&
56
- event.dig("payload", "operation") == "record_launch" }
57
- data = record&.dig("payload", "data")
58
- seal = events.find { |event| event["digest"] == input.fetch("seal_event_id") && event["type"] == "scope_sealed" }
59
- accepted_seal = seal && events.find { |event| event["type"] == "authority_mutation" && event["previous_digest"] == seal.fetch("digest") &&
60
- %w[close_execution_scope stop_attempt].include?(event.dig("payload", "operation")) }
61
- lineage = ExecutionScopeLineage.new(events: events, project_id: input.fetch("project_id"), assignment_id: assignment_id,
62
- attempt_id: attempt_id, mapping_id: input.fetch("mapping_id"))
63
- unless data.is_a?(Hash) && %w[assignment_id attempt_id mapping_id].all? { |key| data[key] == input.fetch(key) } &&
64
- seal && accepted_seal && lineage.seal_event == seal && events.index(record) < events.index(seal) &&
65
- events.drop(events.index(seal) + 1).none? { |event| event["type"] == "prompt_issued" } &&
66
- events.none? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
67
- raise AttemptErrors::EvidenceUnavailable, "Input inhibition does not join accepted original seal"
68
- end
69
- origin = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(data.fetch("guarded_origin"),
70
- terminal_id: data.fetch("process_binding").fetch("terminal_id"), child: data.fetch("process_binding").fetch("process_identity"))
71
- verify_input_drained_evidence!(input.fetch("guarded_evidence"), origin)
72
- rescue KeyError, TypeError, Ace::Runtime::RuntimeUnavailableError
73
- raise AttemptErrors::EvidenceUnavailable, "Input inhibition canonical selection is malformed"
74
- end
75
- end
76
- end
77
- end
78
- end
@@ -1,274 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "digest"
5
- require "ace/herdr/molecules/bounded_process"
6
- require_relative "journal_prompt_mutation"
7
- require_relative "../atoms/candidate_limits"
8
- require_relative "journal_input_inhibition"
9
-
10
- module Ace
11
- module Assign
12
- module Molecules
13
- # Protected authority mutations share the execution ref's lock and CAS.
14
- # Replies live in chained attempt events, rather than an RPC ledger.
15
- module JournalMutation
16
- include JournalPromptMutation
17
- include JournalInputInhibition
18
- ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
19
-
20
- # Shared canonical projection used by admission and protected status.
21
- # Callers hold the same owner exclusion when projecting current events.
22
- def authority_generation(events)
23
- unless Models::EvidenceEvent.chain_valid?(events)
24
- raise AttemptErrors::EvidenceUnavailable, "Attempt event chain is corrupt"
25
- end
26
- events.count { |event| event["type"] == "authority_mutation" }
27
- end
28
-
29
- def canonical_attempt_state(events)
30
- authority_generation(events)
31
- derive_state(events)
32
- end
33
-
34
- # The block runs against the current ref on every CAS attempt. It
35
- # returns events [{type:, payload:}], immutable blobs, and public data.
36
- # Only the authority calls this internal journal API; wire requests
37
- # cannot select blob paths, event types, or accepted response data.
38
- def mutate(assignment_id:, attempt_id:, mutation_id:, operation:, parameters_digest:, expected_generation:, with_replay: false, generation_mode: :expected, prompt_binding: nil, prompt_completion: nil, prompt_observation: nil, input_inhibition: nil, deadline: nil, replay_authorizer: nil)
39
- unless (generation_mode == :expected && expected_generation.is_a?(Integer) && expected_generation >= 0) ||
40
- (generation_mode == :recorded_completion && %w[complete_service complete_no_effect].include?(operation) && expected_generation.nil?) ||
41
- (generation_mode == :prompt_completion && operation == "prompt_attempt" && expected_generation.nil? && prompt_completion.is_a?(Hash)) ||
42
- (generation_mode == :prompt_observation && operation == "prompt_observation" && expected_generation.nil? && prompt_observation.is_a?(Hash)) ||
43
- (generation_mode == :input_inhibition && operation == "input_inhibition_observation" && expected_generation.nil? && input_inhibition.is_a?(Hash))
44
- raise ArgumentError, "invalid fixed mutation generation mode"
45
- end
46
- if input_inhibition && generation_mode != :input_inhibition
47
- raise ArgumentError, "Input inhibition requires its fixed mode"
48
- end
49
- if operation == "input_inhibition_observation" && !(generation_mode == :input_inhibition && input_inhibition)
50
- raise ArgumentError, "Input inhibition observation requires its accepted original selector"
51
- end
52
- if mutation_id.is_a?(String) && mutation_id.start_with?("input-inhibit.") && !(operation == "input_inhibition_observation" && input_inhibition)
53
- raise ArgumentError, "Reserved input inhibition namespace"
54
- end
55
- if prompt_completion && generation_mode != :prompt_completion
56
- raise ArgumentError, "prompt completion selector requires its fixed mode"
57
- end
58
- if prompt_observation && generation_mode != :prompt_observation
59
- raise ArgumentError, "prompt observation selector requires its fixed mode"
60
- end
61
- if operation == "prompt_issue"
62
- validate_prompt_binding!(prompt_binding)
63
- digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(prompt_binding)))
64
- unless parameters_digest == digest && mutation_id == "prompt-issue.#{digest}" &&
65
- assignment_id == prompt_binding.fetch("assignment_id") && attempt_id == prompt_binding.fetch("attempt_id")
66
- raise ArgumentError, "prompt phase does not match source binding"
67
- end
68
- elsif prompt_binding
69
- raise ArgumentError, "prompt binding is only accepted by its fixed issue phase"
70
- end
71
- [assignment_id, attempt_id, mutation_id].each { |id| validate_mutation_id!(id) }
72
- unless parameters_digest.is_a?(String) && parameters_digest.match?(/\A[0-9a-f]{64}\z/)
73
- raise ArgumentError, "invalid mutation parameter digest"
74
- end
75
- raise ArgumentError, "replay authorizer must be source owned" if replay_authorizer && !replay_authorizer.respond_to?(:call)
76
- with_lock(deadline: deadline) do
77
- self.class::CAS_ATTEMPTS.times do
78
- old = ref_value
79
- ensure_checkout!
80
- old = ref_value if old.nil?
81
- sync_checkout(old)
82
- if prompt_binding
83
- verify_prompt_external_namespace!(prompt_binding.fetch("mutation_id"), parameters_digest,
84
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old)
85
- end
86
- verify_prompt_completion!(prompt_completion, mutation_id: mutation_id, digest: parameters_digest,
87
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if prompt_completion
88
- verify_prompt_observation!(prompt_observation, mutation_id: mutation_id,
89
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if prompt_observation
90
- verify_input_inhibition!(input_inhibition, mutation_id: mutation_id, digest: parameters_digest,
91
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if input_inhibition
92
- verify_prompt_mutation_namespace!(mutation_id: mutation_id, operation: operation, parameters_digest: parameters_digest,
93
- assignment_id: assignment_id, attempt_id: attempt_id, prompt_completion: prompt_completion, prompt_observation: prompt_observation, commit: old)
94
- replay = mutation_result(mutation_id, commit: old)
95
- if replay
96
- unless replay["operation"] == operation && replay["parameters_digest"] == parameters_digest &&
97
- replay["assignment_id"] == assignment_id && replay["attempt_id"] == attempt_id
98
- raise AttemptErrors::Conflict, "Mutation ID is already bound to different input"
99
- end
100
- result = replay.fetch("data").merge("journal_commit" => replay.fetch("journal_commit"))
101
- replay_authorizer&.call
102
- return with_replay ? {data: result, replayed: true} : result
103
- end
104
- current = read_events(assignment_id, commit: old).select { |event| event["attempt_id"] == attempt_id }
105
- generation = authority_generation(current)
106
- unless %i[recorded_completion prompt_completion prompt_observation input_inhibition].include?(generation_mode) || expected_generation == generation
107
- raise AttemptErrors::Conflict, "Authority registration generation changed"
108
- end
109
- plan = yield(current, old, generation)
110
- blobs = plan.fetch(:blobs, {})
111
- events = chain_mutation_events(attempt_id, current.last&.fetch("digest"), plan.fetch(:events, []))
112
- updates = prepare_service_updates(plan.fetch(:service_updates, []), assignment_id: assignment_id,
113
- attempt_id: attempt_id, pending: {current_events: current, pending_events: events, blobs: blobs, commit: old, operation: operation,
114
- service_inputs: service_input_context(plan)})
115
- events.concat(chain_mutation_events(attempt_id, events.last&.fetch("digest") || current.last&.fetch("digest"),
116
- updates.map { |prepared| prepared.fetch(:event) }))
117
- data = plan.fetch(:data).merge("generation" => generation + 1)
118
- receipt = Models::EvidenceEvent.build(type: "authority_mutation", attempt_id: attempt_id,
119
- previous_digest: events.last&.fetch("digest") || current.last&.fetch("digest"),
120
- payload: {"mutation_id" => mutation_id, "operation" => operation,
121
- "parameters_digest" => parameters_digest, "assignment_id" => assignment_id,
122
- "attempt_id" => attempt_id, "data" => data})
123
- events << receipt
124
- write_mutation_blobs(blobs, commit: old)
125
- write_service_records(updates)
126
- write_event_files(assignment_id, events)
127
- git!("-C", checkout_dir, "add", "--", "execution/#{assignment_id}")
128
- stage_mutation_blobs(blobs)
129
- stage_service_records(updates)
130
- git!("-C", checkout_dir, "-c", "user.name=ace-assign", "-c", "user.email=ace-assign@localhost",
131
- "-c", "core.hooksPath=/dev/null", "commit", "-m", "evidence: authority #{operation} #{mutation_id}")
132
- commit = git!("-C", checkout_dir, "rev-parse", "HEAD").first
133
- if update_ref_cas(commit, old)
134
- result = data.merge("journal_commit" => commit)
135
- return with_replay ? {data: result, replayed: false} : result
136
- end
137
- end
138
- raise AttemptErrors::EvidenceUnavailable, "Authority mutation ref stayed conflicting"
139
- end
140
- end
141
-
142
- def mutation_result(mutation_id, commit: ref_value)
143
- validate_mutation_id!(mutation_id)
144
- assignment_ids(commit: commit).each do |assignment_id|
145
- events = read_events(assignment_id, commit: commit)
146
- events.group_by { |event| event["attempt_id"] }.each_value do |chain|
147
- unless Models::EvidenceEvent.chain_valid?(chain)
148
- raise AttemptErrors::EvidenceUnavailable, "Authority mutation event chain is corrupt"
149
- end
150
- end
151
- event = events.find do |entry|
152
- entry["type"] == "authority_mutation" && entry.dig("payload", "mutation_id") == mutation_id
153
- end
154
- next unless event
155
-
156
- path = "execution/#{assignment_id}/events/#{event_filename(event)}"
157
- acceptance_commit = git!("log", "--diff-filter=A", "-1", "--format=%H", commit, "--", path).first
158
- raise AttemptErrors::EvidenceUnavailable, "Mutation acceptance commit is missing" if acceptance_commit.empty?
159
- return event.fetch("payload").merge("journal_commit" => acceptance_commit)
160
- end
161
- nil
162
- end
163
-
164
- # Source composition supplies original body bytes only for the current
165
- # service CAS callback. They are never part of persisted mutation data.
166
- def service_input_context(plan)
167
- inputs = plan.fetch(:service_inputs, {})
168
- raise ArgumentError, "invalid ephemeral service input context" unless inputs.is_a?(Hash)
169
- return {}.freeze if inputs.empty?
170
- updates = plan.fetch(:service_updates, [])
171
- unless inputs.size == 1 && updates.size == 1 && inputs.keys == [updates.first.fetch(:request_id)] &&
172
- inputs.values.first.is_a?(String) && inputs.values.first.bytesize.between?(1, 64 * 1024)
173
- raise ArgumentError, "invalid ephemeral service input context"
174
- end
175
- inputs.to_h { |key, bytes| [key.dup.freeze, bytes.dup.freeze] }.freeze
176
- end
177
- private :service_input_context
178
-
179
- # Read immutable bytes directly from a selected canonical ref commit.
180
- # Worktree projections are never consulted for protected evidence.
181
- def blob(path, commit: ref_value)
182
- validate_blob_path!(path)
183
- raise AttemptErrors::EvidenceUnavailable, "Canonical evidence ref is missing" unless commit
184
- return @read_boundary.blob(path, commit: commit, max_bytes: CanonicalEvidence::MAX_ARTIFACT_BYTES) if @read_boundary
185
- out, _error, status = Open3.capture3("git", "show", "#{commit}:#{path}",
186
- chdir: repo_root, stdin_data: "")
187
- raise AttemptErrors::EvidenceUnavailable, "Canonical evidence blob is missing" unless status.success?
188
- out.b
189
- end
190
-
191
- # Prepared inputs use the same immutable Git owner, with a bounded
192
- # capture before any bytes can reach a worker transport.
193
- def bounded_blob(path, commit:, max_bytes:)
194
- validate_blob_path!(path)
195
- unless commit.is_a?(String) && commit.match?(/\A[0-9a-f]{40}(?:[0-9a-f]{24})?\z/) &&
196
- max_bytes.is_a?(Integer) && max_bytes.between?(1, Atoms::CandidateLimits::BYTES)
197
- raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob selector differs"
198
- end
199
- return @read_boundary.blob(path, commit: commit, max_bytes: max_bytes) if @read_boundary
200
- environment = ENV.keys.to_h { |key| [key, nil] }
201
- environment.merge!("GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_GLOBAL" => "/dev/null",
202
- "GIT_TERMINAL_PROMPT" => "0", "GIT_NO_REPLACE_OBJECTS" => "1", "PATH" => "/usr/bin:/bin", "LC_ALL" => "C")
203
- result = Herdr::Molecules::BoundedProcess.call([environment, "/usr/bin/git", "-C", repo_root,
204
- "-c", "core.hooksPath=/dev/null", "cat-file", "blob", "#{commit}:#{path}"],
205
- stdin_data: "", timeout_s: 30, output_limit: max_bytes)
206
- unless result.status.success? && !result.oversized
207
- raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob is missing or oversized"
208
- end
209
- result.stdout.b
210
- rescue Timeout::Error, Herdr::Molecules::BoundedProcess::PostLaunchError, SystemCallError, IOError
211
- raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob read is unavailable"
212
- end
213
-
214
- private
215
-
216
- def validate_mutation_id!(id)
217
- raise ArgumentError, "invalid authority ID" unless id.is_a?(String) && id.match?(ID)
218
- end
219
-
220
- def validate_blob_path!(path)
221
- unless path.is_a?(String) && path.match?(%r{\A(?:evidence/imports|execution/definitions|execution/prepared|execution/merge-evidence|candidates/bundles)/[a-zA-Z0-9_.-]+\z}) &&
222
- !%w[. ..].include?(File.basename(path))
223
- raise ArgumentError, "invalid canonical blob reference"
224
- end
225
- end
226
-
227
- def chain_mutation_events(attempt_id, previous, entries)
228
- entries.map do |entry|
229
- recorded_at = entry.fetch(:recorded_at) { Time.now.utc }
230
- raise ArgumentError, "internal event timestamp must be a UTC Time" unless recorded_at.is_a?(Time) && recorded_at.utc?
231
- event = Models::EvidenceEvent.build(type: entry.fetch(:type), attempt_id: attempt_id,
232
- payload: entry.fetch(:payload), previous_digest: previous, recorded_at: recorded_at)
233
- previous = event.fetch("digest")
234
- event
235
- end
236
- end
237
-
238
- # Git clean/smudge filters and CRLF conversion cannot redefine accepted
239
- # evidence bytes. Insert unfiltered objects after staging the events.
240
- def stage_mutation_blobs(blobs)
241
- blobs.each do |path, bytes|
242
- oid, error, status = Open3.capture3("git", "hash-object", "-w", "--stdin", "--no-filters",
243
- chdir: repo_root, stdin_data: bytes.b)
244
- unless status.success?
245
- raise AttemptErrors::EvidenceUnavailable, "Cannot write canonical evidence object: #{error.strip}"
246
- end
247
- git!("-C", checkout_dir, "update-index", "--add", "--cacheinfo", "100644,#{oid.strip},#{path}")
248
- end
249
- end
250
-
251
- def write_mutation_blobs(blobs, commit:)
252
- blobs.map do |path, bytes|
253
- validate_blob_path!(path)
254
- raise ArgumentError, "canonical blob must contain bytes" unless bytes.is_a?(String)
255
- target = File.join(checkout_dir, path)
256
- if File.exist?(target)
257
- # A checkout may have smudged the file; immutability is checked
258
- # against accepted bytes at the canonical commit, not projection.
259
- unless blob(path, commit: commit) == bytes.b
260
- raise AttemptErrors::Conflict, "Canonical evidence blob is immutable"
261
- end
262
- else
263
- FileUtils.mkdir_p(File.dirname(target))
264
- File.open(target, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) do |file|
265
- file.write(bytes.b)
266
- end
267
- end
268
- path
269
- end
270
- end
271
- end
272
- end
273
- end
274
- end
@@ -1,241 +0,0 @@
1
- # frozen_string_literal: true
2
- require "ace/herdr/molecules/guarded_native_origin"
3
-
4
- module Ace
5
- module Assign
6
- module Molecules
7
- # Durable N2 phases in the existing canonical journal, never an effect ledger.
8
- module JournalPromptMutation
9
- # Source-only phase: commit intent before the caller obtains any
10
- # native dispatch permit. The scope owner reauthenticates inside this
11
- # side-effect-free callback on EVERY journal CAS attempt.
12
- def issue_prompt(binding:, issued_by:, &admission)
13
- validate_prompt_binding!(binding)
14
- unless issued_by.is_a?(Hash) && issued_by.keys.all? { |key| key.is_a?(String) } &&
15
- issued_by.keys.sort == %w[gid groups host parent_pid pid started_at uid] &&
16
- %w[uid gid groups].all? { |key| issued_by[key] == binding.fetch("caller").fetch(key) } &&
17
- %w[pid parent_pid].all? { |key| issued_by[key].is_a?(Integer) && issued_by[key].positive? } &&
18
- %w[host started_at].all? { |key| issued_by[key].is_a?(String) && issued_by[key].bytesize.between?(1, 255) }
19
- raise ArgumentError, "invalid original prompt issuer evidence"
20
- end
21
- binding = JSON.parse(JSON.generate(binding))
22
- issued_by = JSON.parse(JSON.generate(issued_by))
23
- raise ArgumentError, "prompt admission callback is required" unless admission
24
- digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(binding)))
25
- internal_id = "prompt-issue.#{digest}"
26
- response = mutate(assignment_id: binding.fetch("assignment_id"), attempt_id: binding.fetch("attempt_id"),
27
- mutation_id: internal_id, operation: "prompt_issue", parameters_digest: digest,
28
- expected_generation: binding.fetch("expected_generation"), with_replay: true, prompt_binding: binding) do |events, commit, generation|
29
- admission.call(events, commit, generation)
30
- {events: [{type: "prompt_issued", payload: {"external_mutation_id" => binding.fetch("mutation_id"),
31
- "binding_digest" => digest, "binding" => binding, "issued_by" => issued_by}}],
32
- data: {"mutation_id" => binding.fetch("mutation_id"), "binding_digest" => digest, "intent_mutation_id" => internal_id}}
33
- end
34
- response
35
- end
36
-
37
- # Fixed source-owned completion. Its selector authenticates the actual
38
- # accepted issue on every CAS; the callback rejoins the original scope
39
- # and guard without performing an effect. Replay keeps the FIRST public
40
- # reply even if a later observation has a different outcome.
41
- def finalize_prompt(mutation_id:, intent_event_id:, binding_digest:, evidence:, &authentication)
42
- raise ArgumentError, "prompt completion authentication is required" unless authentication
43
- intent = prompt_intent(mutation_id)
44
- raise AttemptErrors::EvidenceUnavailable, "Prompt issue is unavailable" unless intent
45
- binding = intent.fetch("payload").fetch("binding")
46
- selector = {"intent_event_id" => intent_event_id, "binding_digest" => binding_digest}
47
- mutate(assignment_id: binding.fetch("assignment_id"), attempt_id: binding.fetch("attempt_id"),
48
- mutation_id: mutation_id, operation: "prompt_attempt", parameters_digest: binding_digest,
49
- expected_generation: nil, generation_mode: :prompt_completion, prompt_completion: selector, with_replay: true) do |events, commit, generation|
50
- if events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
51
- raise AttemptErrors::EvidenceUnavailable, "Released prompt scope cannot accept new outcome"
52
- end
53
- original = authentication.call(events, commit, generation)
54
- unless original.is_a?(Hash) && original.keys.sort == %w[binding_digest origin] &&
55
- original["binding_digest"] == binding.fetch("original_binding_digest")
56
- raise AttemptErrors::EvidenceUnavailable, "Original prompt scope binding differs"
57
- end
58
- validate_prompt_evidence!(evidence, original.fetch("origin"))
59
- {events: [{type: "prompt_outcome", payload: {"external_mutation_id" => mutation_id,
60
- "intent_event_id" => intent_event_id, "binding_digest" => binding_digest, "evidence" => evidence}}],
61
- data: {"mutation_id" => mutation_id, "attempt_id" => binding.fetch("attempt_id"),
62
- "intent_event_id" => intent_event_id, "outcome" => evidence.fetch("outcome"), "guarded_evidence" => evidence}}
63
- end
64
- end
65
-
66
- def verify_prompt_completion!(selector, mutation_id:, digest:, assignment_id:, attempt_id:, commit:)
67
- unless selector.is_a?(Hash) && selector.keys.all? { |key| key.is_a?(String) } && selector.keys.sort == %w[binding_digest intent_event_id] &&
68
- selector.values.all? { |value| value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/) }
69
- raise ArgumentError, "invalid fixed prompt completion selector"
70
- end
71
- intent = prompt_intent(mutation_id, commit: commit)
72
- unless intent && intent["digest"] == selector.fetch("intent_event_id") &&
73
- intent.dig("payload", "binding_digest") == digest && selector.fetch("binding_digest") == digest &&
74
- intent.dig("payload", "binding", "assignment_id") == assignment_id && intent["attempt_id"] == attempt_id
75
- raise AttemptErrors::EvidenceUnavailable, "Prompt completion does not join accepted issue"
76
- end
77
- true
78
- end
79
- private :verify_prompt_completion!
80
-
81
- # Later authenticated native completion is a separate canonical fact.
82
- # It never replaces the first immutable public prompt reply.
83
- def observe_prompt_completion(mutation_id:, intent_event_id:, binding_digest:, evidence:, &authentication)
84
- raise ArgumentError, "prompt observation authentication is required" unless authentication
85
- intent = prompt_intent(mutation_id)
86
- raise AttemptErrors::EvidenceUnavailable, "Prompt issue is unavailable" unless intent
87
- binding = intent.fetch("payload").fetch("binding")
88
- selector = {"external_mutation_id" => mutation_id, "intent_event_id" => intent_event_id, "binding_digest" => binding_digest}
89
- digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(selector.merge("evidence" => evidence))))
90
- mutate(assignment_id: binding.fetch("assignment_id"), attempt_id: binding.fetch("attempt_id"),
91
- mutation_id: "prompt-observe.#{intent_event_id}", operation: "prompt_observation", parameters_digest: digest,
92
- expected_generation: nil, generation_mode: :prompt_observation, prompt_observation: selector, with_replay: true) do |events, commit, generation|
93
- if events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
94
- raise AttemptErrors::EvidenceUnavailable, "Released prompt scope cannot accept new observation"
95
- end
96
- original = authentication.call(events, commit, generation)
97
- unless original.is_a?(Hash) && original.keys.sort == %w[binding_digest origin] && original["binding_digest"] == binding.fetch("original_binding_digest")
98
- raise AttemptErrors::EvidenceUnavailable, "Original prompt scope binding differs"
99
- end
100
- validate_prompt_evidence!(evidence, original.fetch("origin"))
101
- unless %w[submitted not_issued].include?(evidence.fetch("outcome"))
102
- raise AttemptErrors::EvidenceUnavailable, "Unknown native outcome is not completed evidence"
103
- end
104
- {events: [{type: "prompt_completion_observed", payload: selector.merge("evidence" => evidence)}],
105
- data: selector.merge("outcome" => evidence.fetch("outcome"), "guarded_evidence" => evidence)}
106
- end
107
- end
108
-
109
- def verify_prompt_observation!(selector, mutation_id:, assignment_id:, attempt_id:, commit:)
110
- unless selector.is_a?(Hash) && selector.keys.sort == %w[binding_digest external_mutation_id intent_event_id] &&
111
- mutation_id == "prompt-observe.#{selector["intent_event_id"]}"
112
- raise ArgumentError, "invalid fixed prompt observation selector"
113
- end
114
- verify_prompt_completion!(selector.slice("intent_event_id", "binding_digest"),
115
- mutation_id: selector.fetch("external_mutation_id"), digest: selector.fetch("binding_digest"),
116
- assignment_id: assignment_id, attempt_id: attempt_id, commit: commit)
117
- end
118
- private :verify_prompt_observation!
119
-
120
- def validate_prompt_evidence!(evidence, original)
121
- unless original.is_a?(Hash) && evidence.is_a?(Hash) && evidence.keys.all? { |key| key.is_a?(String) } && evidence["origin"] == original
122
- raise AttemptErrors::EvidenceUnavailable, "Prompt outcome original guard differs"
123
- end
124
- Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(original, terminal_id: original.fetch("terminal_id"), child: original.fetch("child"))
125
- Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(evidence.fetch("origin"), terminal_id: original.fetch("terminal_id"), child: original.fetch("child"))
126
- case evidence["outcome"]
127
- when "submitted"
128
- valid = evidence.keys.sort == %w[origin outcome submission] && evidence["submission"] == "submitted"
129
- when "not_issued"
130
- valid = evidence.keys.sort == %w[code origin outcome phase] && evidence["phase"] == "not_issued" &&
131
- %w[guard_mismatch origin_unavailable origin_exited agent_blocked agent_not_ready target_missing submission_busy].include?(evidence["code"])
132
- when "uncertain"
133
- valid = evidence.keys.sort == %w[origin outcome]
134
- end
135
- raise AttemptErrors::EvidenceUnavailable, "Prompt outcome is malformed" unless valid
136
- true
137
- rescue KeyError, TypeError, Ace::Runtime::RuntimeUnavailableError
138
- raise AttemptErrors::EvidenceUnavailable, "Prompt outcome original guard is malformed"
139
- end
140
- private :validate_prompt_evidence!
141
-
142
- # A read-only selected canonical chain lookup, never a dispatch permit.
143
- def prompt_intent(mutation_id, commit: ref_value)
144
- validate_mutation_id!(mutation_id)
145
- found = nil
146
- assignment_ids(commit: commit).each do |assignment_id|
147
- events = read_events(assignment_id, commit: commit)
148
- events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
149
- unless Models::EvidenceEvent.chain_valid?(chain)
150
- raise AttemptErrors::EvidenceUnavailable, "Prompt intent event chain is corrupt"
151
- end
152
- end
153
- events.each do |event|
154
- next unless event["type"] == "prompt_issued" && event.dig("payload", "external_mutation_id") == mutation_id
155
- raise AttemptErrors::EvidenceUnavailable, "Prompt intent is ambiguous" if found
156
- payload = event.fetch("payload")
157
- validate_prompt_binding!(payload.fetch("binding"))
158
- digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(payload.fetch("binding"))))
159
- binding = payload.fetch("binding")
160
- acceptance = events.find { |entry| entry["type"] == "authority_mutation" &&
161
- entry.dig("payload", "mutation_id") == "prompt-issue.#{digest}" &&
162
- entry.dig("payload", "operation") == "prompt_issue" &&
163
- entry.dig("payload", "parameters_digest") == digest && entry["previous_digest"] == event["digest"] }
164
- unless acceptance && payload["binding_digest"] == digest && binding["mutation_id"] == mutation_id &&
165
- binding["assignment_id"] == assignment_id && binding["attempt_id"] == event["attempt_id"]
166
- raise AttemptErrors::EvidenceUnavailable, "Prompt intent acceptance binding differs"
167
- end
168
- found = event
169
- end
170
- end
171
- found
172
- rescue KeyError, TypeError, ArgumentError
173
- raise AttemptErrors::EvidenceUnavailable, "Prompt intent is malformed"
174
- end
175
-
176
- def validate_prompt_binding!(binding)
177
- fields = %w[action assignment_id attempt_id caller expected_generation mapping_id mutation_id original_binding_digest project_id text_bytes text_sha256]
178
- unless binding.is_a?(Hash) && binding.keys.all? { |key| key.is_a?(String) } && binding.keys.sort == fields &&
179
- binding["action"] == "prompt_attempt" && binding["expected_generation"].is_a?(Integer) && binding["expected_generation"] >= 0 &&
180
- binding["text_bytes"].is_a?(Integer) && binding["text_bytes"].between?(1, 16_384) &&
181
- %w[text_sha256 original_binding_digest].all? { |key| binding[key].is_a?(String) && binding[key].match?(/\A[0-9a-f]{64}\z/) }
182
- raise ArgumentError, "invalid fixed prompt binding"
183
- end
184
- %w[assignment_id attempt_id mapping_id mutation_id project_id].each { |key| validate_mutation_id!(binding.fetch(key)) }
185
- if binding.fetch("mutation_id").start_with?("prompt-issue.", "prompt-observe.", "input-inhibit.")
186
- raise ArgumentError, "public mutation ID uses reserved prompt namespace"
187
- end
188
- caller = binding.fetch("caller")
189
- unless caller.is_a?(Hash) && caller.keys.all? { |key| key.is_a?(String) } && caller.keys.sort == %w[gid groups role uid] &&
190
- %w[launcher supervisor].include?(caller["role"]) &&
191
- %w[uid gid].all? { |key| caller[key].is_a?(Integer) && caller[key].between?(0, 0xffff_ffff) } &&
192
- caller["groups"].is_a?(Array) && caller["groups"].size <= 256 &&
193
- caller["groups"].all? { |group| group.is_a?(Integer) && group.between?(0, 0xffff_ffff) } &&
194
- caller["groups"] == caller["groups"].sort.uniq
195
- raise ArgumentError, "invalid authenticated stable prompt principal"
196
- end
197
- true
198
- end
199
- private :validate_prompt_binding!
200
-
201
- def canonical_prompt_value(value)
202
- case value
203
- when Hash then value.keys.sort.to_h { |key| [key, canonical_prompt_value(value.fetch(key))] }
204
- when Array then value.map { |item| canonical_prompt_value(item) }
205
- else value
206
- end
207
- end
208
- private :canonical_prompt_value
209
-
210
- def verify_prompt_external_namespace!(external_id, digest, assignment_id:, attempt_id:, commit:)
211
- intent = prompt_intent(external_id, commit: commit)
212
- accepted = mutation_result(external_id, commit: commit)
213
- if accepted || (intent && (intent.dig("payload", "binding_digest") != digest ||
214
- intent.dig("payload", "binding", "assignment_id") != assignment_id || intent["attempt_id"] != attempt_id))
215
- raise AttemptErrors::Conflict, "Mutation ID is already bound to different input"
216
- end
217
- end
218
- private :verify_prompt_external_namespace!
219
-
220
- def verify_prompt_mutation_namespace!(mutation_id:, operation:, parameters_digest:, assignment_id:, attempt_id:, prompt_completion:, prompt_observation: nil, commit:)
221
- # Ordinary mutations cannot steal an external ID while its accepted
222
- # prompt issue has no finalized public reply yet.
223
- if mutation_id.start_with?("prompt-issue.")
224
- raise ArgumentError, "reserved internal prompt namespace" unless operation == "prompt_issue"
225
- return
226
- end
227
- if mutation_id.start_with?("prompt-observe.")
228
- raise ArgumentError, "reserved internal prompt observation namespace" unless operation == "prompt_observation" && prompt_observation
229
- return
230
- end
231
- return if operation == "prompt_attempt" && prompt_completion
232
- if prompt_intent(mutation_id, commit: commit)
233
- raise AttemptErrors::Conflict, "Mutation ID is reserved by accepted prompt intent"
234
- end
235
- end
236
- private :verify_prompt_mutation_namespace!
237
-
238
- end
239
- end
240
- end
241
- end