ace-assign 0.71.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -2119
- 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 -4
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
- 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/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 -176
- 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/candidate_limits.rb +0 -20
- 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/atoms/git_blob_batch.rb +0 -59
- 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_blob_batch.rb +0 -55
- data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
- 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/command_execution.rb +0 -180
- 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 -457
- 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 -41
- 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_context.rb +0 -38
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- 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/fixed_publication_lifecycle.rb +0 -176
- data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
- 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 -104
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
- 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_release_artifacts.rb +0 -143
- data/lib/ace/assign/authority/ordinary_results.rb +0 -206
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
- data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
- 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 -309
- 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/release_artifact_manifest.rb +0 -91
- data/lib/ace/assign/authority/retained_input.rb +0 -53
- 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 -189
- 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 -170
- 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 -1899
- 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 -274
- 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 -320
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
- 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,257 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "securerandom"
|
|
3
|
-
require_relative "deployment"
|
|
4
|
-
require_relative "transfer_codec"
|
|
5
|
-
require_relative "campaign_execution"
|
|
6
|
-
require_relative "candidate_transfer"
|
|
7
|
-
require_relative "../molecules/canonical_evidence"
|
|
8
|
-
require_relative "../molecules/execution_scope_lineage"
|
|
9
|
-
|
|
10
|
-
module Ace
|
|
11
|
-
module Assign
|
|
12
|
-
module Authority
|
|
13
|
-
class Client
|
|
14
|
-
Reply = Struct.new(:data, :replayed, :parts, keyword_init: true)
|
|
15
|
-
attr_reader :mapping_id
|
|
16
|
-
def initialize(mapping_id:, deployment: Deployment.load, kernel: Ace::Runtime::Molecules::ProtectedLinux.new)
|
|
17
|
-
@mapping_id, @deployment, @kernel = mapping_id, deployment, kernel
|
|
18
|
-
@map = deployment.mapping(mapping_id)
|
|
19
|
-
@service = deployment.authority(@map.fetch("authority_id"))
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call(operation, params, mutation_id: nil, timeout: nil, upload_parts: nil, download: false, purpose: nil)
|
|
23
|
-
if operation == "register_assignment" && (!upload_parts || purpose != :candidate || download)
|
|
24
|
-
raise ArgumentError, "prepared registration requires fixed candidate upload"
|
|
25
|
-
end
|
|
26
|
-
if operation == "evidence_fetch" && (!download || purpose != (%w[prepared_work campaign_candidate].include?(params["kind"]) ? :candidate : :artifacts) || upload_parts)
|
|
27
|
-
raise ArgumentError, "evidence fetch requires source-fixed kind download"
|
|
28
|
-
end
|
|
29
|
-
if operation == "submit_result" && (!upload_parts || purpose != :receipt_artifacts || download)
|
|
30
|
-
raise ArgumentError, "result submission requires fixed receipt upload"
|
|
31
|
-
end
|
|
32
|
-
if operation == "campaign_export_result" && (!download || purpose != :artifacts || upload_parts || mutation_id)
|
|
33
|
-
raise ArgumentError, "campaign result requires fixed read-only artifact download"
|
|
34
|
-
end
|
|
35
|
-
if operation == "campaign_record_round" && (!upload_parts || purpose != :receipt_artifacts || download)
|
|
36
|
-
raise ArgumentError, "campaign round requires fixed bounded upload"
|
|
37
|
-
end
|
|
38
|
-
@deployment.verify!(mapping_id, kernel: @kernel)
|
|
39
|
-
wire = Ace::Runtime::Molecules::ProtectedSocket
|
|
40
|
-
path = @service.fetch("socket_path")
|
|
41
|
-
before = wire.socket_identity(path)
|
|
42
|
-
raise Ace::Runtime::RuntimeUnavailableError, "authority endpoint owner differs" unless before.last == @service.fetch("uid")
|
|
43
|
-
raise ArgumentError, "cannot upload and download on one request" if upload_parts && download
|
|
44
|
-
prepared_download = operation == "evidence_fetch" && %w[prepared_work campaign_candidate].include?(params["kind"])
|
|
45
|
-
codec = transfer_codec if upload_parts || download && !prepared_download
|
|
46
|
-
descriptor = codec.descriptor(upload_parts, purpose: purpose) if upload_parts
|
|
47
|
-
parameters = upload_parts ? params.merge("transfer" => descriptor) : params
|
|
48
|
-
cap = %w[reserve_attempt close_execution_scope stop_attempt].include?(operation) ? 90 : 30
|
|
49
|
-
duration = timeout || (cap == 90 ? 90 : 5)
|
|
50
|
-
unless duration.is_a?(Numeric) && duration.positive? && duration.finite?
|
|
51
|
-
raise ArgumentError, "authority timeout must be finite and positive"
|
|
52
|
-
end
|
|
53
|
-
deadline = wire.deadline([duration, cap].min)
|
|
54
|
-
wire.connect(path, deadline: deadline) do |socket|
|
|
55
|
-
peer = @kernel.peer(socket)
|
|
56
|
-
unless peer["uid"] == @service.fetch("uid") && peer["gid"] == @service.fetch("gid") &&
|
|
57
|
-
peer["groups"] == @service.fetch("groups") && wire.socket_identity(path) == before
|
|
58
|
-
raise AttemptErrors::UnauthorizedIdentity, "authority endpoint peer changed"
|
|
59
|
-
end
|
|
60
|
-
wire.write(socket, {"version" => 1, "operation" => operation,
|
|
61
|
-
"mutation_id" => mutation_id, "project_id" => @map.fetch("project_id"),
|
|
62
|
-
"params" => parameters.merge("mapping_id" => mapping_id)}, deadline: deadline, limit: upload_parts || download ? 16_384 : wire::LIMIT)
|
|
63
|
-
if %w[bind_inbox finish recover request_review review_status launch_review_intent cancel_review assignment_inventory evidence_fetch observe_execution_scope close_execution_scope stop_attempt prompt_status launch_input_inhibit_selection launch_input_inhibit_completion launch_prompt_intent launch_prompt_completion claim_service_settlement workspace_prune_preview_context].include?(operation) || (operation == "attempt_status" && params.key?("result_candidate_generation"))
|
|
64
|
-
socket.shutdown(Socket::SHUT_WR)
|
|
65
|
-
end
|
|
66
|
-
if upload_parts
|
|
67
|
-
codec.send(socket, parts: upload_parts, descriptor: descriptor, purpose: purpose, deadline: deadline)
|
|
68
|
-
socket.shutdown(Socket::SHUT_WR)
|
|
69
|
-
end
|
|
70
|
-
result = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
71
|
-
unless result.is_a?(Hash) && result["status"] == "ok" && result["data"].is_a?(Hash)
|
|
72
|
-
code = result.is_a?(Hash) ? result.dig("error", "code") : nil
|
|
73
|
-
raise AttemptErrors::BoundedResultUnavailable, "protected authority result exceeds frame bound" if code == "bounded_result"
|
|
74
|
-
raise AttemptErrors::EvidenceUnavailable, "protected authority refused (#{code || 'invalid_response'})"
|
|
75
|
-
end
|
|
76
|
-
transport = result.fetch("transport")
|
|
77
|
-
unless transport.is_a?(Hash) && transport.keys == ["replayed"] && [true, false].include?(transport["replayed"])
|
|
78
|
-
raise AttemptErrors::EvidenceUnavailable, "authority replay metadata is unavailable"
|
|
79
|
-
end
|
|
80
|
-
if operation == "evidence_fetch"
|
|
81
|
-
if %w[prepared_work campaign_candidate].include?(params["kind"]) && transport.fetch("replayed")
|
|
82
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared fetch cannot replay a mutation"
|
|
83
|
-
end
|
|
84
|
-
validate_evidence_download!(result.fetch("data"), params)
|
|
85
|
-
end
|
|
86
|
-
parts = if download
|
|
87
|
-
codec = prepared_transfer_codec(result.fetch("data").fetch("descriptor")) if prepared_download
|
|
88
|
-
codec.receive(socket, descriptor: result.fetch("data").fetch("transfer"), purpose: purpose, deadline: deadline) do |input|
|
|
89
|
-
Array.new(input.count) { |index| input.bytes(index: index) }
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
Reply.new(data: result.fetch("data"), replayed: transport.fetch("replayed"), parts: parts)
|
|
93
|
-
end
|
|
94
|
-
rescue SystemCallError, IOError
|
|
95
|
-
raise AttemptErrors::EvidenceUnavailable, "protected authority connection unavailable; no local fallback"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# Fixed original-launcher stream. Public calls retain their existing
|
|
100
|
-
# complete-upload EOF contract; no caller-selected framing flag exists.
|
|
101
|
-
def with_launch_control(state:)
|
|
102
|
-
@deployment.verify!(mapping_id, kernel: @kernel)
|
|
103
|
-
wire = Ace::Runtime::Molecules::ProtectedSocket
|
|
104
|
-
path = @service.fetch("socket_path")
|
|
105
|
-
before = wire.socket_identity(path)
|
|
106
|
-
raise Ace::Runtime::RuntimeUnavailableError, "Authority endpoint owner differs" unless before.last == @service.fetch("uid")
|
|
107
|
-
deadline = wire.deadline(30)
|
|
108
|
-
wire.connect(path, deadline: deadline) do |socket|
|
|
109
|
-
peer = @kernel.peer(socket)
|
|
110
|
-
unless peer.values_at("uid", "gid", "groups") == @service.values_at("uid", "gid", "groups") && wire.socket_identity(path) == before
|
|
111
|
-
raise AttemptErrors::UnauthorizedIdentity, "Authority stream peer changed"
|
|
112
|
-
end
|
|
113
|
-
wire.write(socket, {"version" => 1, "operation" => "launch_control", "mutation_id" => nil,
|
|
114
|
-
"project_id" => @map.fetch("project_id"), "params" => state.slice("assignment_id", "attempt_id").merge("mapping_id" => mapping_id)},
|
|
115
|
-
deadline: deadline, limit: 16_384)
|
|
116
|
-
ready = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
117
|
-
validate_launch_control_ready!(ready, state)
|
|
118
|
-
yield socket, transfer_codec, ready
|
|
119
|
-
end
|
|
120
|
-
rescue IOError, SystemCallError
|
|
121
|
-
raise AttemptErrors::EvidenceUnavailable, "Original launch control connection is unavailable"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
def validate_launch_control_ready!(ready, state)
|
|
127
|
-
unless ready.is_a?(Hash) && ready.keys.sort == %w[attempt_id generation journal_commit original_binding_digest type version] &&
|
|
128
|
-
ready["version"].is_a?(Integer) && ready["version"] == 1 && ready["type"] == "launch_control_ready" &&
|
|
129
|
-
ready["attempt_id"] == state.fetch("attempt_id") && ready["generation"].is_a?(Integer) && ready["generation"].positive? &&
|
|
130
|
-
ready["journal_commit"].is_a?(String) && ready["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
131
|
-
ready["original_binding_digest"].is_a?(String) && ready["original_binding_digest"].match?(/\A[0-9a-f]{64}\z/)
|
|
132
|
-
raise AttemptErrors::EvidenceUnavailable, "Original launch control admission is unavailable"
|
|
133
|
-
end
|
|
134
|
-
true
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def validate_evidence_download!(data, params)
|
|
138
|
-
descriptor, transfer = data.values_at("descriptor", "transfer")
|
|
139
|
-
if params["kind"] == "prepared_work"
|
|
140
|
-
validate_prepared_download!(data, params)
|
|
141
|
-
return
|
|
142
|
-
end
|
|
143
|
-
if params["kind"] == "campaign_candidate"
|
|
144
|
-
validate_campaign_candidate_download!(data, params)
|
|
145
|
-
return
|
|
146
|
-
end
|
|
147
|
-
unless data.keys.sort == %w[descriptor generation journal_commit transfer] &&
|
|
148
|
-
Molecules::CanonicalEvidence.valid_descriptor?(descriptor) &&
|
|
149
|
-
descriptor["project_id"] == @map.fetch("project_id") &&
|
|
150
|
-
{"assignment_id" => "assignment_id", "attempt_id" => "attempt_id", "kind" => "kind",
|
|
151
|
-
"request_id_or_event_id" => "purpose_id", "artifact_id" => "artifact_id"}.all? { |field, key| descriptor[field] == params[key] } &&
|
|
152
|
-
data["generation"].is_a?(Integer) && data["generation"] >= 0 &&
|
|
153
|
-
data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A[0-9a-f]{40,64}\z/) &&
|
|
154
|
-
transfer.is_a?(Hash) && transfer.keys.sort == %w[bytes parts sha256 version] && transfer["version"] == 1 &&
|
|
155
|
-
transfer["bytes"] == descriptor["bytes"] && transfer["sha256"] == descriptor["sha256"] &&
|
|
156
|
-
transfer["parts"] == [descriptor.slice("bytes", "sha256")]
|
|
157
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical download descriptor differs"
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def transfer_codec
|
|
162
|
-
peer = @kernel.capture(Process.pid)
|
|
163
|
-
project = @deployment.project(@map.fetch("project_id"))
|
|
164
|
-
installed = project.fetch("peer_credentials").fetch(peer.fetch("uid").to_s)
|
|
165
|
-
unless peer["gid"] == installed["gid"] && peer["groups"] == installed["groups"]
|
|
166
|
-
raise AttemptErrors::UnauthorizedIdentity, "transfer scratch principal differs"
|
|
167
|
-
end
|
|
168
|
-
TransferCodec.new(root: installed.fetch("scratch_root"))
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def validate_prepared_download!(data, params)
|
|
172
|
-
descriptor, transfer = data.values_at("descriptor", "transfer")
|
|
173
|
-
fields = %w[version kind purpose artifact project_id mapping_id assignment_id attempt_id task_id scope definition_digest selection_sha256 prepared_head prepared_tree manifest_bytes manifest_sha256 registration_generation registration_commit original_binding_digest ref bytes sha256 task_context_entry original_worker_identity original_worker_scratch_root workspace_exclusion].sort
|
|
174
|
-
valid = data.keys.sort == %w[descriptor generation journal_commit transfer] && descriptor.is_a?(Hash) && descriptor.keys.sort == fields &&
|
|
175
|
-
descriptor["version"].is_a?(Integer) && descriptor["version"] == 1 &&
|
|
176
|
-
params.values_at("kind", "purpose_id", "artifact_id") == %w[prepared_work original_prepared_work prepared_bundle] &&
|
|
177
|
-
descriptor.values_at("kind", "purpose", "artifact") == %w[prepared_work original_prepared_work prepared_bundle] &&
|
|
178
|
-
descriptor["project_id"] == @map.fetch("project_id") && descriptor["mapping_id"] == mapping_id &&
|
|
179
|
-
%w[assignment_id attempt_id].all? { |key| descriptor[key] == params[key] } &&
|
|
180
|
-
descriptor["task_id"].is_a?(String) && descriptor["task_id"].match?(Deployment::TOKEN) &&
|
|
181
|
-
descriptor["scope"].is_a?(String) && descriptor["scope"].bytesize.between?(1, 128) && descriptor["scope"].match?(/\A[0-9]+(?:\.[0-9]+)*\z/) &&
|
|
182
|
-
%w[definition_digest selection_sha256 manifest_sha256 original_binding_digest sha256].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
183
|
-
%w[prepared_head prepared_tree].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{40}\z/) } &&
|
|
184
|
-
descriptor["manifest_bytes"].is_a?(Integer) && descriptor["manifest_bytes"].between?(1, 32_768) &&
|
|
185
|
-
descriptor["registration_generation"].is_a?(Integer) && descriptor["registration_generation"].positive? &&
|
|
186
|
-
descriptor["bytes"].is_a?(Integer) && descriptor["bytes"].between?(1, TransferCodec::LIMITS.fetch(:candidate).first) &&
|
|
187
|
-
descriptor["registration_commit"].is_a?(String) && descriptor["registration_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
188
|
-
descriptor["ref"] == "execution/prepared/#{params.fetch('assignment_id')}-#{descriptor['sha256']}.bundle" &&
|
|
189
|
-
data["generation"].is_a?(Integer) && data["generation"].positive? &&
|
|
190
|
-
data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
191
|
-
transfer.is_a?(Hash) && transfer.keys.sort == %w[bytes parts sha256 version] && transfer["version"].is_a?(Integer) && transfer["version"] == 1 &&
|
|
192
|
-
transfer.values_at("bytes", "sha256") == descriptor.values_at("bytes", "sha256") && transfer["parts"] == [descriptor.slice("bytes", "sha256")]
|
|
193
|
-
raise AttemptErrors::EvidenceUnavailable, "original prepared download descriptor differs" unless valid
|
|
194
|
-
TaskContextEntry.validate!(descriptor.fetch("task_context_entry"))
|
|
195
|
-
Molecules::LifecycleExclusion.workspace_reader(projection: descriptor.fetch("workspace_exclusion"))
|
|
196
|
-
unless descriptor.dig("workspace_exclusion", "root_resource", "view_path") ==
|
|
197
|
-
"/run/ace/lifecycle-exclusion/#{descriptor.fetch('mapping_id')}"
|
|
198
|
-
raise AttemptErrors::EvidenceUnavailable, "original prepared lifecycle mapping differs"
|
|
199
|
-
end
|
|
200
|
-
identity = descriptor.fetch("original_worker_identity")
|
|
201
|
-
birth = identity.is_a?(Hash) && identity["started_at"]
|
|
202
|
-
boot = birth.is_a?(String) && birth.split(":", -1)[1]
|
|
203
|
-
Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: boot)
|
|
204
|
-
root = descriptor.fetch("original_worker_scratch_root")
|
|
205
|
-
unless root.is_a?(String) && root.valid_encoding? && root.bytesize.between?(2, 4096) &&
|
|
206
|
-
root.start_with?("/") && !root.include?("\0") && File.expand_path(root) == root
|
|
207
|
-
raise AttemptErrors::EvidenceUnavailable, "original prepared scratch root differs"
|
|
208
|
-
end
|
|
209
|
-
rescue ArgumentError, KeyError
|
|
210
|
-
raise AttemptErrors::EvidenceUnavailable, "original prepared download descriptor differs"
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def validate_campaign_candidate_download!(data, params)
|
|
214
|
-
descriptor, transfer = data.values_at("descriptor", "transfer")
|
|
215
|
-
fields = %w[head tree bytes sha256 candidate_generation campaign_execution mapping_id assignment_id attempt_id
|
|
216
|
-
project_id original_worker_identity original_worker_scratch_root original_binding_digest].sort
|
|
217
|
-
unless data.keys.sort == %w[descriptor generation journal_commit transfer] &&
|
|
218
|
-
descriptor.is_a?(Hash) && descriptor.keys.sort == fields &&
|
|
219
|
-
params.values_at("purpose_id", "artifact_id") == %w[original_campaign_candidate candidate_bundle] &&
|
|
220
|
-
descriptor["mapping_id"] == mapping_id && descriptor["project_id"] == @map.fetch("project_id") &&
|
|
221
|
-
%w[assignment_id attempt_id].all? { |key| descriptor[key] == params[key] } &&
|
|
222
|
-
%w[head tree].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(CandidateTransfer::SHA) } &&
|
|
223
|
-
%w[sha256 original_binding_digest].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
224
|
-
descriptor["candidate_generation"].is_a?(Integer) && descriptor["candidate_generation"].positive? &&
|
|
225
|
-
descriptor["bytes"].is_a?(Integer) && descriptor["bytes"].between?(1, CandidateTransfer::MAX_BYTES) &&
|
|
226
|
-
descriptor["campaign_execution"].is_a?(Hash) && descriptor["campaign_execution"].keys.sort == CampaignExecution::FIELDS &&
|
|
227
|
-
data["generation"].is_a?(Integer) && data["generation"].positive? &&
|
|
228
|
-
data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
229
|
-
transfer.is_a?(Hash) && transfer.keys.sort == %w[bytes parts sha256 version] && transfer["version"] == 1 &&
|
|
230
|
-
transfer["version"].is_a?(Integer) &&
|
|
231
|
-
transfer.values_at("bytes", "sha256") == descriptor.values_at("bytes", "sha256") && transfer["parts"] == [descriptor.slice("bytes", "sha256")]
|
|
232
|
-
raise AttemptErrors::EvidenceUnavailable, "original campaign candidate download differs"
|
|
233
|
-
end
|
|
234
|
-
identity = descriptor.fetch("original_worker_identity")
|
|
235
|
-
birth = identity.is_a?(Hash) && identity["started_at"]
|
|
236
|
-
Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: birth.to_s.split(":", -1)[1])
|
|
237
|
-
root = descriptor.fetch("original_worker_scratch_root")
|
|
238
|
-
unless root.is_a?(String) && root.valid_encoding? && root.bytesize.between?(2, 4096) &&
|
|
239
|
-
root.start_with?("/") && !root.include?("\0") && File.expand_path(root) == root
|
|
240
|
-
raise AttemptErrors::EvidenceUnavailable, "original campaign scratch root differs"
|
|
241
|
-
end
|
|
242
|
-
rescue ArgumentError, KeyError
|
|
243
|
-
raise AttemptErrors::EvidenceUnavailable, "original campaign candidate download differs"
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def prepared_transfer_codec(descriptor)
|
|
247
|
-
peer = @kernel.capture(Process.pid)
|
|
248
|
-
original = descriptor.fetch("original_worker_identity")
|
|
249
|
-
unless peer.values_at("uid", "gid", "groups") == original.values_at("uid", "gid", "groups")
|
|
250
|
-
raise AttemptErrors::UnauthorizedIdentity, "original prepared scratch principal differs"
|
|
251
|
-
end
|
|
252
|
-
TransferCodec.new(root: descriptor.fetch("original_worker_scratch_root"))
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
end
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "digest"
|
|
5
|
-
require_relative "../molecules/evidence_journal"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Authority
|
|
10
|
-
# Project commands share the installed journal, without manufacturing a
|
|
11
|
-
# managed task or projecting command exit as completed product delivery.
|
|
12
|
-
class CommandExecution
|
|
13
|
-
OPERATIONS = %w[command_admit command_status command_claim command_bind command_complete].freeze
|
|
14
|
-
ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}\z/
|
|
15
|
-
SHA = /\A[0-9a-f]{64}\z/
|
|
16
|
-
|
|
17
|
-
def initialize(project:, journal:, admission:)
|
|
18
|
-
@project, @journal, @admission = project, journal, admission
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
attr_reader :journal
|
|
22
|
-
|
|
23
|
-
def dispatch(request:, peer:, role:, deadline:)
|
|
24
|
-
operation = request.fetch("operation")
|
|
25
|
-
params = request.fetch("params")
|
|
26
|
-
raise ArgumentError, "invalid command project" unless request.fetch("project_id") == @project
|
|
27
|
-
id = params.fetch("execution_id")
|
|
28
|
-
raise ArgumentError, "invalid command reference" unless id.is_a?(String) && ID.match?(id)
|
|
29
|
-
if operation == "command_status"
|
|
30
|
-
strict!(params, %w[execution_id])
|
|
31
|
-
raise ArgumentError, "status mutation must be null" unless request.fetch("mutation_id").nil?
|
|
32
|
-
commit = @journal.ref_value
|
|
33
|
-
data = projection(id, commit)
|
|
34
|
-
@admission.command_reader!(peer: peer, definition: data.fetch("definition"), deadline: deadline)
|
|
35
|
-
return {data: data.merge("journal_commit" => commit), replayed: false}
|
|
36
|
-
end
|
|
37
|
-
strict!(params, case operation
|
|
38
|
-
when "command_admit" then %w[execution_id selection]
|
|
39
|
-
when "command_claim" then %w[execution_id caller]
|
|
40
|
-
when "command_bind" then %w[execution_id child]
|
|
41
|
-
when "command_complete" then %w[execution_id result]
|
|
42
|
-
else raise ArgumentError, "unknown command operation"
|
|
43
|
-
end)
|
|
44
|
-
authenticated_peer = peer
|
|
45
|
-
launcher = nil
|
|
46
|
-
plan = if operation == "command_admit"
|
|
47
|
-
@admission.command_request!(selection: params.fetch("selection"), peer: peer, deadline: deadline)
|
|
48
|
-
else
|
|
49
|
-
launcher = @admission.command_launcher!(peer: authenticated_peer, deadline: deadline)
|
|
50
|
-
nil
|
|
51
|
-
end
|
|
52
|
-
authenticate = lambda do
|
|
53
|
-
if operation == "command_admit"
|
|
54
|
-
unless @admission.command_request!(selection: params.fetch("selection"), peer: peer, deadline: deadline) == plan
|
|
55
|
-
raise AttemptErrors::UnauthorizedIdentity, "command admission changed"
|
|
56
|
-
end
|
|
57
|
-
else
|
|
58
|
-
unless @admission.command_launcher!(peer: authenticated_peer, deadline: deadline) == launcher
|
|
59
|
-
raise AttemptErrors::UnauthorizedIdentity, "installed command launcher changed"
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
peer = launcher if launcher
|
|
64
|
-
expected = {"command_admit" => 0, "command_claim" => 1, "command_bind" => 2, "command_complete" => 3}.fetch(operation)
|
|
65
|
-
digest = Atoms::EvidenceDigest.digest({"project" => @project, "params" => params, "peer" => peer, "plan" => plan})
|
|
66
|
-
@journal.mutate(assignment_id: namespace(id), attempt_id: namespace(id),
|
|
67
|
-
mutation_id: request.fetch("mutation_id"), operation: operation, parameters_digest: digest,
|
|
68
|
-
expected_generation: expected, with_replay: true, deadline: deadline,
|
|
69
|
-
replay_authorizer: authenticate) do |_events, commit, _generation|
|
|
70
|
-
authenticate.call
|
|
71
|
-
prior = projection(id, commit, missing: true)
|
|
72
|
-
data = transition(operation, id, params, plan, prior, peer, deadline)
|
|
73
|
-
{events: [], data: data}
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Read only the accepted launcher-owned four-stage execution at the
|
|
78
|
-
# caller's held CAS prefix. Arbitrary allowed commands are not tests.
|
|
79
|
-
def test_evidence_at!(execution_id:, commit:, head:, tree:, deadline:)
|
|
80
|
-
unless execution_id.is_a?(String) && ID.match?(execution_id) && commit && @journal.ref_value == commit
|
|
81
|
-
raise AttemptErrors::EvidenceUnavailable, "test execution prefix or selector differs"
|
|
82
|
-
end
|
|
83
|
-
data = projection(execution_id, commit)
|
|
84
|
-
definition = data.fetch("definition")
|
|
85
|
-
events = @journal.read_events(namespace(execution_id), commit: commit)
|
|
86
|
-
accepted = events.select { |event| event["type"] == "authority_mutation" }
|
|
87
|
-
stages = accepted.map { |event| event.fetch("payload").fetch("data") }
|
|
88
|
-
unless accepted.size == 4 && data["phase"] == "exited" && data["result"] == {"exit_status" => 0, "signal" => nil} &&
|
|
89
|
-
definition["purpose"] == "test" && definition["source"] == {"head" => head, "tree" => tree} &&
|
|
90
|
-
[head, tree].all? { |value| value.is_a?(String) && value.match?(/\A[0-9a-f]{40}\z/) } &&
|
|
91
|
-
stages.all? { |stage| stage.fetch("definition") == definition } &&
|
|
92
|
-
stages.map { |stage| stage.fetch("phase") } == %w[admitted claimed running exited] &&
|
|
93
|
-
stages.drop(1).all? { |stage| stage.fetch("launcher") == data.fetch("launcher") } &&
|
|
94
|
-
stages[2].fetch("child") == data.fetch("child") && data.dig("launcher", "uid") == 0 &&
|
|
95
|
-
data.dig("child", "witnessed_by") == data.fetch("launcher") &&
|
|
96
|
-
data.dig("child", "kernel_identity") == data.dig("child", "root_witness")&.slice("pid", "uid", "started_at", "host") &&
|
|
97
|
-
data.dig("child", "root_witness", "cwd") == definition.dig("executor", "cwd") &&
|
|
98
|
-
data.dig("child", "kernel_identity", "uid") == definition.dig("executor", "uid")
|
|
99
|
-
raise AttemptErrors::ReceiptRejected, "candidate requires an exact successful executed test command"
|
|
100
|
-
end
|
|
101
|
-
@admission.command_test_evidence!(definition: definition, deadline: deadline)
|
|
102
|
-
command = JSON.generate(definition.fetch("argv"))
|
|
103
|
-
raise AttemptErrors::ReceiptRejected, "executed test command reference is oversized" if command.bytesize > 2048
|
|
104
|
-
{"schema" => "ace.test.execution-reference/v1", "reference" => execution_id,
|
|
105
|
-
"candidate_head" => head, "command" => command, "result" => "passed",
|
|
106
|
-
"recorded_at" => accepted.last.fetch("recorded_at"), "receipt_sha256" => Atoms::EvidenceDigest.digest(data)}
|
|
107
|
-
rescue KeyError, TypeError
|
|
108
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical test execution is incomplete"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def transition(operation, id, params, plan, prior, peer, deadline)
|
|
114
|
-
base = {"schema" => "project-command/v1", "execution_id" => id, "project_id" => @project}
|
|
115
|
-
case operation
|
|
116
|
-
when "command_admit"
|
|
117
|
-
raise AttemptErrors::Conflict, "command already admitted" if prior
|
|
118
|
-
# Plan contains references and nonsecret context; its closed schema
|
|
119
|
-
# is owned by the installed Lab admission, never requester input.
|
|
120
|
-
base.merge("phase" => "admitted", "definition" => plan)
|
|
121
|
-
when "command_claim"
|
|
122
|
-
phase!(prior, "admitted")
|
|
123
|
-
@admission.command_claim!(definition: prior.fetch("definition"), caller: params.fetch("caller"),
|
|
124
|
-
launcher: peer, deadline: deadline)
|
|
125
|
-
prior.merge("phase" => "claimed", "launcher" => peer)
|
|
126
|
-
when "command_bind"
|
|
127
|
-
phase!(prior, "claimed")
|
|
128
|
-
launcher!(prior, peer)
|
|
129
|
-
child = @admission.command_child!(definition: prior.fetch("definition"), child: params.fetch("child"),
|
|
130
|
-
launcher: peer, deadline: deadline)
|
|
131
|
-
prior.merge("phase" => "running", "child" => child)
|
|
132
|
-
when "command_complete"
|
|
133
|
-
phase!(prior, "running")
|
|
134
|
-
launcher!(prior, peer)
|
|
135
|
-
result = params.fetch("result")
|
|
136
|
-
strict!(result, %w[exit_status signal])
|
|
137
|
-
exit_status, signal = result.values_at("exit_status", "signal")
|
|
138
|
-
unless (exit_status.is_a?(Integer) && exit_status.between?(0, 255) && signal.nil?) ||
|
|
139
|
-
(exit_status.nil? && signal.is_a?(Integer) && signal.between?(1, 127))
|
|
140
|
-
raise ArgumentError, "invalid command termination"
|
|
141
|
-
end
|
|
142
|
-
prior.merge("phase" => "exited", "result" => result)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def projection(id, commit, missing: false)
|
|
147
|
-
events = @journal.read_events(namespace(id), commit: commit)
|
|
148
|
-
unless Models::EvidenceEvent.chain_valid?(events)
|
|
149
|
-
raise AttemptErrors::EvidenceUnavailable, "command history is corrupt"
|
|
150
|
-
end
|
|
151
|
-
accepted = events.select { |event| event["type"] == "authority_mutation" }
|
|
152
|
-
if accepted.empty?
|
|
153
|
-
return nil if missing
|
|
154
|
-
raise AttemptErrors::NotFound, "command reference is unknown"
|
|
155
|
-
end
|
|
156
|
-
operations = accepted.map { |event| event.dig("payload", "operation") }
|
|
157
|
-
unless operations == OPERATIONS.reject { |name| name == "command_status" }.take(operations.size) && operations.size <= 4
|
|
158
|
-
raise AttemptErrors::EvidenceUnavailable, "command lifecycle is corrupt"
|
|
159
|
-
end
|
|
160
|
-
data = accepted.last.fetch("payload").fetch("data")
|
|
161
|
-
unless data.values_at("schema", "project_id", "execution_id") == ["project-command/v1", @project, id]
|
|
162
|
-
raise AttemptErrors::EvidenceUnavailable, "command association differs"
|
|
163
|
-
end
|
|
164
|
-
data
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def namespace(id) = "command-#{id}"
|
|
168
|
-
def strict!(value, keys)
|
|
169
|
-
raise ArgumentError, "invalid command fields" unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
170
|
-
end
|
|
171
|
-
def phase!(prior, phase)
|
|
172
|
-
raise AttemptErrors::Conflict, "command phase differs; inspect original execution" unless prior && prior["phase"] == phase
|
|
173
|
-
end
|
|
174
|
-
def launcher!(prior, peer)
|
|
175
|
-
raise AttemptErrors::UnauthorizedIdentity, "command launcher changed" unless prior.fetch("launcher") == peer
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|