ace-assign 0.70.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +3 -2099
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,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,273 +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 "journal_input_inhibition"
8
-
9
- module Ace
10
- module Assign
11
- module Molecules
12
- # Protected authority mutations share the execution ref's lock and CAS.
13
- # Replies live in chained attempt events, rather than an RPC ledger.
14
- module JournalMutation
15
- include JournalPromptMutation
16
- include JournalInputInhibition
17
- ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
18
-
19
- # Shared canonical projection used by admission and protected status.
20
- # Callers hold the same owner exclusion when projecting current events.
21
- def authority_generation(events)
22
- unless Models::EvidenceEvent.chain_valid?(events)
23
- raise AttemptErrors::EvidenceUnavailable, "Attempt event chain is corrupt"
24
- end
25
- events.count { |event| event["type"] == "authority_mutation" }
26
- end
27
-
28
- def canonical_attempt_state(events)
29
- authority_generation(events)
30
- derive_state(events)
31
- end
32
-
33
- # The block runs against the current ref on every CAS attempt. It
34
- # returns events [{type:, payload:}], immutable blobs, and public data.
35
- # Only the authority calls this internal journal API; wire requests
36
- # cannot select blob paths, event types, or accepted response data.
37
- 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)
38
- unless (generation_mode == :expected && expected_generation.is_a?(Integer) && expected_generation >= 0) ||
39
- (generation_mode == :recorded_completion && %w[complete_service complete_no_effect].include?(operation) && expected_generation.nil?) ||
40
- (generation_mode == :prompt_completion && operation == "prompt_attempt" && expected_generation.nil? && prompt_completion.is_a?(Hash)) ||
41
- (generation_mode == :prompt_observation && operation == "prompt_observation" && expected_generation.nil? && prompt_observation.is_a?(Hash)) ||
42
- (generation_mode == :input_inhibition && operation == "input_inhibition_observation" && expected_generation.nil? && input_inhibition.is_a?(Hash))
43
- raise ArgumentError, "invalid fixed mutation generation mode"
44
- end
45
- if input_inhibition && generation_mode != :input_inhibition
46
- raise ArgumentError, "Input inhibition requires its fixed mode"
47
- end
48
- if operation == "input_inhibition_observation" && !(generation_mode == :input_inhibition && input_inhibition)
49
- raise ArgumentError, "Input inhibition observation requires its accepted original selector"
50
- end
51
- if mutation_id.is_a?(String) && mutation_id.start_with?("input-inhibit.") && !(operation == "input_inhibition_observation" && input_inhibition)
52
- raise ArgumentError, "Reserved input inhibition namespace"
53
- end
54
- if prompt_completion && generation_mode != :prompt_completion
55
- raise ArgumentError, "prompt completion selector requires its fixed mode"
56
- end
57
- if prompt_observation && generation_mode != :prompt_observation
58
- raise ArgumentError, "prompt observation selector requires its fixed mode"
59
- end
60
- if operation == "prompt_issue"
61
- validate_prompt_binding!(prompt_binding)
62
- digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(prompt_binding)))
63
- unless parameters_digest == digest && mutation_id == "prompt-issue.#{digest}" &&
64
- assignment_id == prompt_binding.fetch("assignment_id") && attempt_id == prompt_binding.fetch("attempt_id")
65
- raise ArgumentError, "prompt phase does not match source binding"
66
- end
67
- elsif prompt_binding
68
- raise ArgumentError, "prompt binding is only accepted by its fixed issue phase"
69
- end
70
- [assignment_id, attempt_id, mutation_id].each { |id| validate_mutation_id!(id) }
71
- unless parameters_digest.is_a?(String) && parameters_digest.match?(/\A[0-9a-f]{64}\z/)
72
- raise ArgumentError, "invalid mutation parameter digest"
73
- end
74
- raise ArgumentError, "replay authorizer must be source owned" if replay_authorizer && !replay_authorizer.respond_to?(:call)
75
- with_lock(deadline: deadline) do
76
- self.class::CAS_ATTEMPTS.times do
77
- old = ref_value
78
- ensure_checkout!
79
- old = ref_value if old.nil?
80
- sync_checkout(old)
81
- if prompt_binding
82
- verify_prompt_external_namespace!(prompt_binding.fetch("mutation_id"), parameters_digest,
83
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old)
84
- end
85
- verify_prompt_completion!(prompt_completion, mutation_id: mutation_id, digest: parameters_digest,
86
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if prompt_completion
87
- verify_prompt_observation!(prompt_observation, mutation_id: mutation_id,
88
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if prompt_observation
89
- verify_input_inhibition!(input_inhibition, mutation_id: mutation_id, digest: parameters_digest,
90
- assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if input_inhibition
91
- verify_prompt_mutation_namespace!(mutation_id: mutation_id, operation: operation, parameters_digest: parameters_digest,
92
- assignment_id: assignment_id, attempt_id: attempt_id, prompt_completion: prompt_completion, prompt_observation: prompt_observation, commit: old)
93
- replay = mutation_result(mutation_id, commit: old)
94
- if replay
95
- unless replay["operation"] == operation && replay["parameters_digest"] == parameters_digest &&
96
- replay["assignment_id"] == assignment_id && replay["attempt_id"] == attempt_id
97
- raise AttemptErrors::Conflict, "Mutation ID is already bound to different input"
98
- end
99
- result = replay.fetch("data").merge("journal_commit" => replay.fetch("journal_commit"))
100
- replay_authorizer&.call
101
- return with_replay ? {data: result, replayed: true} : result
102
- end
103
- current = read_events(assignment_id, commit: old).select { |event| event["attempt_id"] == attempt_id }
104
- generation = authority_generation(current)
105
- unless %i[recorded_completion prompt_completion prompt_observation input_inhibition].include?(generation_mode) || expected_generation == generation
106
- raise AttemptErrors::Conflict, "Authority registration generation changed"
107
- end
108
- plan = yield(current, old, generation)
109
- blobs = plan.fetch(:blobs, {})
110
- events = chain_mutation_events(attempt_id, current.last&.fetch("digest"), plan.fetch(:events, []))
111
- updates = prepare_service_updates(plan.fetch(:service_updates, []), assignment_id: assignment_id,
112
- attempt_id: attempt_id, pending: {current_events: current, pending_events: events, blobs: blobs, commit: old, operation: operation,
113
- service_inputs: service_input_context(plan)})
114
- events.concat(chain_mutation_events(attempt_id, events.last&.fetch("digest") || current.last&.fetch("digest"),
115
- updates.map { |prepared| prepared.fetch(:event) }))
116
- data = plan.fetch(:data).merge("generation" => generation + 1)
117
- receipt = Models::EvidenceEvent.build(type: "authority_mutation", attempt_id: attempt_id,
118
- previous_digest: events.last&.fetch("digest") || current.last&.fetch("digest"),
119
- payload: {"mutation_id" => mutation_id, "operation" => operation,
120
- "parameters_digest" => parameters_digest, "assignment_id" => assignment_id,
121
- "attempt_id" => attempt_id, "data" => data})
122
- events << receipt
123
- write_mutation_blobs(blobs, commit: old)
124
- write_service_records(updates)
125
- write_event_files(assignment_id, events)
126
- git!("-C", checkout_dir, "add", "--", "execution/#{assignment_id}")
127
- stage_mutation_blobs(blobs)
128
- stage_service_records(updates)
129
- git!("-C", checkout_dir, "-c", "user.name=ace-assign", "-c", "user.email=ace-assign@localhost",
130
- "-c", "core.hooksPath=/dev/null", "commit", "-m", "evidence: authority #{operation} #{mutation_id}")
131
- commit = git!("-C", checkout_dir, "rev-parse", "HEAD").first
132
- if update_ref_cas(commit, old)
133
- result = data.merge("journal_commit" => commit)
134
- return with_replay ? {data: result, replayed: false} : result
135
- end
136
- end
137
- raise AttemptErrors::EvidenceUnavailable, "Authority mutation ref stayed conflicting"
138
- end
139
- end
140
-
141
- def mutation_result(mutation_id, commit: ref_value)
142
- validate_mutation_id!(mutation_id)
143
- assignment_ids(commit: commit).each do |assignment_id|
144
- events = read_events(assignment_id, commit: commit)
145
- events.group_by { |event| event["attempt_id"] }.each_value do |chain|
146
- unless Models::EvidenceEvent.chain_valid?(chain)
147
- raise AttemptErrors::EvidenceUnavailable, "Authority mutation event chain is corrupt"
148
- end
149
- end
150
- event = events.find do |entry|
151
- entry["type"] == "authority_mutation" && entry.dig("payload", "mutation_id") == mutation_id
152
- end
153
- next unless event
154
-
155
- path = "execution/#{assignment_id}/events/#{event_filename(event)}"
156
- acceptance_commit = git!("log", "--diff-filter=A", "-1", "--format=%H", commit, "--", path).first
157
- raise AttemptErrors::EvidenceUnavailable, "Mutation acceptance commit is missing" if acceptance_commit.empty?
158
- return event.fetch("payload").merge("journal_commit" => acceptance_commit)
159
- end
160
- nil
161
- end
162
-
163
- # Source composition supplies original body bytes only for the current
164
- # service CAS callback. They are never part of persisted mutation data.
165
- def service_input_context(plan)
166
- inputs = plan.fetch(:service_inputs, {})
167
- raise ArgumentError, "invalid ephemeral service input context" unless inputs.is_a?(Hash)
168
- return {}.freeze if inputs.empty?
169
- updates = plan.fetch(:service_updates, [])
170
- unless inputs.size == 1 && updates.size == 1 && inputs.keys == [updates.first.fetch(:request_id)] &&
171
- inputs.values.first.is_a?(String) && inputs.values.first.bytesize.between?(1, 64 * 1024)
172
- raise ArgumentError, "invalid ephemeral service input context"
173
- end
174
- inputs.to_h { |key, bytes| [key.dup.freeze, bytes.dup.freeze] }.freeze
175
- end
176
- private :service_input_context
177
-
178
- # Read immutable bytes directly from a selected canonical ref commit.
179
- # Worktree projections are never consulted for protected evidence.
180
- def blob(path, commit: ref_value)
181
- validate_blob_path!(path)
182
- raise AttemptErrors::EvidenceUnavailable, "Canonical evidence ref is missing" unless commit
183
- return @read_boundary.blob(path, commit: commit, max_bytes: CanonicalEvidence::MAX_ARTIFACT_BYTES) if @read_boundary
184
- out, _error, status = Open3.capture3("git", "show", "#{commit}:#{path}",
185
- chdir: repo_root, stdin_data: "")
186
- raise AttemptErrors::EvidenceUnavailable, "Canonical evidence blob is missing" unless status.success?
187
- out.b
188
- end
189
-
190
- # Prepared inputs use the same immutable Git owner, with a bounded
191
- # capture before any bytes can reach a worker transport.
192
- def bounded_blob(path, commit:, max_bytes:)
193
- validate_blob_path!(path)
194
- unless commit.is_a?(String) && commit.match?(/\A[0-9a-f]{40}(?:[0-9a-f]{24})?\z/) &&
195
- max_bytes.is_a?(Integer) && max_bytes.between?(1, 64 * 1024 * 1024)
196
- raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob selector differs"
197
- end
198
- return @read_boundary.blob(path, commit: commit, max_bytes: max_bytes) if @read_boundary
199
- environment = ENV.keys.to_h { |key| [key, nil] }
200
- environment.merge!("GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_GLOBAL" => "/dev/null",
201
- "GIT_TERMINAL_PROMPT" => "0", "GIT_NO_REPLACE_OBJECTS" => "1", "PATH" => "/usr/bin:/bin", "LC_ALL" => "C")
202
- result = Herdr::Molecules::BoundedProcess.call([environment, "/usr/bin/git", "-C", repo_root,
203
- "-c", "core.hooksPath=/dev/null", "cat-file", "blob", "#{commit}:#{path}"],
204
- stdin_data: "", timeout_s: 30, output_limit: max_bytes)
205
- unless result.status.success? && !result.oversized
206
- raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob is missing or oversized"
207
- end
208
- result.stdout.b
209
- rescue Timeout::Error, Herdr::Molecules::BoundedProcess::PostLaunchError, SystemCallError, IOError
210
- raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob read is unavailable"
211
- end
212
-
213
- private
214
-
215
- def validate_mutation_id!(id)
216
- raise ArgumentError, "invalid authority ID" unless id.is_a?(String) && id.match?(ID)
217
- end
218
-
219
- def validate_blob_path!(path)
220
- 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}) &&
221
- !%w[. ..].include?(File.basename(path))
222
- raise ArgumentError, "invalid canonical blob reference"
223
- end
224
- end
225
-
226
- def chain_mutation_events(attempt_id, previous, entries)
227
- entries.map do |entry|
228
- recorded_at = entry.fetch(:recorded_at) { Time.now.utc }
229
- raise ArgumentError, "internal event timestamp must be a UTC Time" unless recorded_at.is_a?(Time) && recorded_at.utc?
230
- event = Models::EvidenceEvent.build(type: entry.fetch(:type), attempt_id: attempt_id,
231
- payload: entry.fetch(:payload), previous_digest: previous, recorded_at: recorded_at)
232
- previous = event.fetch("digest")
233
- event
234
- end
235
- end
236
-
237
- # Git clean/smudge filters and CRLF conversion cannot redefine accepted
238
- # evidence bytes. Insert unfiltered objects after staging the events.
239
- def stage_mutation_blobs(blobs)
240
- blobs.each do |path, bytes|
241
- oid, error, status = Open3.capture3("git", "hash-object", "-w", "--stdin", "--no-filters",
242
- chdir: repo_root, stdin_data: bytes.b)
243
- unless status.success?
244
- raise AttemptErrors::EvidenceUnavailable, "Cannot write canonical evidence object: #{error.strip}"
245
- end
246
- git!("-C", checkout_dir, "update-index", "--add", "--cacheinfo", "100644,#{oid.strip},#{path}")
247
- end
248
- end
249
-
250
- def write_mutation_blobs(blobs, commit:)
251
- blobs.map do |path, bytes|
252
- validate_blob_path!(path)
253
- raise ArgumentError, "canonical blob must contain bytes" unless bytes.is_a?(String)
254
- target = File.join(checkout_dir, path)
255
- if File.exist?(target)
256
- # A checkout may have smudged the file; immutability is checked
257
- # against accepted bytes at the canonical commit, not projection.
258
- unless blob(path, commit: commit) == bytes.b
259
- raise AttemptErrors::Conflict, "Canonical evidence blob is immutable"
260
- end
261
- else
262
- FileUtils.mkdir_p(File.dirname(target))
263
- File.open(target, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) do |file|
264
- file.write(bytes.b)
265
- end
266
- end
267
- path
268
- end
269
- end
270
- end
271
- end
272
- end
273
- 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