ace-assign 0.63.1 → 0.66.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.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +398 -0
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +59 -0
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +286 -0
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +187 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +38 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +56 -4
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -0,0 +1,271 @@
|
|
|
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)
|
|
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
|
+
with_lock do
|
|
75
|
+
self.class::CAS_ATTEMPTS.times do
|
|
76
|
+
old = ref_value
|
|
77
|
+
ensure_checkout!
|
|
78
|
+
old = ref_value if old.nil?
|
|
79
|
+
sync_checkout(old)
|
|
80
|
+
if prompt_binding
|
|
81
|
+
verify_prompt_external_namespace!(prompt_binding.fetch("mutation_id"), parameters_digest,
|
|
82
|
+
assignment_id: assignment_id, attempt_id: attempt_id, commit: old)
|
|
83
|
+
end
|
|
84
|
+
verify_prompt_completion!(prompt_completion, mutation_id: mutation_id, digest: parameters_digest,
|
|
85
|
+
assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if prompt_completion
|
|
86
|
+
verify_prompt_observation!(prompt_observation, mutation_id: mutation_id,
|
|
87
|
+
assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if prompt_observation
|
|
88
|
+
verify_input_inhibition!(input_inhibition, mutation_id: mutation_id, digest: parameters_digest,
|
|
89
|
+
assignment_id: assignment_id, attempt_id: attempt_id, commit: old) if input_inhibition
|
|
90
|
+
verify_prompt_mutation_namespace!(mutation_id: mutation_id, operation: operation, parameters_digest: parameters_digest,
|
|
91
|
+
assignment_id: assignment_id, attempt_id: attempt_id, prompt_completion: prompt_completion, prompt_observation: prompt_observation, commit: old)
|
|
92
|
+
replay = mutation_result(mutation_id, commit: old)
|
|
93
|
+
if replay
|
|
94
|
+
unless replay["operation"] == operation && replay["parameters_digest"] == parameters_digest &&
|
|
95
|
+
replay["assignment_id"] == assignment_id && replay["attempt_id"] == attempt_id
|
|
96
|
+
raise AttemptErrors::Conflict, "Mutation ID is already bound to different input"
|
|
97
|
+
end
|
|
98
|
+
result = replay.fetch("data").merge("journal_commit" => replay.fetch("journal_commit"))
|
|
99
|
+
return with_replay ? {data: result, replayed: true} : result
|
|
100
|
+
end
|
|
101
|
+
current = read_events(assignment_id, commit: old).select { |event| event["attempt_id"] == attempt_id }
|
|
102
|
+
generation = authority_generation(current)
|
|
103
|
+
unless %i[recorded_completion prompt_completion prompt_observation input_inhibition].include?(generation_mode) || expected_generation == generation
|
|
104
|
+
raise AttemptErrors::Conflict, "Authority registration generation changed"
|
|
105
|
+
end
|
|
106
|
+
plan = yield(current, old, generation)
|
|
107
|
+
blobs = plan.fetch(:blobs, {})
|
|
108
|
+
events = chain_mutation_events(attempt_id, current.last&.fetch("digest"), plan.fetch(:events, []))
|
|
109
|
+
updates = prepare_service_updates(plan.fetch(:service_updates, []), assignment_id: assignment_id,
|
|
110
|
+
attempt_id: attempt_id, pending: {current_events: current, pending_events: events, blobs: blobs, commit: old, operation: operation,
|
|
111
|
+
service_inputs: service_input_context(plan)})
|
|
112
|
+
events.concat(chain_mutation_events(attempt_id, events.last&.fetch("digest") || current.last&.fetch("digest"),
|
|
113
|
+
updates.map { |prepared| prepared.fetch(:event) }))
|
|
114
|
+
data = plan.fetch(:data).merge("generation" => generation + 1)
|
|
115
|
+
receipt = Models::EvidenceEvent.build(type: "authority_mutation", attempt_id: attempt_id,
|
|
116
|
+
previous_digest: events.last&.fetch("digest") || current.last&.fetch("digest"),
|
|
117
|
+
payload: {"mutation_id" => mutation_id, "operation" => operation,
|
|
118
|
+
"parameters_digest" => parameters_digest, "assignment_id" => assignment_id,
|
|
119
|
+
"attempt_id" => attempt_id, "data" => data})
|
|
120
|
+
events << receipt
|
|
121
|
+
write_mutation_blobs(blobs, commit: old)
|
|
122
|
+
write_service_records(updates)
|
|
123
|
+
write_event_files(assignment_id, events)
|
|
124
|
+
git!("-C", checkout_dir, "add", "--", "execution/#{assignment_id}")
|
|
125
|
+
stage_mutation_blobs(blobs)
|
|
126
|
+
stage_service_records(updates)
|
|
127
|
+
git!("-C", checkout_dir, "-c", "user.name=ace-assign", "-c", "user.email=ace-assign@localhost",
|
|
128
|
+
"-c", "core.hooksPath=/dev/null", "commit", "-m", "evidence: authority #{operation} #{mutation_id}")
|
|
129
|
+
commit = git!("-C", checkout_dir, "rev-parse", "HEAD").first
|
|
130
|
+
if update_ref_cas(commit, old)
|
|
131
|
+
result = data.merge("journal_commit" => commit)
|
|
132
|
+
return with_replay ? {data: result, replayed: false} : result
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
raise AttemptErrors::EvidenceUnavailable, "Authority mutation ref stayed conflicting"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def mutation_result(mutation_id, commit: ref_value)
|
|
140
|
+
validate_mutation_id!(mutation_id)
|
|
141
|
+
assignment_ids(commit: commit).each do |assignment_id|
|
|
142
|
+
events = read_events(assignment_id, commit: commit)
|
|
143
|
+
events.group_by { |event| event["attempt_id"] }.each_value do |chain|
|
|
144
|
+
unless Models::EvidenceEvent.chain_valid?(chain)
|
|
145
|
+
raise AttemptErrors::EvidenceUnavailable, "Authority mutation event chain is corrupt"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
event = events.find do |entry|
|
|
149
|
+
entry["type"] == "authority_mutation" && entry.dig("payload", "mutation_id") == mutation_id
|
|
150
|
+
end
|
|
151
|
+
next unless event
|
|
152
|
+
|
|
153
|
+
path = "execution/#{assignment_id}/events/#{event_filename(event)}"
|
|
154
|
+
acceptance_commit = git!("log", "--diff-filter=A", "-1", "--format=%H", commit, "--", path).first
|
|
155
|
+
raise AttemptErrors::EvidenceUnavailable, "Mutation acceptance commit is missing" if acceptance_commit.empty?
|
|
156
|
+
return event.fetch("payload").merge("journal_commit" => acceptance_commit)
|
|
157
|
+
end
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Source composition supplies original body bytes only for the current
|
|
162
|
+
# service CAS callback. They are never part of persisted mutation data.
|
|
163
|
+
def service_input_context(plan)
|
|
164
|
+
inputs = plan.fetch(:service_inputs, {})
|
|
165
|
+
raise ArgumentError, "invalid ephemeral service input context" unless inputs.is_a?(Hash)
|
|
166
|
+
return {}.freeze if inputs.empty?
|
|
167
|
+
updates = plan.fetch(:service_updates, [])
|
|
168
|
+
unless inputs.size == 1 && updates.size == 1 && inputs.keys == [updates.first.fetch(:request_id)] &&
|
|
169
|
+
inputs.values.first.is_a?(String) && inputs.values.first.bytesize.between?(1, 64 * 1024)
|
|
170
|
+
raise ArgumentError, "invalid ephemeral service input context"
|
|
171
|
+
end
|
|
172
|
+
inputs.to_h { |key, bytes| [key.dup.freeze, bytes.dup.freeze] }.freeze
|
|
173
|
+
end
|
|
174
|
+
private :service_input_context
|
|
175
|
+
|
|
176
|
+
# Read immutable bytes directly from a selected canonical ref commit.
|
|
177
|
+
# Worktree projections are never consulted for protected evidence.
|
|
178
|
+
def blob(path, commit: ref_value)
|
|
179
|
+
validate_blob_path!(path)
|
|
180
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical evidence ref is missing" unless commit
|
|
181
|
+
return @read_boundary.blob(path, commit: commit, max_bytes: CanonicalEvidence::MAX_ARTIFACT_BYTES) if @read_boundary
|
|
182
|
+
out, _error, status = Open3.capture3("git", "show", "#{commit}:#{path}",
|
|
183
|
+
chdir: repo_root, stdin_data: "")
|
|
184
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical evidence blob is missing" unless status.success?
|
|
185
|
+
out.b
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Prepared inputs use the same immutable Git owner, with a bounded
|
|
189
|
+
# capture before any bytes can reach a worker transport.
|
|
190
|
+
def bounded_blob(path, commit:, max_bytes:)
|
|
191
|
+
validate_blob_path!(path)
|
|
192
|
+
unless commit.is_a?(String) && commit.match?(/\A[0-9a-f]{40}(?:[0-9a-f]{24})?\z/) &&
|
|
193
|
+
max_bytes.is_a?(Integer) && max_bytes.between?(1, 64 * 1024 * 1024)
|
|
194
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob selector differs"
|
|
195
|
+
end
|
|
196
|
+
return @read_boundary.blob(path, commit: commit, max_bytes: max_bytes) if @read_boundary
|
|
197
|
+
environment = ENV.keys.to_h { |key| [key, nil] }
|
|
198
|
+
environment.merge!("GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_GLOBAL" => "/dev/null",
|
|
199
|
+
"GIT_TERMINAL_PROMPT" => "0", "GIT_NO_REPLACE_OBJECTS" => "1", "PATH" => "/usr/bin:/bin", "LC_ALL" => "C")
|
|
200
|
+
result = Herdr::Molecules::BoundedProcess.call([environment, "/usr/bin/git", "-C", repo_root,
|
|
201
|
+
"-c", "core.hooksPath=/dev/null", "cat-file", "blob", "#{commit}:#{path}"],
|
|
202
|
+
stdin_data: "", timeout_s: 30, output_limit: max_bytes)
|
|
203
|
+
unless result.status.success? && !result.oversized
|
|
204
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob is missing or oversized"
|
|
205
|
+
end
|
|
206
|
+
result.stdout.b
|
|
207
|
+
rescue Timeout::Error, Herdr::Molecules::BoundedProcess::PostLaunchError, SystemCallError, IOError
|
|
208
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical prepared blob read is unavailable"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def validate_mutation_id!(id)
|
|
214
|
+
raise ArgumentError, "invalid authority ID" unless id.is_a?(String) && id.match?(ID)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def validate_blob_path!(path)
|
|
218
|
+
unless path.is_a?(String) && path.match?(%r{\A(?:evidence/imports|execution/definitions|execution/prepared|candidates/bundles)/[a-zA-Z0-9_.-]+\z}) &&
|
|
219
|
+
!%w[. ..].include?(File.basename(path))
|
|
220
|
+
raise ArgumentError, "invalid canonical blob reference"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def chain_mutation_events(attempt_id, previous, entries)
|
|
225
|
+
entries.map do |entry|
|
|
226
|
+
recorded_at = entry.fetch(:recorded_at) { Time.now.utc }
|
|
227
|
+
raise ArgumentError, "internal event timestamp must be a UTC Time" unless recorded_at.is_a?(Time) && recorded_at.utc?
|
|
228
|
+
event = Models::EvidenceEvent.build(type: entry.fetch(:type), attempt_id: attempt_id,
|
|
229
|
+
payload: entry.fetch(:payload), previous_digest: previous, recorded_at: recorded_at)
|
|
230
|
+
previous = event.fetch("digest")
|
|
231
|
+
event
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Git clean/smudge filters and CRLF conversion cannot redefine accepted
|
|
236
|
+
# evidence bytes. Insert unfiltered objects after staging the events.
|
|
237
|
+
def stage_mutation_blobs(blobs)
|
|
238
|
+
blobs.each do |path, bytes|
|
|
239
|
+
oid, error, status = Open3.capture3("git", "hash-object", "-w", "--stdin", "--no-filters",
|
|
240
|
+
chdir: repo_root, stdin_data: bytes.b)
|
|
241
|
+
unless status.success?
|
|
242
|
+
raise AttemptErrors::EvidenceUnavailable, "Cannot write canonical evidence object: #{error.strip}"
|
|
243
|
+
end
|
|
244
|
+
git!("-C", checkout_dir, "update-index", "--add", "--cacheinfo", "100644,#{oid.strip},#{path}")
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def write_mutation_blobs(blobs, commit:)
|
|
249
|
+
blobs.map do |path, bytes|
|
|
250
|
+
validate_blob_path!(path)
|
|
251
|
+
raise ArgumentError, "canonical blob must contain bytes" unless bytes.is_a?(String)
|
|
252
|
+
target = File.join(checkout_dir, path)
|
|
253
|
+
if File.exist?(target)
|
|
254
|
+
# A checkout may have smudged the file; immutability is checked
|
|
255
|
+
# against accepted bytes at the canonical commit, not projection.
|
|
256
|
+
unless blob(path, commit: commit) == bytes.b
|
|
257
|
+
raise AttemptErrors::Conflict, "Canonical evidence blob is immutable"
|
|
258
|
+
end
|
|
259
|
+
else
|
|
260
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
261
|
+
File.open(target, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) do |file|
|
|
262
|
+
file.write(bytes.b)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
path
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|