ace-assign 0.71.0 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +5 -2116
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,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