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.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +3 -2099
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -1
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +4 -161
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -455
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -207
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/endcap_services.rb +0 -598
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_results.rb +0 -196
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -304
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -187
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Molecules
|
|
9
|
-
# Canonical receipt validation for accepted execution evidence.
|
|
10
|
-
#
|
|
11
|
-
# Verifies schema and field hygiene, attempt binding, producer
|
|
12
|
-
# attribution, transition authority, current-head candidate binding,
|
|
13
|
-
# artifact digests, executed checks, and independent review verdicts.
|
|
14
|
-
# Every failure is a rejection with an actionable reason; a receipt is
|
|
15
|
-
# never partially accepted.
|
|
16
|
-
class ReceiptVerifier
|
|
17
|
-
# Operations that act outside the local repository once executed.
|
|
18
|
-
EXTERNAL_EFFECT_OPERATIONS = %w[merge publish deploy release].freeze
|
|
19
|
-
|
|
20
|
-
REVIEW_OPERATION = "review"
|
|
21
|
-
HEAD_PATTERN = /\A[0-9a-f]{7,64}\z/.freeze
|
|
22
|
-
APPROVED_VERDICT = "approved"
|
|
23
|
-
|
|
24
|
-
# @param identity_resolver [ExecutionIdentityResolver] Trust boundary
|
|
25
|
-
# @param artifact_reader [#call, nil] Source-owned canonical byte
|
|
26
|
-
# reader for protected composition; nil selects explicit local files
|
|
27
|
-
def initialize(identity_resolver: nil, artifact_reader: nil, campaign_verifier: nil)
|
|
28
|
-
@identity_resolver = identity_resolver || ExecutionIdentityResolver.new
|
|
29
|
-
unless artifact_reader.nil? || artifact_reader.respond_to?(:call)
|
|
30
|
-
raise ArgumentError, "artifact reader must be a source-owned callable"
|
|
31
|
-
end
|
|
32
|
-
@artifact_reader = artifact_reader
|
|
33
|
-
unless campaign_verifier.nil? || campaign_verifier.respond_to?(:call)
|
|
34
|
-
raise ArgumentError, "campaign verifier must be a source-owned callable"
|
|
35
|
-
end
|
|
36
|
-
@campaign_verifier = campaign_verifier
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Validate a submitted receipt against an attempt.
|
|
40
|
-
#
|
|
41
|
-
# @param data [Hash] Parsed receipt payload
|
|
42
|
-
# @param attempt [Models::Attempt] Target attempt (loaded by coordinator)
|
|
43
|
-
# @param identity [ExecutionIdentityResolver::Identity] Finishing boundary identity
|
|
44
|
-
# @param live_head [String] Current candidate revision (git rev-parse HEAD)
|
|
45
|
-
# @param repo_root [String] Project root for artifact resolution
|
|
46
|
-
# @return [Models::ExecutionReceipt] Normalized receipt with digest
|
|
47
|
-
# @raise [AttemptErrors::ReceiptRejected] on any validation failure
|
|
48
|
-
def verify!(data, attempt:, identity:, live_head:, repo_root:)
|
|
49
|
-
verify_receipt!(data, attempt: attempt, live_head: live_head, repo_root: repo_root) do
|
|
50
|
-
verify_authority(data, identity)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Submission validates evidence without granting terminal acceptance.
|
|
55
|
-
# The authenticated owner separately checks the exact worker attribution.
|
|
56
|
-
def verify_result!(data, attempt:, live_head:, repo_root:)
|
|
57
|
-
verify_receipt!(data, attempt: attempt, live_head: live_head, repo_root: repo_root)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def verify_receipt!(data, attempt:, live_head:, repo_root:)
|
|
61
|
-
reject_unless(data.is_a?(Hash), "receipt must be a JSON object")
|
|
62
|
-
|
|
63
|
-
if Models::ExecutionReceipt.forbidden_field?(data)
|
|
64
|
-
reject("receipt carries forbidden fields (credentials, environment, or terminal output)")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
required = %w[attempt_id assignment_id project_id scope operation producer head verdict]
|
|
68
|
-
missing = required.select { |key| data[key].nil? || data[key].to_s.strip.empty? }
|
|
69
|
-
reject("missing required fields: #{missing.join(', ')}") unless missing.empty?
|
|
70
|
-
unless Models::ExecutionReceipt::VERDICTS.include?(data["verdict"].to_s)
|
|
71
|
-
reject("unsupported verdict '#{data['verdict']}': must be one of #{Models::ExecutionReceipt::VERDICTS.join(', ')}")
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
verify_binding(data, attempt)
|
|
75
|
-
verify_producer(data)
|
|
76
|
-
yield if block_given?
|
|
77
|
-
verify_head(data, live_head)
|
|
78
|
-
verify_artifacts(data, repo_root) if data["verdict"] == "succeeded"
|
|
79
|
-
verify_checks(data)
|
|
80
|
-
verify_review(data)
|
|
81
|
-
verify_campaign(data, repo_root: repo_root, live_head: live_head)
|
|
82
|
-
|
|
83
|
-
receipt = Models::ExecutionReceipt.from_h(data.merge("recorded_at" => Time.now.utc))
|
|
84
|
-
verify_digest(receipt)
|
|
85
|
-
|
|
86
|
-
receipt
|
|
87
|
-
end
|
|
88
|
-
private :verify_receipt!
|
|
89
|
-
|
|
90
|
-
# Recheck the source artifacts/checks of an already accepted receipt.
|
|
91
|
-
# Local historical reads (recorded at a past head) validate acceptance from
|
|
92
|
-
# the append-only journal without re-hashing artifacts: the review
|
|
93
|
-
# lifecycle legitimately archives and annotates finding files after
|
|
94
|
-
# collection, and `.ace-local` working files are disposable, so their
|
|
95
|
-
# current existence proves nothing about an accepted past receipt.
|
|
96
|
-
# Current-head reads keep full artifact re-verification. Protected
|
|
97
|
-
# readers always reverify imported bytes, including historical reads.
|
|
98
|
-
def verify_accepted_evidence!(data, live_head:, repo_root:, historical: false)
|
|
99
|
-
verify_head(data, live_head)
|
|
100
|
-
artifacts = data["artifacts"]
|
|
101
|
-
reject_unless(artifacts.is_a?(Array), "accepted artifacts must be an array")
|
|
102
|
-
if (@artifact_reader || !historical) && (data["verdict"] == "succeeded" || !artifacts.empty?)
|
|
103
|
-
verify_artifacts(data, repo_root)
|
|
104
|
-
end
|
|
105
|
-
verify_checks(data)
|
|
106
|
-
verify_review(data)
|
|
107
|
-
verify_campaign(data, repo_root: repo_root, live_head: live_head) if @artifact_reader
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# @param operation [String] Receipt operation
|
|
111
|
-
# @return [Boolean] True when the operation acts outside the repository
|
|
112
|
-
def external_effect?(operation)
|
|
113
|
-
EXTERNAL_EFFECT_OPERATIONS.include?(operation.to_s)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
private
|
|
117
|
-
|
|
118
|
-
def verify_binding(data, attempt)
|
|
119
|
-
binding = attempt.binding
|
|
120
|
-
unless data["attempt_id"] == attempt.attempt_id
|
|
121
|
-
reject("receipt attempt_id #{data['attempt_id']} does not match attempt #{attempt.attempt_id}")
|
|
122
|
-
end
|
|
123
|
-
unless data["assignment_id"] == binding.assignment_id
|
|
124
|
-
reject("receipt assignment #{data['assignment_id']} does not match attempt assignment #{binding.assignment_id}")
|
|
125
|
-
end
|
|
126
|
-
unless data["project_id"] == binding.project_id
|
|
127
|
-
reject("receipt project #{data['project_id']} does not match attempt project #{binding.project_id}")
|
|
128
|
-
end
|
|
129
|
-
unless Atoms::AssignmentScope.equal?(data["scope"], binding.scope)
|
|
130
|
-
reject("receipt scope #{data['scope']} does not match attempt scope #{binding.scope}")
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def verify_producer(data)
|
|
135
|
-
producer = data["producer"]
|
|
136
|
-
reject_unless(producer.is_a?(Hash), "producer must be an object with actor, role, runtime")
|
|
137
|
-
|
|
138
|
-
actor = producer["actor"].to_s.strip
|
|
139
|
-
role = producer["role"].to_s.strip
|
|
140
|
-
runtime = producer["runtime"].to_s.strip
|
|
141
|
-
if actor.empty? || runtime.empty? || !ExecutionIdentityResolver::ROLES.include?(role)
|
|
142
|
-
reject("producer attribution incomplete: actor, runtime, and a known role are required")
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Workers submit attributable results but cannot self-approve
|
|
147
|
-
# succeeded outcomes; only coordinator/service authority accepts them.
|
|
148
|
-
def verify_authority(data, identity)
|
|
149
|
-
return if data["verdict"] != "succeeded"
|
|
150
|
-
return if @identity_resolver.trusted?(identity)
|
|
151
|
-
|
|
152
|
-
reject("identity #{identity.actor}/#{identity.role} may not accept a succeeded verdict")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# The tested/reviewed head must be the live candidate revision; stale
|
|
156
|
-
# heads are rejected (candidate invalidation is recorded by the
|
|
157
|
-
# coordinator).
|
|
158
|
-
def verify_head(data, live_head)
|
|
159
|
-
reject("live candidate head unavailable") if live_head.nil? || live_head.to_s.strip.empty?
|
|
160
|
-
reject("receipt head '#{data['head']}' is not a git SHA") unless data["head"].match?(HEAD_PATTERN)
|
|
161
|
-
|
|
162
|
-
unless data["head"] == live_head
|
|
163
|
-
reject("receipt head #{data['head']} is stale; current candidate is #{live_head}")
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Succeeded outcomes need at least one artifact whose recorded digest
|
|
168
|
-
# matches the file on disk. Paths stay below the project root; symlink
|
|
169
|
-
# or traversal escapes are rejected.
|
|
170
|
-
def verify_artifacts(data, repo_root)
|
|
171
|
-
artifacts = data["artifacts"]
|
|
172
|
-
reject_unless(artifacts.is_a?(Array) && !artifacts.empty?, "succeeded verdict requires at least one artifact")
|
|
173
|
-
|
|
174
|
-
artifacts.each do |artifact|
|
|
175
|
-
reject_unless(artifact.is_a?(Hash), "artifact entries must be objects")
|
|
176
|
-
path = artifact["path"].to_s
|
|
177
|
-
recorded = artifact["sha256"].to_s
|
|
178
|
-
reject("artifact missing path or sha256") if path.empty? || recorded.empty?
|
|
179
|
-
|
|
180
|
-
actual = Digest::SHA256.hexdigest(artifact_bytes(data, artifact, repo_root))
|
|
181
|
-
reject("artifact digest mismatch for #{path}") unless actual == recorded
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def verify_checks(data)
|
|
186
|
-
checks = data["checks"] || []
|
|
187
|
-
reject_unless(checks.is_a?(Array), "checks must be an array")
|
|
188
|
-
if data["verdict"] == "succeeded" && checks.empty?
|
|
189
|
-
reject("succeeded verdict requires at least one executed check")
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
checks.each do |check|
|
|
193
|
-
reject_unless(check.is_a?(Hash), "check entries must be objects")
|
|
194
|
-
reject("check missing name") if check["name"].to_s.strip.empty?
|
|
195
|
-
reject("check #{check['name']} did not pass") if check["verdict"].to_s != "passed" && data["verdict"] == "succeeded"
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# A caller-supplied digest must equal the canonical digest of the
|
|
200
|
-
# receipt payload; arbitrary or all-zero digests are rejected.
|
|
201
|
-
def verify_digest(receipt)
|
|
202
|
-
return if receipt.digest == Atoms::EvidenceDigest.digest(receipt.digest_payload)
|
|
203
|
-
|
|
204
|
-
reject("receipt digest mismatch: supplied digest does not match the canonical payload")
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Review receipts need an executed, independent reviewer verdict for
|
|
208
|
-
# exactly the tested head — a report existing is not approval, and a
|
|
209
|
-
# review receipt without a reviewer verdict is not evidence.
|
|
210
|
-
def verify_review(data)
|
|
211
|
-
review = data["review"]
|
|
212
|
-
if review.nil?
|
|
213
|
-
if data["operation"] == REVIEW_OPERATION && data["verdict"] == "succeeded"
|
|
214
|
-
reject("review receipt requires an executed independent reviewer verdict")
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
return
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
reject_unless(review.is_a?(Hash), "review must be an object")
|
|
221
|
-
reviewer = review["reviewer"]
|
|
222
|
-
reject_unless(reviewer.is_a?(Hash) && !reviewer["actor"].to_s.strip.empty?, "review must name an executed reviewer")
|
|
223
|
-
|
|
224
|
-
producer_actor = data.dig("producer", "actor").to_s.strip
|
|
225
|
-
if reviewer["actor"].to_s.strip == producer_actor
|
|
226
|
-
reject("reviewer and producer must differ (self-approval rejected)")
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
reject("review verdict must be '#{APPROVED_VERDICT}' for a succeeded receipt") if review["verdict"] != APPROVED_VERDICT && data["verdict"] == "succeeded"
|
|
230
|
-
unless review["head"] == data["head"]
|
|
231
|
-
reject("review head #{review['head']} does not match receipt head #{data['head']}")
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Consume ace-review's single campaign authority through the existing
|
|
236
|
-
# receipt boundary; a worker-supplied local result grants no authority.
|
|
237
|
-
def verify_campaign(data, repo_root:, live_head:)
|
|
238
|
-
campaign = data["campaign"]
|
|
239
|
-
return unless campaign
|
|
240
|
-
reject_unless(campaign.is_a?(Hash), "campaign must be an object")
|
|
241
|
-
reject("campaign result requires a succeeded review receipt") unless
|
|
242
|
-
data["operation"] == REVIEW_OPERATION && data["verdict"] == "succeeded"
|
|
243
|
-
reference = campaign["result"]
|
|
244
|
-
reject_unless(reference.is_a?(Hash), "campaign requires a result artifact reference")
|
|
245
|
-
unless Array(data["artifacts"]).include?(reference)
|
|
246
|
-
reject("campaign result must be included in verified receipt artifacts")
|
|
247
|
-
end
|
|
248
|
-
result = JSON.parse(artifact_bytes(data, reference, repo_root))
|
|
249
|
-
reject_unless(result.is_a?(Hash), "campaign result must be an object")
|
|
250
|
-
if @campaign_verifier
|
|
251
|
-
@campaign_verifier.call(data, result, live_head)
|
|
252
|
-
return
|
|
253
|
-
end
|
|
254
|
-
require "ace/review"
|
|
255
|
-
manager = Ace::Review::Organisms::CampaignManager.new(repo_root: repo_root)
|
|
256
|
-
current = manager.status(campaign["id"])
|
|
257
|
-
reject("campaign result ID differs from receipt") unless result["campaign_id"] == campaign["id"]
|
|
258
|
-
manager.with_verified_result!(result: result, subject: current.fetch("subject"),
|
|
259
|
-
contract_identity: current.fetch("contract_identity"), policy: current.fetch("effective_policy"),
|
|
260
|
-
head: live_head, base: current.fetch("evidence").fetch("current_base"),
|
|
261
|
-
producer: data.dig("producer", "actor"), reviewer: data.dig("review", "reviewer", "actor")) { true }
|
|
262
|
-
rescue JSON::ParserError, ArgumentError, TypeError, KeyError => e
|
|
263
|
-
reject("invalid campaign result: #{e.message}")
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Protected composition injects the canonical imported-byte owner.
|
|
267
|
-
# Its failure never falls through to the disposable workspace copy.
|
|
268
|
-
def artifact_bytes(data, artifact, repo_root)
|
|
269
|
-
if @artifact_reader
|
|
270
|
-
bytes = @artifact_reader.call(data, artifact)
|
|
271
|
-
reject("canonical artifact reader did not return bytes") unless bytes.is_a?(String)
|
|
272
|
-
return bytes.b
|
|
273
|
-
end
|
|
274
|
-
path = artifact["path"].to_s
|
|
275
|
-
expanded = begin
|
|
276
|
-
File.expand_path(path, File.realpath(repo_root))
|
|
277
|
-
rescue Errno::ENOENT, Errno::EACCES
|
|
278
|
-
nil
|
|
279
|
-
end
|
|
280
|
-
reject("artifact file not found: #{path}") if expanded.nil? || !File.exist?(expanded)
|
|
281
|
-
resolved = safe_resolve(repo_root, path)
|
|
282
|
-
reject("artifact path escapes project root: #{path}") if resolved.nil?
|
|
283
|
-
File.read(resolved)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Resolve a project-relative artifact path to its real location,
|
|
287
|
-
# rejecting traversal and symlink escapes (an in-repo symlink must
|
|
288
|
-
# not smuggle in an external file); returns nil when the path leaves
|
|
289
|
-
# the root.
|
|
290
|
-
def safe_resolve(repo_root, path)
|
|
291
|
-
root = File.realpath(repo_root)
|
|
292
|
-
candidate = File.expand_path(path, root)
|
|
293
|
-
return nil unless candidate == root || candidate.start_with?(root + File::SEPARATOR)
|
|
294
|
-
return nil unless File.exist?(candidate)
|
|
295
|
-
|
|
296
|
-
resolved = File.realpath(candidate)
|
|
297
|
-
return nil unless resolved == root || resolved.start_with?(root + File::SEPARATOR)
|
|
298
|
-
|
|
299
|
-
resolved
|
|
300
|
-
rescue Errno::ENOENT, Errno::EACCES
|
|
301
|
-
nil
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def reject(message)
|
|
305
|
-
raise AttemptErrors::ReceiptRejected, message
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def reject_unless(condition, message)
|
|
309
|
-
reject(message) unless condition
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
end
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require_relative "receipt_verifier"
|
|
3
|
-
require_relative "canonical_evidence"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Assign
|
|
7
|
-
module Molecules
|
|
8
|
-
# Pure reader over already accepted terminal owner evidence. A submitted
|
|
9
|
-
# result alone carries no terminal authority; local filesystem artifacts
|
|
10
|
-
# are never substituted for its imported protected bytes.
|
|
11
|
-
class TerminalScopeReceipt
|
|
12
|
-
BINDING_FIELDS = %w[mapping_id project_id assignment_id attempt_id result_id worker_uid worker_actor
|
|
13
|
-
launch_ticket process_binding head candidate_generation uploaded_receipt_sha256 original_receipt_digest].freeze
|
|
14
|
-
PAYLOAD_FIELDS = %w[version result_id binding uploaded_receipt_sha256 original_receipt_digest receipt_digest receipt artifacts].freeze
|
|
15
|
-
|
|
16
|
-
def self.verify!(events:, lineage:, journal:, commit:, mapping:)
|
|
17
|
-
raise AttemptErrors::EvidenceUnavailable, "terminal receipt chain differs" unless Models::EvidenceEvent.chain_valid?(events)
|
|
18
|
-
terminal = events.select { |event| event["type"] == "receipt_accepted" &&
|
|
19
|
-
%w[succeeded failed].include?(event.dig("payload", "receipt", "verdict")) }
|
|
20
|
-
raise AttemptErrors::EvidenceUnavailable, "no unique accepted terminal receipt" unless terminal.size == 1
|
|
21
|
-
accepted = terminal.first
|
|
22
|
-
receipt = accepted.fetch("payload").fetch("receipt")
|
|
23
|
-
before = events.take_while { |event| event.fetch("digest") != accepted.fetch("digest") }
|
|
24
|
-
submitted = before.select { |event| event["type"] == "result_submitted" &&
|
|
25
|
-
event.dig("payload", "receipt_digest") == receipt.fetch("digest") }
|
|
26
|
-
raise AttemptErrors::EvidenceUnavailable, "accepted terminal result provenance differs" unless submitted.size == 1
|
|
27
|
-
payload = submitted.first.fetch("payload")
|
|
28
|
-
binding = payload.fetch("binding")
|
|
29
|
-
reservation = before.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
|
|
30
|
-
origin = reservation.fetch("payload").fetch("data")
|
|
31
|
-
expected = {"mapping_id" => origin.fetch("mapping_id"), "project_id" => mapping.fetch("project_id"),
|
|
32
|
-
"assignment_id" => origin.fetch("assignment_id"), "attempt_id" => accepted.fetch("attempt_id"),
|
|
33
|
-
"worker_uid" => mapping.fetch("worker_uid"), "worker_actor" => mapping.fetch("worker_actor"),
|
|
34
|
-
"launch_ticket" => origin.fetch("launch_ticket"), "process_binding" => lineage.child_event.fetch("payload").fetch("original_process_binding")}
|
|
35
|
-
candidates = before.select { |event| event["type"] == "authority_mutation" &&
|
|
36
|
-
event.dig("payload", "operation") == "submit_candidate" &&
|
|
37
|
-
event.dig("payload", "data", "candidate_generation") == binding.fetch("candidate_generation") }
|
|
38
|
-
candidate = candidates.one? && candidates.first.dig("payload", "data")
|
|
39
|
-
unless payload.keys.sort == PAYLOAD_FIELDS.sort && binding.keys.sort == BINDING_FIELDS.sort &&
|
|
40
|
-
payload["version"] == 1 && expected.all? { |key, value| binding[key] == value } &&
|
|
41
|
-
candidate && candidate["head"] == binding["head"] && receipt["head"] == binding["head"] &&
|
|
42
|
-
payload["result_id"] == binding["result_id"] && payload["uploaded_receipt_sha256"] == binding["uploaded_receipt_sha256"] &&
|
|
43
|
-
payload["original_receipt_digest"] == binding["original_receipt_digest"] &&
|
|
44
|
-
Models::ExecutionReceipt.from_h(payload.fetch("receipt")).digest_payload == Models::ExecutionReceipt.from_h(receipt).digest_payload &&
|
|
45
|
-
(receipt.keys - Models::ExecutionReceipt.from_h(receipt).to_h.keys).empty? &&
|
|
46
|
-
payload["artifacts"] == receipt["artifacts"] &&
|
|
47
|
-
receipt["producer"] == {"actor" => mapping.fetch("worker_actor"), "role" => "worker", "runtime" => "herdr"} &&
|
|
48
|
-
journal.canonical_attempt_state(events) == receipt.fetch("verdict")
|
|
49
|
-
raise AttemptErrors::EvidenceUnavailable, "accepted terminal receipt selectors differ"
|
|
50
|
-
end
|
|
51
|
-
model = Models::ExecutionReceipt.from_h(receipt)
|
|
52
|
-
unless model.digest == Atoms::EvidenceDigest.digest(model.digest_payload) && model.digest == payload.fetch("receipt_digest")
|
|
53
|
-
raise AttemptErrors::EvidenceUnavailable, "accepted terminal receipt digest differs"
|
|
54
|
-
end
|
|
55
|
-
finishes = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "finish" }
|
|
56
|
-
unless finishes.empty?
|
|
57
|
-
transition = events[events.index(accepted) + 1]
|
|
58
|
-
finish = events[events.index(accepted) + 2]
|
|
59
|
-
expected_reply = binding.slice("attempt_id", "result_id", "head", "candidate_generation").merge(
|
|
60
|
-
"receipt_digest" => payload.fetch("receipt_digest"), "state" => receipt.fetch("verdict"))
|
|
61
|
-
unless finishes.one? && finishes.first == finish && transition && transition["type"] == "transition" &&
|
|
62
|
-
transition.fetch("payload") == {"from" => "running", "to" => receipt.fetch("verdict"), "reason" => "protected_result_accepted"} &&
|
|
63
|
-
finish.dig("payload", "data").except("generation") == expected_reply &&
|
|
64
|
-
finish.dig("payload", "data", "generation") == events.take(events.index(finish) + 1).count { |event| event["type"] == "authority_mutation" }
|
|
65
|
-
raise AttemptErrors::EvidenceUnavailable, "accepted finish reply or terminal transition differs"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
canonical = CanonicalEvidence.new(journal: journal)
|
|
69
|
-
context = {kind: "result", project_id: binding.fetch("project_id"), assignment_id: binding.fetch("assignment_id"),
|
|
70
|
-
attempt_id: binding.fetch("attempt_id"), peer_uid: binding.fetch("worker_uid"), binding: binding,
|
|
71
|
-
request_id_or_event_id: binding.fetch("result_id"), generation: binding.fetch("candidate_generation")}
|
|
72
|
-
reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
73
|
-
**context, commit: commit) }
|
|
74
|
-
Array(receipt["artifacts"]).each { |artifact| reader.call(receipt, artifact) }
|
|
75
|
-
intent = before.find { |event| event["type"] == "intent" }
|
|
76
|
-
attempt = journal.send(:build_attempt, binding.fetch("assignment_id"), binding.fetch("attempt_id"),
|
|
77
|
-
intent.fetch("payload"), before, "running")
|
|
78
|
-
verifier = ReceiptVerifier.new(artifact_reader: reader)
|
|
79
|
-
verifier.verify_result!(receipt, attempt: attempt, live_head: binding.fetch("head"), repo_root: journal.repo_root)
|
|
80
|
-
verifier.verify_accepted_evidence!(receipt, live_head: binding.fetch("head"), repo_root: journal.repo_root, historical: true)
|
|
81
|
-
accepted
|
|
82
|
-
rescue KeyError, TypeError, ArgumentError, NoMethodError, AttemptErrors::ReceiptRejected
|
|
83
|
-
raise AttemptErrors::EvidenceUnavailable, "authenticated terminal scope receipt unavailable"
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|