ace-lab 0.8.0 → 0.13.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/CHANGELOG.md +363 -92
- data/README.md +66 -100
- data/docs/usage.md +529 -404
- data/lib/ace/lab/atoms/install_schema.rb +373 -0
- data/lib/ace/lab/cli/commands/install.rb +161 -0
- data/lib/ace/lab/cli/commands/prepare.rb +118 -0
- data/lib/ace/lab/cli/commands/project_add.rb +101 -0
- data/lib/ace/lab/cli/commands/status.rb +109 -0
- data/lib/ace/lab/cli/commands/verify.rb +115 -0
- data/lib/ace/lab/cli.rb +21 -13
- data/lib/ace/lab/install_manifest_path.rb +23 -0
- data/lib/ace/lab/models/status_report.rb +38 -0
- data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
- data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
- data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
- data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +49 -23
- data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
- data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
- data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
- data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
- data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
- data/lib/ace/lab/molecules/project_registry.rb +251 -0
- data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
- data/lib/ace/lab/molecules/role_verifier.rb +223 -0
- data/lib/ace/lab/organisms/install.rb +213 -0
- data/lib/ace/lab/organisms/prepare.rb +156 -0
- data/lib/ace/lab/organisms/project_add.rb +170 -0
- data/lib/ace/lab/organisms/status_service.rb +70 -0
- data/lib/ace/lab/organisms/verify.rb +768 -0
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +34 -6
- metadata +34 -102
- data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
- data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
- data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
- data/lib/ace/lab/atoms/service_input.rb +0 -92
- data/lib/ace/lab/cli/commands/assignment.rb +0 -45
- data/lib/ace/lab/cli/commands/service.rb +0 -240
- data/lib/ace/lab/models/service_receipt.rb +0 -18
- data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
- data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
- data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
- data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
- data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
- data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -139
- data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
- data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
- data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
- data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
- data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
- data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
- data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
- data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
- data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
- data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
- data/lib/ace/lab/molecules/service_executor.rb +0 -99
- data/lib/ace/lab/molecules/service_policy.rb +0 -289
- data/lib/ace/lab/organisms/authority_composition.rb +0 -87
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -149
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -123
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -688
- data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
- data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
- data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
- data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
- data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
- data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
- data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
- data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
- data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
- data/lib/ace/lab/organisms/service_request_service.rb +0 -249
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "json"
|
|
5
|
-
require "open3"
|
|
6
|
-
require_relative "protected_service_request"
|
|
7
|
-
require_relative "../molecules/ordinary_socket"
|
|
8
|
-
require "ace/assign/atoms/fixed_merge_schemas"
|
|
9
|
-
require "ace/assign/molecules/installed_journal_read"
|
|
10
|
-
|
|
11
|
-
module Ace
|
|
12
|
-
module Lab
|
|
13
|
-
module Organisms
|
|
14
|
-
# The ordinary fixed merge admission producer (spec 8x3.t.xz9.4). One
|
|
15
|
-
# mode of the maintained `service request/status` vocabulary: it
|
|
16
|
-
# canonicalizes the immutable request binding, authenticates the
|
|
17
|
-
# verified project overseer against the installed ordinary mapping,
|
|
18
|
-
# resolves the named queue entry and attempt, transports the request
|
|
19
|
-
# to the configured ordinary receiver, and emits the adopted ace-lab
|
|
20
|
-
# envelopes — never a second protocol. Refusals are pre-effect and
|
|
21
|
-
# carry no durable reference; unavailability is a caller-side blocker;
|
|
22
|
-
# the canonical Assign attempt/evidence state stays the sole
|
|
23
|
-
# queue/effect record, so this class owns no ledger.
|
|
24
|
-
class OrdinaryMergeRequest
|
|
25
|
-
MODE = "ordinary-merge"
|
|
26
|
-
OPERATION = "merge"
|
|
27
|
-
|
|
28
|
-
# The closed pre-effect refusal codes (exhaustive pre-effect mapping).
|
|
29
|
-
CODES = %w[ordinary_merge_invalid_request ordinary_merge_unauthenticated
|
|
30
|
-
ordinary_merge_forbidden ordinary_merge_selection_mismatch
|
|
31
|
-
ordinary_merge_authorization_invalid ordinary_merge_provenance_mismatch
|
|
32
|
-
ordinary_merge_target_changed ordinary_merge_evidence_invalid
|
|
33
|
-
ordinary_merge_request_conflict ordinary_merge_unavailable].freeze
|
|
34
|
-
|
|
35
|
-
# Selector keys this producer accepts; anything else — including the
|
|
36
|
-
# protected mapping/scope/service/generation selectors and any
|
|
37
|
-
# executable/argv/UID/socket/evidence-path/credential/token selector
|
|
38
|
-
# — refuses before effect.
|
|
39
|
-
REQUEST_KEYS = %i[project assignment attempt queue_entry operation input authorization
|
|
40
|
-
request_id mode].freeze
|
|
41
|
-
STATUS_KEYS = %i[project assignment attempt queue_entry reference request input_digest
|
|
42
|
-
mode format].freeze
|
|
43
|
-
|
|
44
|
-
ID = Molecules::ServicePolicy::ID
|
|
45
|
-
|
|
46
|
-
def initialize(topology: nil, policy: nil, coordinator: nil, repo_root: nil, ingress: nil)
|
|
47
|
-
@topology = topology || TopologyService.from_config
|
|
48
|
-
@repo_root = repo_root || git_toplevel
|
|
49
|
-
@coordinator = coordinator
|
|
50
|
-
@policy = policy || Molecules::ServicePolicy.new(
|
|
51
|
-
Molecules::GrantResolver.trusted_document(Ace::Lab.authorization_path)
|
|
52
|
-
)
|
|
53
|
-
@ingress = ingress
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def request(project:, assignment:, attempt:, queue_entry:, operation:, input:,
|
|
57
|
-
authorization:, request_id:, **options)
|
|
58
|
-
digest = nil
|
|
59
|
-
unless (options.keys - REQUEST_KEYS).empty?
|
|
60
|
-
invalid!("unknown request selector: #{unknown(options.keys, REQUEST_KEYS)}", request_id)
|
|
61
|
-
end
|
|
62
|
-
invalid!("operation must be merge", request_id) unless operation == OPERATION
|
|
63
|
-
identifiers!(project: project, assignment: assignment, attempt: attempt,
|
|
64
|
-
queue_entry: queue_entry, request_id: request_id)
|
|
65
|
-
unless authorization.is_a?(String) && authorization.match?(ID)
|
|
66
|
-
invalid!("invalid authorization reference", request_id)
|
|
67
|
-
end
|
|
68
|
-
document = load_request_document(input, request_id)
|
|
69
|
-
validate_document!(document, request_id)
|
|
70
|
-
digest = input_digest(project: project, assignment: assignment, attempt: attempt,
|
|
71
|
-
queue_entry: queue_entry, authorization: authorization, request_id: request_id,
|
|
72
|
-
document: document)
|
|
73
|
-
principal = authenticate!(project)
|
|
74
|
-
unless @coordinator
|
|
75
|
-
return with_installed_journal(project) { |producer| producer.request(project: project,
|
|
76
|
-
assignment: assignment, attempt: attempt, queue_entry: queue_entry, operation: operation,
|
|
77
|
-
input: input, authorization: authorization, request_id: request_id, **options) }
|
|
78
|
-
end
|
|
79
|
-
existing = @coordinator.fixed_merge_record(request_id)
|
|
80
|
-
if existing
|
|
81
|
-
return ack_envelope(existing) if exact_replay?(existing, project: project,
|
|
82
|
-
assignment: assignment, attempt: attempt, queue_entry: queue_entry,
|
|
83
|
-
authorization: authorization, digest: digest)
|
|
84
|
-
conflict!("request ID already exists with a different binding", request_id, digest)
|
|
85
|
-
end
|
|
86
|
-
receiver = receiver_mapping!(project)
|
|
87
|
-
resolve_selection!(project: project, assignment: assignment, attempt: attempt,
|
|
88
|
-
queue_entry: queue_entry)
|
|
89
|
-
authorize!(authorization, project: project, assignment: assignment, attempt: attempt,
|
|
90
|
-
queue_entry: queue_entry, document: document, digest: digest)
|
|
91
|
-
verify_candidate_head!(document)
|
|
92
|
-
submission = {"project_id" => project, "assignment_id" => assignment,
|
|
93
|
-
"attempt_id" => attempt, "queue_entry" => queue_entry, "operation" => OPERATION,
|
|
94
|
-
"authorization" => authorization, "request_id" => request_id,
|
|
95
|
-
"input_digest" => digest, "request" => document, "principal" => principal,
|
|
96
|
-
"receiver_id" => receiver.fetch("receiver_id"),
|
|
97
|
-
"configuration_sha256" => receiver.fetch("configuration_sha256")}
|
|
98
|
-
response = deliver(receiver, submission)
|
|
99
|
-
normalize_response(response, request_id: request_id, input_digest: digest)
|
|
100
|
-
rescue OrdinaryMergeRefusal => e
|
|
101
|
-
self.class.refusal_envelope(e.code, e.detail, request_id: e.request_id || request_id,
|
|
102
|
-
input_digest: e.input_digest || digest)
|
|
103
|
-
rescue Ace::Assign::AttemptErrors::AuthorizationConflict => e
|
|
104
|
-
self.class.refusal_envelope("ordinary_merge_authorization_invalid", e.message,
|
|
105
|
-
request_id: request_id, input_digest: digest)
|
|
106
|
-
rescue Ace::Assign::AttemptErrors::Conflict => e
|
|
107
|
-
self.class.refusal_envelope("ordinary_merge_request_conflict", e.message,
|
|
108
|
-
request_id: request_id, input_digest: digest)
|
|
109
|
-
rescue Molecules::OrdinarySocket::OrdinaryUnavailable => e
|
|
110
|
-
self.class.refusal_envelope("ordinary_merge_unavailable", e.message,
|
|
111
|
-
request_id: request_id, input_digest: digest)
|
|
112
|
-
rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
|
|
113
|
-
# Canonical evidence or installed configuration is unreadable: a
|
|
114
|
-
# caller-side blocker (endpoint/configuration failed), never a
|
|
115
|
-
# refusal and never a durable reference.
|
|
116
|
-
self.class.refusal_envelope("ordinary_merge_unavailable", e.message,
|
|
117
|
-
request_id: request_id, input_digest: digest)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Read-only status: resolve the opaque reference against the canonical
|
|
121
|
-
# record with every selector; performs no dispatch, retry,
|
|
122
|
-
# authorization refresh or selector repair. Read-only status survives
|
|
123
|
-
# rollback and receiver downtime because the canonical Assign state —
|
|
124
|
-
# never a receiver — owns the record.
|
|
125
|
-
def status(project:, assignment:, attempt:, queue_entry:, reference:, request:,
|
|
126
|
-
input_digest:, **options)
|
|
127
|
-
unless (options.keys - STATUS_KEYS).empty?
|
|
128
|
-
return status_error(self.class.refusal_envelope("ordinary_merge_invalid_request",
|
|
129
|
-
"unknown status selector: #{unknown(options.keys, STATUS_KEYS)}",
|
|
130
|
-
request_id: request, input_digest: nil,
|
|
131
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID))
|
|
132
|
-
end
|
|
133
|
-
identifiers!(project: project, assignment: assignment, attempt: attempt,
|
|
134
|
-
queue_entry: queue_entry, request_id: request)
|
|
135
|
-
unless options[:format] == "json"
|
|
136
|
-
invalid!("status requires --format json", request)
|
|
137
|
-
end
|
|
138
|
-
unless reference.is_a?(String) && Ace::Assign::Molecules::FixedEffectReference.valid?(reference)
|
|
139
|
-
invalid!("invalid reference", request)
|
|
140
|
-
end
|
|
141
|
-
unless input_digest.is_a?(String) && input_digest.match?(/\A[0-9a-f]{64}\z/)
|
|
142
|
-
invalid!("invalid input digest", request)
|
|
143
|
-
end
|
|
144
|
-
authenticate!(project)
|
|
145
|
-
unless @coordinator
|
|
146
|
-
return with_installed_journal(project) { |producer| producer.status(project: project,
|
|
147
|
-
assignment: assignment, attempt: attempt, queue_entry: queue_entry, reference: reference,
|
|
148
|
-
request: request, input_digest: input_digest, **options) }
|
|
149
|
-
end
|
|
150
|
-
record = @coordinator.fixed_merge_status(reference: reference, request_id: request,
|
|
151
|
-
project_id: project, assignment_id: assignment, attempt_id: attempt,
|
|
152
|
-
queue_entry: queue_entry, input_digest: input_digest)
|
|
153
|
-
# Unknown or mismatched references answer identically, without
|
|
154
|
-
# revealing whether another project holds the reference.
|
|
155
|
-
return status_error(selection_error) unless record
|
|
156
|
-
{"status" => "ok", "data" => {
|
|
157
|
-
"schema" => Ace::Assign::Atoms::FixedMergeSchemas::STATUS_SCHEMA_ID,
|
|
158
|
-
"state" => record.fetch("state"),
|
|
159
|
-
"reference" => record.fetch("reference"),
|
|
160
|
-
"request_id" => record.fetch("request_id"),
|
|
161
|
-
"input_digest" => record.fetch("input_digest"),
|
|
162
|
-
"receipt" => %w[pending uncertain].include?(record.fetch("state")) ? nil : record["receipt"]
|
|
163
|
-
}}
|
|
164
|
-
rescue OrdinaryMergeRefusal => e
|
|
165
|
-
status_error(self.class.refusal_envelope(e.code, e.detail, request_id: request, input_digest: nil,
|
|
166
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID))
|
|
167
|
-
rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
|
|
168
|
-
# Canonical state or installed configuration unreadable: the status
|
|
169
|
-
# command degrades to the caller-side blocker envelope, never to a
|
|
170
|
-
# fabricated state or a raw crash.
|
|
171
|
-
status_error(self.class.refusal_envelope("ordinary_merge_unavailable", e.message,
|
|
172
|
-
request_id: request, input_digest: nil,
|
|
173
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID))
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# ---- envelope composition (the adopted ace-lab vocabulary) ---------
|
|
177
|
-
|
|
178
|
-
def ack_envelope(record)
|
|
179
|
-
{"status" => "ok", "data" => {
|
|
180
|
-
"schema" => Ace::Assign::Atoms::FixedMergeSchemas::ACK_SCHEMA_ID,
|
|
181
|
-
"type" => "accepted",
|
|
182
|
-
"request_id" => record.fetch("request_id"),
|
|
183
|
-
"input_digest" => record.fetch("input_digest"),
|
|
184
|
-
"reference" => record.fetch("reference")
|
|
185
|
-
}}
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def self.sanitize(message)
|
|
189
|
-
cleaned = message.to_s.scrub.gsub(/\s+/, " ").strip[0, 512]
|
|
190
|
-
cleaned.empty? ? "ordinary merge request refused" : cleaned
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Envelope composition is instance-independent; the CLI reuses it for
|
|
194
|
-
# mode-selection refusals before any producer exists.
|
|
195
|
-
def self.refusal_envelope(code, message, request_id:, input_digest:,
|
|
196
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::REFUSAL_SCHEMA_ID)
|
|
197
|
-
raise ArgumentError, "unknown ordinary merge refusal code" unless CODES.include?(code)
|
|
198
|
-
echo = request_id.to_s[0, 256]
|
|
199
|
-
echo = "unknown" if echo.empty?
|
|
200
|
-
{"status" => "error", "error" => {
|
|
201
|
-
"schema" => schema,
|
|
202
|
-
"code" => code,
|
|
203
|
-
"message" => sanitize(message),
|
|
204
|
-
"request_id" => echo,
|
|
205
|
-
"input_digest" => input_digest
|
|
206
|
-
}}
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
private
|
|
210
|
-
|
|
211
|
-
def with_installed_journal(project)
|
|
212
|
-
receiver = @policy.ordinary_receiver!(project)
|
|
213
|
-
unless receiver && %w[journal_repository journal_ref].all? { |key| receiver.key?(key) }
|
|
214
|
-
raise Ace::Lab::InvalidConfigurationError, "installed ordinary journal selection is missing"
|
|
215
|
-
end
|
|
216
|
-
repository, ref = receiver.values_at("journal_repository", "journal_ref")
|
|
217
|
-
boundary = Ace::Assign::Molecules::InstalledJournalRead.new(repo_root: repository,
|
|
218
|
-
ref: ref, owner_uid: receiver.fetch("receiver_uid"))
|
|
219
|
-
boundary.with(deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30) do |view|
|
|
220
|
-
journal = Ace::Assign::Molecules::EvidenceJournal.new(repo_root: repository,
|
|
221
|
-
ref: ref, checkout_root: File.join(repository, ".unused-reader"), read_boundary: view)
|
|
222
|
-
coordinator = Ace::Assign::Organisms::AttemptCoordinator.new(repo_root: @repo_root, journal: journal)
|
|
223
|
-
yield self.class.new(topology: @topology, policy: @policy, repo_root: @repo_root,
|
|
224
|
-
coordinator: coordinator, ingress: @ingress)
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# The immutable binding digest — spec ruling r4: lowercase hex SHA-256
|
|
229
|
-
# over the canonical JSON (recursively key-sorted, compact, UTF-8) of
|
|
230
|
-
# the full eight-field binding. Computed once here at admission; the
|
|
231
|
-
# same shared implementation re-derives it for replay and receipt
|
|
232
|
-
# verification, so the pair can never diverge.
|
|
233
|
-
def input_digest(project:, assignment:, attempt:, queue_entry:, authorization:,
|
|
234
|
-
request_id:, document:)
|
|
235
|
-
Ace::Assign::Atoms::FixedMergeSchemas.canonical_request_digest(
|
|
236
|
-
project: project, assignment: assignment, attempt: attempt,
|
|
237
|
-
queue_entry: queue_entry, authorization: authorization,
|
|
238
|
-
request_id: request_id, input: document
|
|
239
|
-
)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def validate_document!(document, request_id)
|
|
243
|
-
Ace::Assign::Atoms::FixedMergeSchemas.validate_request!(document)
|
|
244
|
-
Ace::Assign::Atoms::FixedMergeSchemas.require_request_joins!(document)
|
|
245
|
-
rescue Ace::Assign::Atoms::FixedMergeSchemas::SchemaError => e
|
|
246
|
-
invalid!(e.message, request_id)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# The verified local process owner is the only caller identity; it
|
|
250
|
-
# must be the configured ordinary project overseer principal.
|
|
251
|
-
def authenticate!(project)
|
|
252
|
-
identities = Molecules::CallerAuthorizer.local_identity
|
|
253
|
-
refused("ordinary_merge_unauthenticated", "verified local identity is unavailable") if identities.empty?
|
|
254
|
-
receiver = @policy.ordinary_receiver!(project)
|
|
255
|
-
unless receiver.is_a?(Hash) && identities.include?(receiver.fetch("principal"))
|
|
256
|
-
refused("ordinary_merge_forbidden",
|
|
257
|
-
"authenticated principal is not the configured ordinary project overseer")
|
|
258
|
-
end
|
|
259
|
-
receiver.fetch("principal")
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# The installed ordinary section supplies identity and placement only.
|
|
263
|
-
# A disabled or absent admission mapping is rollback: a caller-side
|
|
264
|
-
# blocker that never redirects ordinary callers and never deletes the
|
|
265
|
-
# canonical records — read-only status stays fully available.
|
|
266
|
-
def receiver_mapping!(project)
|
|
267
|
-
receiver = @policy.ordinary_receiver!(project)
|
|
268
|
-
if receiver.nil? || receiver.fetch("admission", "enabled") != "enabled"
|
|
269
|
-
raise Molecules::OrdinarySocket::OrdinaryUnavailable,
|
|
270
|
-
"ordinary merge admission is not available in the installed configuration"
|
|
271
|
-
end
|
|
272
|
-
receiver.merge("configuration_sha256" => Molecules::ServicePolicy.configuration_digest(@policy.document))
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def resolve_selection!(project:, assignment:, attempt:, queue_entry:)
|
|
276
|
-
@coordinator.service_attempt("assignment_id" => assignment, "attempt_id" => attempt,
|
|
277
|
-
"project_id" => project)
|
|
278
|
-
rescue Ace::Assign::AttemptErrors::NotFound, Ace::Assign::AttemptErrors::ReceiptRejected,
|
|
279
|
-
Ace::Assign::AttemptErrors::InvalidState => e
|
|
280
|
-
refused("ordinary_merge_selection_mismatch", e.message)
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def authorize!(authorization, project:, assignment:, attempt:, queue_entry:, document:, digest:)
|
|
284
|
-
binding = {"operation" => OPERATION, "project_id" => project,
|
|
285
|
-
"assignment_id" => assignment, "attempt_id" => attempt, "queue_entry" => queue_entry,
|
|
286
|
-
"input_digest" => digest, "forge" => document.fetch("forge"),
|
|
287
|
-
"pr_url" => document.fetch("pr_url"), "head_repository" => document.fetch("head_repository"),
|
|
288
|
-
"head_ref" => document.fetch("head_ref"), "candidate_head" => document.fetch("candidate_head"),
|
|
289
|
-
"base_repository" => document.fetch("base_repository"), "base_ref" => document.fetch("base_ref"),
|
|
290
|
-
"expected_base_head" => document.fetch("expected_base_head")}
|
|
291
|
-
if authorization.start_with?("proposal-")
|
|
292
|
-
@coordinator.proposal_authorization(authorization, binding.slice(
|
|
293
|
-
"operation", "project_id", "assignment_id", "attempt_id", "input_digest", "candidate_head").merge(
|
|
294
|
-
"target" => {"resource" => document.fetch("pr_url"), "artifact_digest" => nil},
|
|
295
|
-
"caller_uid" => Process.uid))
|
|
296
|
-
else
|
|
297
|
-
@policy.authorize_ordinary!(authorization, binding)
|
|
298
|
-
end
|
|
299
|
-
rescue SecurityError, Ace::Assign::AttemptErrors::ReceiptRejected => e
|
|
300
|
-
# Spec table (ruling r4): the authorization decision or reference is
|
|
301
|
-
# invalid for this request — missing, expired, revoked, out of scope,
|
|
302
|
-
# bound to another operation/project/PR/head, or not an ordinary
|
|
303
|
-
# merge decision at all. Authorization-decision mismatches NEVER use
|
|
304
|
-
# provenance_mismatch; that code is reserved for registered
|
|
305
|
-
# provenance drift verified by the receiver (and relayed verbatim).
|
|
306
|
-
refused("ordinary_merge_authorization_invalid", e.message)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def verify_candidate_head!(document)
|
|
310
|
-
unless current_head == document.fetch("candidate_head")
|
|
311
|
-
refused("ordinary_merge_target_changed", "candidate head moved from the immutable request")
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def deliver(receiver, submission)
|
|
316
|
-
ingress = @ingress || default_ingress(receiver)
|
|
317
|
-
ingress.call(submission: submission)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def default_ingress(receiver)
|
|
321
|
-
socket = Molecules::OrdinarySocket.new(socket_path: receiver.fetch("socket_path"),
|
|
322
|
-
receiver_uid: receiver.fetch("receiver_uid"))
|
|
323
|
-
->(submission:) { socket.submit(submission) }
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# A valid receiver acknowledgment is relayed exactly; a valid receiver
|
|
327
|
-
# refusal is relayed with its closed code; anything else is a lost or
|
|
328
|
-
# unauthenticated response — a caller-side blocker, never a proven
|
|
329
|
-
# refusal and never a resubmission authorization.
|
|
330
|
-
def normalize_response(response, request_id:, input_digest:)
|
|
331
|
-
unless response.is_a?(Hash) && (response.keys - %w[status data error]).empty? &&
|
|
332
|
-
%w[ok error].include?(response["status"])
|
|
333
|
-
raise Molecules::OrdinarySocket::OrdinaryUnavailable, "receiver response envelope is invalid"
|
|
334
|
-
end
|
|
335
|
-
return normalize_ack(response, request_id: request_id, input_digest: input_digest) if
|
|
336
|
-
response.fetch("status") == "ok"
|
|
337
|
-
normalize_refusal(response, request_id: request_id, input_digest: input_digest)
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def normalize_ack(response, request_id:, input_digest:)
|
|
341
|
-
data = response.fetch("data") { {} }
|
|
342
|
-
expected = %w[schema type request_id input_digest reference]
|
|
343
|
-
digest = data.is_a?(Hash) && data["input_digest"]
|
|
344
|
-
unless data.is_a?(Hash) && (data.keys - expected).empty? &&
|
|
345
|
-
expected.all? { |key| data[key].is_a?(String) && !data[key].empty? } &&
|
|
346
|
-
data.fetch("schema") == Ace::Assign::Atoms::FixedMergeSchemas::ACK_SCHEMA_ID &&
|
|
347
|
-
data.fetch("type") == "accepted" && data.fetch("request_id") == request_id &&
|
|
348
|
-
digest == input_digest && digest.match?(/\A[0-9a-f]{64}\z/) &&
|
|
349
|
-
Ace::Assign::Molecules::FixedEffectReference.valid?(data.fetch("reference"))
|
|
350
|
-
raise Molecules::OrdinarySocket::OrdinaryUnavailable,
|
|
351
|
-
"receiver acknowledgment is invalid or lost before durability could be confirmed"
|
|
352
|
-
end
|
|
353
|
-
{"status" => "ok", "data" => data.slice(*expected)}
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
def normalize_refusal(response, request_id:, input_digest:)
|
|
357
|
-
error = response.fetch("error") { {} }
|
|
358
|
-
expected = %w[schema code message request_id input_digest]
|
|
359
|
-
valid = error.is_a?(Hash) && error.keys.sort == expected.sort &&
|
|
360
|
-
error.fetch("schema") == Ace::Assign::Atoms::FixedMergeSchemas::REFUSAL_SCHEMA_ID &&
|
|
361
|
-
CODES.include?(error.fetch("code")) &&
|
|
362
|
-
error.fetch("message").is_a?(String) && !error.fetch("message").empty? &&
|
|
363
|
-
error.fetch("message").length <= 512 &&
|
|
364
|
-
error.fetch("request_id") == request_id &&
|
|
365
|
-
(error.fetch("input_digest").nil? || error.fetch("input_digest") == input_digest)
|
|
366
|
-
unless valid
|
|
367
|
-
raise Molecules::OrdinarySocket::OrdinaryUnavailable, "receiver refusal envelope is invalid"
|
|
368
|
-
end
|
|
369
|
-
{"status" => "error", "error" => error.slice(*expected)}
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
def selection_error
|
|
373
|
-
self.class.refusal_envelope("ordinary_merge_selection_mismatch",
|
|
374
|
-
"reference and selectors do not match a canonical ordinary merge record",
|
|
375
|
-
request_id: nil, input_digest: nil,
|
|
376
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID)
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
def status_error(envelope)
|
|
380
|
-
{"status" => "error", "error" => envelope.fetch("error")}
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
def identifiers!(project:, assignment:, attempt:, queue_entry:, request_id:)
|
|
384
|
-
{"project" => project, "assignment" => assignment, "attempt" => attempt,
|
|
385
|
-
"queue_entry" => queue_entry, "request_id" => request_id}.each do |name, value|
|
|
386
|
-
invalid!("invalid #{name} ID", request_id) unless value.is_a?(String) && value.match?(ID)
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
def load_request_document(input, request_id)
|
|
391
|
-
Atoms::ServiceInput.load(input)
|
|
392
|
-
rescue ArgumentError => e
|
|
393
|
-
invalid!(e.message, request_id)
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def unknown(keys, allowed)
|
|
397
|
-
(keys - allowed).sort.join(", ")
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
# A pre-effect refusal carries its closed code, sanitized message and
|
|
401
|
-
# the request binding for the wire envelope — never a reference.
|
|
402
|
-
OrdinaryMergeRefusal = Class.new(::StandardError) do
|
|
403
|
-
attr_reader :code, :detail, :request_id, :input_digest
|
|
404
|
-
|
|
405
|
-
def initialize(code, detail, request_id: nil, input_digest: nil)
|
|
406
|
-
@code = code
|
|
407
|
-
@detail = detail
|
|
408
|
-
@request_id = request_id
|
|
409
|
-
@input_digest = input_digest
|
|
410
|
-
super(detail)
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def invalid!(message, request_id)
|
|
415
|
-
raise OrdinaryMergeRefusal.new("ordinary_merge_invalid_request", message, request_id: request_id)
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def conflict!(message, request_id, digest)
|
|
419
|
-
raise OrdinaryMergeRefusal.new("ordinary_merge_request_conflict", message,
|
|
420
|
-
request_id: request_id, input_digest: digest)
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
# A replay must match every caller-supplied immutable field: command
|
|
424
|
-
# selectors, authorization, input byte meaning and canonical digest.
|
|
425
|
-
def exact_replay?(existing, project:, assignment:, attempt:, queue_entry:,
|
|
426
|
-
authorization:, digest:)
|
|
427
|
-
existing["state"] &&
|
|
428
|
-
existing.slice("project_id", "assignment_id", "attempt_id", "queue_entry",
|
|
429
|
-
"authorization", "input_digest") == {
|
|
430
|
-
"project_id" => project, "assignment_id" => assignment, "attempt_id" => attempt,
|
|
431
|
-
"queue_entry" => queue_entry, "authorization" => authorization, "input_digest" => digest
|
|
432
|
-
}
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
def refused(code, message)
|
|
436
|
-
raise OrdinaryMergeRefusal.new(code, message)
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
# The git toplevel (not a package directory that happens to hold a
|
|
440
|
-
# Rakefile) bounds candidate-head verification for nested invocations.
|
|
441
|
-
def git_toplevel
|
|
442
|
-
out, status = Open3.capture2("git", "rev-parse", "--show-toplevel")
|
|
443
|
-
(status&.success? ? out.strip : Dir.pwd)
|
|
444
|
-
rescue Errno::ENOENT
|
|
445
|
-
Dir.pwd
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def current_head
|
|
449
|
-
out, _stderr, status = Open3.capture3("git", "rev-parse", "HEAD", chdir: @repo_root)
|
|
450
|
-
refused("ordinary_merge_target_changed", "candidate head is unavailable") unless status.success?
|
|
451
|
-
out.strip
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
end
|