ace-assign 0.67.0 → 0.68.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.
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "canonical_attempt_state"
4
+ require_relative "fixed_effect_reference"
5
+ require_relative "../atoms/fixed_merge_schemas"
4
6
 
5
7
  require "digest"
6
8
  require "fileutils"
@@ -32,6 +34,7 @@ module Ace
32
34
  class EvidenceJournal
33
35
  include JournalMutation
34
36
  include ProposalJournal
37
+
35
38
  CAS_ATTEMPTS = 3
36
39
  HISTORY_LIMIT = 100_000
37
40
  EVENT_BATCH_BYTES = 1024 * 1024
@@ -204,8 +207,10 @@ module Ace
204
207
  def event_commits_for_assignments!(selectors:, commit:)
205
208
  unless selectors.is_a?(Hash) && !selectors.empty? && selectors.size <= HISTORY_LIMIT &&
206
209
  selectors.keys.all? { |id| id.is_a?(String) && id.match?(JournalMutation::ID) } &&
207
- selectors.values.all? { |digests| digests.is_a?(Array) && !digests.empty? &&
208
- digests.uniq.size == digests.size && digests.all? { |digest| digest.is_a?(String) && digest.match?(/\A[0-9a-f]{64}\z/) } } &&
210
+ selectors.values.all? { |digests|
211
+ digests.is_a?(Array) && !digests.empty? &&
212
+ digests.uniq.size == digests.size && digests.all? { |digest| digest.is_a?(String) && digest.match?(/\A[0-9a-f]{64}\z/) }
213
+ } &&
209
214
  selectors.values.sum(&:size) <= HISTORY_LIMIT
210
215
  raise AttemptErrors::EvidenceUnavailable, "historical event selectors are invalid"
211
216
  end
@@ -400,9 +405,12 @@ module Ace
400
405
  end
401
406
  modes = match.values_at(1, 2)
402
407
  unless modes.all? { |mode| %w[000000 100644 100755 120000].include?(mode) } &&
403
- (status == "A" ? old == "0" * 40 && fresh != "0" * 40 && modes.first == "000000" && modes.last != "000000" :
404
- status == "D" ? old != "0" * 40 && fresh == "0" * 40 && modes.first != "000000" && modes.last == "000000" :
405
- old != "0" * 40 && fresh != "0" * 40 && modes.none? { |mode| mode == "000000" })
408
+ (if status == "A"
409
+ old == "0" * 40 && fresh != "0" * 40 && modes.first == "000000" && modes.last != "000000"
410
+ else
411
+ (status == "D") ? old != "0" * 40 && fresh == "0" * 40 && modes.first != "000000" && modes.last == "000000" :
412
+ old != "0" * 40 && fresh != "0" * 40 && modes.none? { |mode| mode == "000000" }
413
+ end)
406
414
  raise AttemptErrors::EvidenceUnavailable, "historical event object type differs"
407
415
  end
408
416
  result.fetch(commit) << [path, status, old, fresh]
@@ -815,8 +823,10 @@ module Ace
815
823
  rescue Errno::EACCES, Errno::ELOOP
816
824
  false
817
825
  end
818
- raise AttemptErrors::ReceiptRejected,
819
- "Service terminal receipt evidence is unverifiable: #{item["ref"]}" unless intact
826
+ unless intact
827
+ raise AttemptErrors::ReceiptRejected,
828
+ "Service terminal receipt evidence is unverifiable: #{item["ref"]}"
829
+ end
820
830
  verify_service_attestation!(File.read(real), current, state)
821
831
  end
822
832
  end
@@ -1224,13 +1234,13 @@ module Ace
1224
1234
 
1225
1235
  def raise_authorization_conflict!(conflict)
1226
1236
  raise AttemptErrors::Conflict,
1227
- "Authorization reference already consumed by request #{conflict.fetch('request_id')}"
1237
+ "Authorization reference already consumed by request #{conflict.fetch("request_id")}"
1228
1238
  end
1229
1239
 
1230
1240
  def verify_service_attestation!(content, request, state)
1231
1241
  expression = /^ace-service-attestation request:#{Regexp.escape(request["request_id"])} input:#{Regexp.escape(request["input_digest"])} outcome:(\S+)( no-effect:(\S+))?$/
1232
1242
  attested = content.scan(expression).first
1233
- outcome = state == "failed-settled" ? "failed" : state
1243
+ outcome = (state == "failed-settled") ? "failed" : state
1234
1244
  unless attested && attested.first == outcome && (state != "failed-settled" || attested[2] == "true")
1235
1245
  raise AttemptErrors::ReceiptRejected, "Service terminal evidence does not attest #{outcome}"
1236
1246
  end
@@ -1245,6 +1255,267 @@ module Ace
1245
1255
  raise ArgumentError, "invalid service request ID"
1246
1256
  end
1247
1257
 
1258
+ # ---- ordinary fixed merge records (spec 8x3.t.xz9.4) --------------
1259
+ #
1260
+ # The canonical Assign attempt/evidence state is the SOLE queue/effect
1261
+ # record for ordinary fixed merge requests: admitted records live in
1262
+ # this evidence ref, chained to their owning attempt, and the issuing
1263
+ # receiver keeps no journal, mapping database or receipt ledger. The
1264
+ # closed state model is pending -> uncertain -> succeeded/failed;
1265
+ # pre-effect refusals are never durable (a refusal has no reference),
1266
+ # so every recorded request is an admitted one.
1267
+
1268
+ FIXED_MERGE_STATES = %w[pending uncertain succeeded failed].freeze
1269
+ # Immutable binding: a replay under the same request ID must match
1270
+ # every caller-supplied field, or it refuses before effect. The
1271
+ # opaque reference is receiver-issued, never caller-supplied, and
1272
+ # replays return the originally issued one.
1273
+ FIXED_MERGE_BINDING_FIELDS = %w[request_id project_id assignment_id attempt_id
1274
+ queue_entry operation authorization input_digest request principal receiver_id
1275
+ configuration_sha256 execution_identity].freeze
1276
+ FIXED_MERGE_MUTABLE_FIELDS = %w[state receipt reference recorded_at claimed_at finished_at].freeze
1277
+
1278
+ def fixed_merge_request(request_id, commit: ref_value)
1279
+ return nil unless commit
1280
+ out, stderr, status = git("show", "#{commit}:#{fixed_merge_request_path(request_id)}")
1281
+ if status.success?
1282
+ record = JSON.parse(out)
1283
+ verify_fixed_record!(record, commit: commit) if @mode == :protected
1284
+ return record
1285
+ end
1286
+ return nil if stderr.include?("does not exist") || stderr.include?("exists on disk")
1287
+ raise AttemptErrors::EvidenceUnavailable, "Cannot read fixed merge request: #{stderr}"
1288
+ rescue JSON::ParserError
1289
+ raise AttemptErrors::EvidenceUnavailable, "Corrupt fixed merge request #{request_id}"
1290
+ end
1291
+
1292
+ def fixed_merge_request_records(commit: ref_value)
1293
+ value = commit
1294
+ return [] unless value
1295
+ paths, stderr, status = git("ls-tree", "-r", "--name-only", value, "--", "execution/merge-requests/")
1296
+ raise AttemptErrors::EvidenceUnavailable, "Cannot read fixed merge index: #{stderr}" unless status.success?
1297
+ paths.lines.map(&:strip).select { |path| path.end_with?(".json") }.filter_map do |path|
1298
+ content, error, read_status = git("show", "#{value}:#{path}")
1299
+ unless read_status.success?
1300
+ raise AttemptErrors::EvidenceUnavailable, "Cannot read fixed merge request #{path}: #{error}"
1301
+ end
1302
+ record = JSON.parse(content)
1303
+ verify_fixed_record!(record, commit: value) if @mode == :protected
1304
+ record
1305
+ end
1306
+ rescue JSON::ParserError
1307
+ raise AttemptErrors::EvidenceUnavailable, "Corrupt fixed merge request index"
1308
+ end
1309
+
1310
+ # Admit an ordinary fixed merge request: one CAS create that binds the
1311
+ # receiver-issued opaque reference before the record can ever be read
1312
+ # back. An identical binding under the same request ID replays the
1313
+ # original record (the reference is created once); any different
1314
+ # field is a conflict, never a replay. Another live record holding
1315
+ # the same exact authorization is refused: only a terminal record
1316
+ # with proven no-effect evidence frees an authorization.
1317
+ def admit_fixed_merge_request(record, guard: nil)
1318
+ reference = record.fetch("reference")
1319
+ raise ArgumentError, "fixed merge record needs an opaque reference" unless Molecules::FixedEffectReference.valid?(reference)
1320
+ raise ArgumentError, "invalid fixed merge state" unless record.fetch("state") == "pending"
1321
+ update_fixed_merge_request(record.fetch("request_id"), expected: nil,
1322
+ replacement: record, event_type: "merge_admission", guard: guard)
1323
+ end
1324
+
1325
+ # Claim the start of the fixed effect: pending -> uncertain in one
1326
+ # CAS transition, so a receiver restart can never dispatch a second
1327
+ # merge for the same admitted record — begin replays as already
1328
+ # started, and only the owning effect may later settle it.
1329
+ def begin_fixed_merge_request(request_id)
1330
+ current = fixed_merge_request(request_id)
1331
+ raise AttemptErrors::NotFound, "Fixed merge request #{request_id} not found" unless current
1332
+ return current if current["state"] == "uncertain"
1333
+ unless current["state"] == "pending"
1334
+ raise AttemptErrors::InvalidState, "Fixed merge request #{request_id} is terminal"
1335
+ end
1336
+ update_fixed_merge_request(request_id, expected: current,
1337
+ replacement: current.merge("state" => "uncertain", "claimed_at" => Time.now.utc.iso8601(9)),
1338
+ event_type: "merge_transition")
1339
+ end
1340
+
1341
+ # Terminal settlement: succeeded/failed with the exact receipt
1342
+ # validated HERE at the journal boundary — structure, the closed
1343
+ # terminal invariants, and the binding against the current record.
1344
+ # Failure is terminal only with immutable no-effect evidence.
1345
+ def settle_fixed_merge_request(request_id, terminal_status:, receipt:)
1346
+ unless FIXED_MERGE_STATES.include?(terminal_status) && %w[succeeded failed].include?(terminal_status)
1347
+ raise ArgumentError, "invalid fixed merge terminal status"
1348
+ end
1349
+ current = fixed_merge_request(request_id)
1350
+ raise AttemptErrors::NotFound, "Fixed merge request #{request_id} not found" unless current
1351
+ unless %w[pending uncertain].include?(current["state"])
1352
+ raise AttemptErrors::InvalidState, "Fixed merge request #{request_id} is terminal"
1353
+ end
1354
+ validate_fixed_receipt!(current, terminal_status, receipt)
1355
+ update_fixed_merge_request(request_id, expected: current,
1356
+ replacement: current.merge("state" => terminal_status, "receipt" => receipt,
1357
+ "finished_at" => Time.now.utc.iso8601(9)),
1358
+ event_type: "merge_transition")
1359
+ end
1360
+
1361
+ def fixed_merge_request_path(request_id)
1362
+ "execution/merge-requests/#{request_id}.json"
1363
+ end
1364
+
1365
+ def validate_fixed_receipt!(current, terminal_status, receipt)
1366
+ Atoms::FixedMergeSchemas.validate_receipt!(receipt)
1367
+ Atoms::FixedMergeSchemas.validate_receipt_against!(receipt, current)
1368
+ unless receipt.fetch("terminal_status") == terminal_status
1369
+ raise AttemptErrors::ReceiptRejected, "Fixed merge receipt terminal status does not match the settlement"
1370
+ end
1371
+ true
1372
+ rescue Atoms::FixedMergeSchemas::SchemaError => e
1373
+ raise AttemptErrors::ReceiptRejected, "Fixed merge receipt is invalid: #{e.message}"
1374
+ end
1375
+
1376
+ # Another live fixed merge record holding the same exact
1377
+ # authorization. Terminal failure with intact no-effect evidence
1378
+ # proves the effect never happened and frees the reference.
1379
+ def fixed_authorization_conflict(record)
1380
+ authorization = record["authorization"]
1381
+ return nil unless authorization.is_a?(String) && !authorization.empty?
1382
+ fixed_merge_request_records.find do |other|
1383
+ next false if other["request_id"] == record.fetch("request_id")
1384
+ next false if other["state"] == "failed" && fixed_no_effect_intact?(other)
1385
+ other["authorization"] == authorization &&
1386
+ other["operation"] == record.fetch("operation") &&
1387
+ other["project_id"] == record.fetch("project_id")
1388
+ end
1389
+ end
1390
+
1391
+ def fixed_no_effect_intact?(record)
1392
+ receipt = record["receipt"]
1393
+ return false unless receipt.is_a?(Hash)
1394
+ evidence = receipt.dig("effect", "no_effect_evidence")
1395
+ return false unless evidence.is_a?(Hash)
1396
+ repo_root = File.realpath(@repo_root)
1397
+ path = File.expand_path(evidence["reference"].to_s, repo_root)
1398
+ real = begin
1399
+ File.realpath(path)
1400
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
1401
+ return false
1402
+ end
1403
+ real.start_with?(repo_root + File::SEPARATOR) &&
1404
+ Digest::SHA256.file(real).hexdigest == evidence.fetch("sha256")
1405
+ rescue Errno::ENOENT, AttemptErrors::EvidenceUnavailable
1406
+ false
1407
+ end
1408
+
1409
+ def update_fixed_merge_request(request_id, expected:, replacement:, event_type:, guard: nil)
1410
+ unless %w[merge_admission merge_transition].include?(event_type)
1411
+ raise ArgumentError, "invalid fixed merge event type"
1412
+ end
1413
+ with_lock do
1414
+ CAS_ATTEMPTS.times do
1415
+ old = ref_value
1416
+ ensure_checkout!
1417
+ old = ref_value if old.nil?
1418
+ sync_checkout(old)
1419
+ prepared = prepare_fixed_update(request_id: request_id, expected: expected,
1420
+ replacement: replacement, event_type: event_type, guard: guard)
1421
+ return prepared.fetch(:record) if prepared[:replayed]
1422
+ path = fixed_merge_request_path(request_id)
1423
+ full = File.join(checkout_dir, path)
1424
+ FileUtils.mkdir_p(File.dirname(full))
1425
+ File.write(full, JSON.pretty_generate(prepared.fetch(:record)))
1426
+ assignment_id = replacement.fetch("assignment_id")
1427
+ attempt_id = replacement.fetch("attempt_id")
1428
+ prior = read_events(assignment_id).reverse
1429
+ .find { |entry| entry["attempt_id"] == attempt_id }&.fetch("digest")
1430
+ event = Models::EvidenceEvent.build(type: event_type, attempt_id: attempt_id,
1431
+ payload: prepared.fetch(:event).fetch(:payload), previous_digest: prior)
1432
+ write_event_files(assignment_id, [event])
1433
+ git!("-C", checkout_dir, "add", "--", path, "execution/#{assignment_id}/events")
1434
+ state = replacement.fetch("state")
1435
+ git!("-C", checkout_dir, "-c", "user.name=ace-assign", "-c", "user.email=ace-assign@localhost",
1436
+ "commit", "-m", "evidence: fixed merge request #{request_id} #{state}")
1437
+ commit = git!("-C", checkout_dir, "rev-parse", "HEAD").first
1438
+ return replacement.merge("journal_commit" => commit) if update_ref_cas(commit, old)
1439
+ end
1440
+ raise AttemptErrors::EvidenceUnavailable, "Fixed merge evidence ref stayed conflicting"
1441
+ end
1442
+ end
1443
+ private :update_fixed_merge_request
1444
+
1445
+ def prepare_fixed_update(request_id:, expected:, replacement:, event_type:, guard: nil)
1446
+ validate_request_id!(request_id)
1447
+ unless replacement.is_a?(Hash) && replacement["request_id"] == request_id &&
1448
+ FIXED_MERGE_STATES.include?(replacement["state"]) &&
1449
+ (replacement.keys - (FIXED_MERGE_BINDING_FIELDS + FIXED_MERGE_MUTABLE_FIELDS + %w[journal_commit])).empty?
1450
+ raise ArgumentError, "invalid fixed merge update plan"
1451
+ end
1452
+ existing = fixed_merge_request(request_id)
1453
+ if expected.nil?
1454
+ unless event_type == "merge_admission" && replacement["state"] == "pending"
1455
+ raise AttemptErrors::InvalidState, "Fixed merge request needs an initial admission"
1456
+ end
1457
+ if existing
1458
+ return {record: existing, replayed: true} if fixed_replay?(existing, replacement)
1459
+ raise AttemptErrors::Conflict, "Fixed merge request #{request_id} has different input"
1460
+ end
1461
+ unless Molecules::FixedEffectReference.valid?(replacement.fetch("reference"))
1462
+ raise ArgumentError, "fixed merge record needs an opaque reference"
1463
+ end
1464
+ if fixed_merge_request_records.any? { |other| other["reference"] == replacement.fetch("reference") }
1465
+ raise AttemptErrors::Conflict, "Fixed merge reference is already bound"
1466
+ end
1467
+ else
1468
+ unless event_type == "merge_transition"
1469
+ raise AttemptErrors::InvalidState, "Admitted fixed merge request needs a transition"
1470
+ end
1471
+ unless existing == expected
1472
+ raise AttemptErrors::Conflict, "Fixed merge request #{request_id} changed during transition"
1473
+ end
1474
+ unless existing.except(*FIXED_MERGE_MUTABLE_FIELDS) == replacement.except(*FIXED_MERGE_MUTABLE_FIELDS)
1475
+ raise AttemptErrors::Conflict, "Fixed merge request #{request_id} changed immutable binding"
1476
+ end
1477
+ end
1478
+ conflict = guard&.call
1479
+ raise_authorization_conflict!(conflict) if conflict
1480
+ payload = {"request_id" => request_id, "state" => replacement.fetch("state"),
1481
+ "input_digest" => replacement.fetch("input_digest"),
1482
+ "record_digest" => Atoms::EvidenceDigest.digest(replacement),
1483
+ "receipt_digest" => replacement["receipt"] && Atoms::EvidenceDigest.digest(replacement["receipt"])}
1484
+ {record: replacement, event: {type: event_type, payload: payload}, replayed: false}
1485
+ end
1486
+ private :prepare_fixed_update
1487
+
1488
+ def fixed_replay?(existing, replacement)
1489
+ FIXED_MERGE_BINDING_FIELDS.all? do |field|
1490
+ existing[field] == replacement.fetch(field)
1491
+ end
1492
+ end
1493
+ private :fixed_replay?
1494
+
1495
+ # Protected-mode parity with service records: the record must match
1496
+ # the latest merge event digest in the owning attempt chain.
1497
+ def verify_fixed_record!(record, commit: ref_value)
1498
+ events = read_events(record.fetch("assignment_id"), commit: commit)
1499
+ .select { |event| event["attempt_id"] == record.fetch("attempt_id") }
1500
+ unless Models::EvidenceEvent.chain_valid?(events)
1501
+ raise AttemptErrors::EvidenceUnavailable, "Canonical fixed merge event chain is unverifiable"
1502
+ end
1503
+ event = events.reverse.find do |entry|
1504
+ %w[merge_admission merge_transition].include?(entry["type"]) &&
1505
+ entry.dig("payload", "request_id") == record["request_id"]
1506
+ end
1507
+ unless event && event.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(record)
1508
+ raise AttemptErrors::EvidenceUnavailable, "Canonical fixed merge record does not match accepted event"
1509
+ end
1510
+ rescue KeyError, TypeError
1511
+ raise AttemptErrors::EvidenceUnavailable, "Canonical fixed merge record has invalid binding"
1512
+ end
1513
+
1514
+ # The module marks a private region above; the fixed merge primitive
1515
+ # is a public coordinator-facing surface, so it is re-published here.
1516
+ public :fixed_merge_request, :fixed_merge_request_records, :admit_fixed_merge_request,
1517
+ :begin_fixed_merge_request, :settle_fixed_merge_request, :fixed_authorization_conflict
1518
+
1248
1519
  # Latest lifecycle state implied by the events, or nil when the
1249
1520
  # events do not describe a full attempt (intent missing).
1250
1521
  def derive_state(events)
@@ -1399,7 +1670,7 @@ module Ace
1399
1670
  lock.flock(File::LOCK_EX)
1400
1671
  begin
1401
1672
  yield
1402
- rescue StandardError
1673
+ rescue
1403
1674
  # Ordinary mutation rollback is unchanged. Initialization must
1404
1675
  # not adopt/reset a foreign disposable checkout on refusal.
1405
1676
  sync_checkout(ref_value) if cleanup_on_failure && File.exist?(File.join(checkout_dir, ".git"))
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "securerandom"
5
+
6
+ module Ace
7
+ module Assign
8
+ module Molecules
9
+ # The shared opaque-reference primitive for fixed-effect records
10
+ # (spec 8x3.t.xz9.4). The reference is receiver-issued at admission and
11
+ # names the canonical Assign record; clients may persist and echo it
12
+ # but must never parse it or derive paths from it. Creation is random
13
+ # (not derived from binding values), so a reference cannot be guessed
14
+ # from public request fields, and resolution always goes through the
15
+ # canonical record — there is no receiver-local mapping.
16
+ class FixedEffectReference
17
+ PREFIX = "fmr1"
18
+ PATTERN = /\Afmr1-[A-Za-z0-9_-]{40,86}\z/
19
+ MAX_LENGTH = 256
20
+
21
+ def self.create
22
+ "#{PREFIX}-#{Base64.urlsafe_encode64(SecureRandom.random_bytes(32), padding: false)}"
23
+ end
24
+
25
+ def self.valid?(value)
26
+ value.is_a?(String) && value.length.between?(1, MAX_LENGTH) && value.match?(PATTERN)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require_relative "../errors"
6
+
7
+ module Ace
8
+ module Assign
9
+ module Molecules
10
+ # Invokes the maintained ordinary fixed merge producer through the
11
+ # installed `ace-lab service request/status --mode ordinary-merge`
12
+ # CLI. The argv is closed: no shell, no caller-selected executable,
13
+ # socket, evidence path or token selector beyond the published request
14
+ # form. Output is bounded and parsed as one strict JSON envelope.
15
+ #
16
+ # Any local execution, framing or parse failure raises
17
+ # UnavailableError: a caller-side blocker that never proves refusal
18
+ # and never authorizes resubmission.
19
+ class OrdinaryMergeCli
20
+ MAX_OUTPUT_BYTES = 16 * 1024
21
+
22
+ # Raised when no maintained producer response is obtainable before
23
+ # the envelope could be observed. Maps to the closed
24
+ # `ordinary_merge_unavailable` caller-side blocker.
25
+ class UnavailableError < Ace::Assign::AttemptErrors::EvidenceUnavailable
26
+ def initialize(message = "ordinary merge producer response unavailable")
27
+ super
28
+ end
29
+ end
30
+
31
+ def initialize(binary: "ace-lab")
32
+ @binary = binary
33
+ end
34
+
35
+ def request(project:, assignment:, attempt:, queue_entry:, input_path:, authorization:, request_id:)
36
+ run(["service", "request", "--mode", "ordinary-merge",
37
+ "--project", project, "--assignment", assignment, "--attempt", attempt,
38
+ "--queue-entry", queue_entry, "--operation", "merge", "--input", input_path,
39
+ "--authorization", authorization, "--request-id", request_id])
40
+ end
41
+
42
+ def status(reference:, request_id:, project:, assignment:, attempt:, queue_entry:, input_digest:)
43
+ run(["service", "status", "--mode", "ordinary-merge", "--reference", reference,
44
+ "--request", request_id, "--project", project, "--assignment", assignment,
45
+ "--attempt", attempt, "--queue-entry", queue_entry,
46
+ "--input-digest", input_digest, "--format", "json"])
47
+ end
48
+
49
+ private
50
+
51
+ def run(arguments)
52
+ output, = Open3.capture3(@binary, *arguments)
53
+ raise UnavailableError, "ordinary merge producer output is missing or exceeds its bound" unless
54
+ output.is_a?(String) && !output.empty? && output.bytesize <= MAX_OUTPUT_BYTES
55
+ envelope = JSON.parse(output, max_nesting: 16, create_additions: false,
56
+ allow_duplicate_key: false, allow_comments: false, allow_nan: false)
57
+ unless envelope.is_a?(Hash) && envelope["status"].is_a?(String) &&
58
+ (envelope.keys - %w[status data error]).empty?
59
+ raise UnavailableError, "ordinary merge producer output is not a maintained envelope"
60
+ end
61
+ envelope
62
+ rescue JSON::ParserError, SystemCallError
63
+ raise UnavailableError, "ordinary merge producer response unavailable"
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end