ace-assign 0.69.0 → 0.70.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
  3. data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
  4. data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
  5. data/CHANGELOG.md +7 -0
  6. data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
  7. data/lib/ace/assign/authority/assignment_definition.rb +35 -0
  8. data/lib/ace/assign/authority/candidate_transfer.rb +2 -2
  9. data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
  10. data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
  11. data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
  12. data/lib/ace/assign/authority/ordinary_client.rb +70 -0
  13. data/lib/ace/assign/authority/ordinary_completion.rb +94 -0
  14. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
  15. data/lib/ace/assign/authority/ordinary_lifecycle.rb +191 -0
  16. data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
  17. data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
  18. data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
  19. data/lib/ace/assign/authority/ordinary_results.rb +196 -0
  20. data/lib/ace/assign/authority/ordinary_server.rb +75 -0
  21. data/lib/ace/assign/authority/prepared_work.rb +2 -2
  22. data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
  23. data/lib/ace/assign/authority/router.rb +11 -6
  24. data/lib/ace/assign/authority/server.rb +11 -180
  25. data/lib/ace/assign/authority/socket_server.rb +255 -0
  26. data/lib/ace/assign/authority/transfer_codec.rb +2 -2
  27. data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
  28. data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
  29. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
  30. data/lib/ace/assign/molecules/evidence_journal.rb +141 -34
  31. data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
  32. data/lib/ace/assign/molecules/journal_mutation.rb +5 -3
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
  34. data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
  35. data/lib/ace/assign/organisms/attempt_coordinator.rb +51 -36
  36. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
  37. data/lib/ace/assign/version.rb +1 -1
  38. metadata +23 -9
@@ -37,7 +37,10 @@ module Ace
37
37
  # @param identity_resolver [Molecules::ExecutionIdentityResolver, nil] Trust boundary
38
38
  # @param verifier [Molecules::ReceiptVerifier, nil] Receipt verifier
39
39
  def initialize(cache_base: nil, repo_root: nil, journal: nil, identity_resolver: nil, verifier: nil,
40
- lifecycle_exclusion: nil)
40
+ lifecycle_exclusion: nil, recorded_binding_observer: nil)
41
+ unless recorded_binding_observer.nil? || recorded_binding_observer.respond_to?(:call)
42
+ raise ArgumentError, "recorded native binding observer must be callable"
43
+ end
41
44
  @manager = Molecules::AssignmentManager.new(cache_base: cache_base)
42
45
  @store = @manager.attempt_store
43
46
  @repo_root = repo_root || Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
@@ -45,6 +48,7 @@ module Ace
45
48
  @identity_resolver = identity_resolver || Molecules::ExecutionIdentityResolver.new
46
49
  @verifier = verifier || Molecules::ReceiptVerifier.new(identity_resolver: @identity_resolver)
47
50
  @lifecycle_exclusion = lifecycle_exclusion
51
+ @recorded_binding_observer = recorded_binding_observer
48
52
  end
49
53
 
50
54
  attr_reader :store
@@ -59,16 +63,17 @@ module Ace
59
63
  {events: [{type: "attempt_stopped", payload: payload}], blobs: {}}
60
64
  end
61
65
 
62
- # The protected result owner authenticates imported bytes, candidate,
66
+ # The source-selected result owner authenticates imported bytes, candidate,
63
67
  # closure and independent settlements at the same CAS prefix.
64
- def self.finished_transition_plan(events:, receipt:)
68
+ def self.finished_transition_plan(events:, receipt:, authority:)
69
+ raise ArgumentError, "unknown result authority" unless %i[ordinary protected].include?(authority)
65
70
  state = Molecules::CanonicalAttemptState.derive(events)
66
71
  unless state == "running" && %w[succeeded failed].include?(receipt.fetch("verdict"))
67
72
  raise AttemptErrors::Conflict, "Result finish requires a running attempt"
68
73
  end
69
74
  Atoms::AttemptStateMachine.transition!(state, receipt.fetch("verdict"))
70
75
  {events: [{type: "receipt_accepted", payload: {"receipt" => receipt}},
71
- {type: "transition", payload: {"from" => state, "to" => receipt.fetch("verdict"), "reason" => "protected_result_accepted"}}], blobs: {}}
76
+ {type: "transition", payload: {"from" => state, "to" => receipt.fetch("verdict"), "reason" => "#{authority}_result_accepted"}}], blobs: {}}
72
77
  end
73
78
 
74
79
  def self.protected_recovery_decision(state:, effects_pending:, bound:, live:, checkpoint:)
@@ -113,21 +118,13 @@ module Ace
113
118
  raise AttemptErrors::ReceiptRejected, "Native reverse requires an active managed attempt"
114
119
  end
115
120
  binding = reconciler.recorded_binding(attempt)
116
- observation = reconciler.observation(attempt)
117
- unless binding && observation["liveness"] == "live" &&
118
- binding["process_identity"] == observation["identity"] &&
119
- %w[runtime session pane].all? { |key| binding[key].is_a?(String) && !binding[key].empty? }
121
+ unless binding && %w[runtime session pane].all? { |key| binding[key].is_a?(String) && !binding[key].empty? }
120
122
  raise AttemptErrors::ReceiptRejected, "Exact native owner binding is unavailable or changed"
121
123
  end
122
- unless caller_pid.nil?
123
- unless caller_pid.is_a?(Integer) && caller_pid.positive?
124
- raise AttemptErrors::UnauthorizedIdentity, "Kernel caller process is unavailable"
125
- end
126
- caller = Ace::Runtime.resolve(binding["runtime"]).process_binding(pane: binding["pane"], caller_pid: caller_pid)
127
- unless caller == binding
128
- raise AttemptErrors::UnauthorizedIdentity, "Caller does not descend from the recorded native owner"
129
- end
124
+ unless caller_pid.nil? || (caller_pid.is_a?(Integer) && caller_pid.positive?)
125
+ raise AttemptErrors::UnauthorizedIdentity, "Kernel caller process is unavailable"
130
126
  end
127
+ observe_runtime_binding!(attempt, binding, caller_pid)
131
128
  latest = recover_managed_attempt(attempt.binding.assignment_id, attempt_id)
132
129
  unless latest&.active? && latest.binding.to_h == attempt.binding.to_h && reconciler.recorded_binding(latest) == binding
133
130
  raise AttemptErrors::ReceiptRejected, "Native owner changed during observation"
@@ -137,6 +134,28 @@ module Ace
137
134
  raise AttemptErrors::ReceiptRejected, "Native owner observation unavailable (#{e.class})"
138
135
  end
139
136
 
137
+ # An installed consumer selects its own bounded native reader. It must
138
+ # never fall back to the service process's ambient runtime on failure.
139
+ def observe_runtime_binding!(attempt, binding, caller_pid)
140
+ if @recorded_binding_observer
141
+ actual = @recorded_binding_observer.call(binding: binding, caller_pid: caller_pid)
142
+ unless actual == binding
143
+ raise AttemptErrors::UnauthorizedIdentity, "Installed native owner observation differs"
144
+ end
145
+ return
146
+ end
147
+ observation = reconciler.observation(attempt)
148
+ unless observation["liveness"] == "live" && binding["process_identity"] == observation["identity"]
149
+ raise AttemptErrors::ReceiptRejected, "Exact native owner binding is unavailable or changed"
150
+ end
151
+ return if caller_pid.nil?
152
+ caller = Ace::Runtime.resolve(binding["runtime"]).process_binding(pane: binding["pane"], caller_pid: caller_pid)
153
+ unless caller == binding
154
+ raise AttemptErrors::UnauthorizedIdentity, "Caller does not descend from the recorded native owner"
155
+ end
156
+ end
157
+ private :observe_runtime_binding!
158
+
140
159
  # Claim an external service effect before dispatch. Only a managed,
141
160
  # active attempt for the exact project and candidate may own it. The
142
161
  # guard re-runs inside the journal lock per CAS attempt, so a
@@ -174,7 +193,10 @@ module Ace
174
193
  end
175
194
 
176
195
  def service_attempt(binding)
177
- attempt = authoritative_attempt(binding)
196
+ # An effect can never acquire authority from a requester cache, even
197
+ # when the assignment definition is missing from this consumer.
198
+ attempt = journal_for.derived_attempts(binding.fetch("assignment_id"))
199
+ .find { |candidate| candidate.attempt_id == binding.fetch("attempt_id") }
178
200
  raise AttemptErrors::NotFound, "Attempt not found" unless attempt
179
201
  unless attempt.managed? && attempt.active? &&
180
202
  attempt.binding.assignment_id == binding["assignment_id"] &&
@@ -306,28 +328,29 @@ module Ace
306
328
  # never need message-string discrimination: replay/binding conflicts
307
329
  # stay Conflict, a consumed authorization is AuthorizationConflict.
308
330
  guard = lambda do
309
- conflict = @journal.fixed_authorization_conflict(record)
331
+ conflict = journal_for.fixed_authorization_conflict(record)
310
332
  if conflict
311
333
  raise AttemptErrors::AuthorizationConflict,
312
334
  "Authorization reference is not currently valid"
313
335
  end
314
336
  nil
315
337
  end
316
- @journal.admit_fixed_merge_request(record, guard: guard)
338
+ journal_for.admit_fixed_merge_request(record, guard: guard)
317
339
  end
318
340
 
319
341
  # Begin the fixed effect (pending -> uncertain). The single CAS
320
342
  # transition is the at-most-one-merge proof: a restarted receiver
321
- # replays "already started" and must not dispatch again.
343
+ # refuses an already-started claim and must not dispatch again.
322
344
  def begin_fixed_merge_effect(request_id)
323
- @journal.begin_fixed_merge_request(request_id)
345
+ journal_for.begin_fixed_merge_request(request_id)
324
346
  end
325
347
 
326
348
  # Settle the fixed effect with the exact terminal receipt. The
327
349
  # journal validates the closed receipt schema and its binding to the
328
350
  # admitted record before the transition lands.
329
- def settle_fixed_merge_effect(request_id, terminal_status:, receipt:)
330
- @journal.settle_fixed_merge_request(request_id, terminal_status: terminal_status, receipt: receipt)
351
+ def settle_fixed_merge_effect(request_id, terminal_status:, receipt:, attestation: nil)
352
+ journal_for.settle_fixed_merge_request(request_id, terminal_status: terminal_status,
353
+ receipt: receipt, attestation: attestation)
331
354
  end
332
355
 
333
356
  # Read-only canonical record lookup by request ID: lets the producer
@@ -352,7 +375,7 @@ module Ace
352
375
  # without revealing whether another project holds the reference.
353
376
  def fixed_merge_status(reference:, request_id:, project_id:, assignment_id:, attempt_id:,
354
377
  queue_entry:, input_digest:)
355
- record = @journal.fixed_merge_request(request_id)
378
+ record = journal_for.fixed_merge_request(request_id)
356
379
  return nil unless record
357
380
  matches = reference == record["reference"] && input_digest == record["input_digest"] &&
358
381
  [project_id, assignment_id, attempt_id, queue_entry].zip(
@@ -603,7 +626,7 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
603
626
 
604
627
  active_scopes = @store.list(assignment_id).select(&:active?).map(&:binding)
605
628
  active_scopes.concat(journal_actives.map(&:binding))
606
- blocker = active_scopes.find { |binding| scopes_overlap?(binding.scope, scope) }
629
+ blocker = active_scopes.find { |binding| Atoms::AssignmentScope.overlap?(binding.scope, scope) }
607
630
  if blocker
608
631
  raise AttemptErrors::Conflict,
609
632
  "Active attempt scope #{blocker.scope} overlaps requested scope #{scope} on #{assignment_id}; " \
@@ -842,9 +865,9 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
842
865
  unless %w[check review-collection review-approval].include?(kind) && check_name.is_a?(String) && !check_name.strip.empty?
843
866
  raise AttemptErrors::ReceiptRejected, "Invalid evidence kind or check name"
844
867
  end
845
- if !historical_head.nil? && (!%w[review-collection review-approval].include?(kind) || !historical_head.is_a?(String) ||
868
+ if !historical_head.nil? && (!historical_head.is_a?(String) ||
846
869
  !historical_head.match?(/\A[0-9a-f]{40,64}\z/))
847
- raise AttemptErrors::ReceiptRejected, "Historical evidence requires review collection/approval and an exact recorded head"
870
+ raise AttemptErrors::ReceiptRejected, "Historical evidence requires an exact recorded head"
848
871
  end
849
872
  unless attempt_id.to_s.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
850
873
  receipt_digest.to_s.match?(/\A[0-9a-f]{64}\z/)
@@ -994,14 +1017,6 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
994
1017
  raise Error, "Failed to generate a unique attempt ID for #{assignment_id} after 100 attempts"
995
1018
  end
996
1019
 
997
- # Ancestors and descendants overlap: an active attempt for 010 owns
998
- # its 010.01 subtree, and vice versa.
999
- def scopes_overlap?(left, right)
1000
- return true if Atoms::AssignmentScope.equal?(left, right)
1001
-
1002
- left.start_with?("#{right}.") || right.start_with?("#{left}.")
1003
- end
1004
-
1005
1020
  # Active attempts derived from the authoritative journal, used when
1006
1021
  # the local record may be missing (lost or wiped cache). Derivation
1007
1022
  # lives in the journal (owner of attempt-state interpretation).
@@ -1059,7 +1074,7 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
1059
1074
  # merges both). The loser records a stopped transition and refuses.
1060
1075
  def yield_lost_ownership_race(attempt, assignment_id, scope)
1061
1076
  rival = journal_for.active_attempts(assignment_id).find do |other|
1062
- other.attempt_id != attempt.attempt_id && scopes_overlap?(other.binding.scope, scope)
1077
+ other.attempt_id != attempt.attempt_id && Atoms::AssignmentScope.overlap?(other.binding.scope, scope)
1063
1078
  end
1064
1079
  return attempt if rival.nil?
1065
1080
 
@@ -212,7 +212,7 @@ module Ace
212
212
  return {state: "accepted", reference: ack.fetch("reference"),
213
213
  request_id: ack.fetch("request_id"), input_digest: ack.fetch("input_digest")}
214
214
  end
215
- handle_refusal(binding, identity, branch[:payload], expected_digest: digest)
215
+ handle_refusal(binding, identity, branch[:payload], expected_digest: digest, request_id: selectors.fetch(:request_id))
216
216
  end
217
217
 
218
218
  # Closed outer envelope for admission responses: exactly {status,
@@ -241,7 +241,7 @@ module Ace
241
241
  end
242
242
  end
243
243
 
244
- def handle_refusal(binding, identity, error, expected_digest:)
244
+ def handle_refusal(binding, identity, error, expected_digest:, request_id:)
245
245
  refusal = begin
246
246
  Molecules::OrdinaryMergeContract.validate_refusal!(error)
247
247
  rescue ArgumentError
@@ -249,9 +249,9 @@ module Ace
249
249
  "ordinary merge refusal is not the maintained contract; admission stays unconfirmed"
250
250
  end
251
251
  echo = refusal.fetch("input_digest")
252
- if echo && echo != expected_digest
252
+ if refusal.fetch("request_id") != request_id || (echo && echo != expected_digest)
253
253
  raise AttemptErrors::EvidenceUnavailable,
254
- "ordinary merge refusal echoes a different input digest; admission stays unconfirmed"
254
+ "ordinary merge refusal echoes a different request; admission stays unconfirmed"
255
255
  end
256
256
  if refusal.fetch("code") == Molecules::OrdinaryMergeContract::UNAVAILABLE_CODE
257
257
  raise AttemptErrors::EvidenceUnavailable,
@@ -259,7 +259,7 @@ module Ace
259
259
  end
260
260
  record_event(binding, identity, {"stage" => RESULT_STAGE, "outcome" => "refused_no_effect",
261
261
  "code" => refusal.fetch("code"), "message" => refusal.fetch("message"),
262
- "request_id" => refusal.fetch("request_id"), "input_digest" => refusal.fetch("input_digest")})
262
+ "request_id" => request_id, "input_digest" => expected_digest})
263
263
  {state: "refused", code: refusal.fetch("code"), message: refusal.fetch("message")}
264
264
  end
265
265
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Assign
5
- VERSION = "0.69.0"
5
+ VERSION = "0.70.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-assign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.69.0
4
+ version: 0.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
@@ -175,42 +175,42 @@ dependencies:
175
175
  requirements:
176
176
  - - "~>"
177
177
  - !ruby/object:Gem::Version
178
- version: 0.60.2
178
+ version: 0.60.3
179
179
  type: :runtime
180
180
  prerelease: false
181
181
  version_requirements: !ruby/object:Gem::Requirement
182
182
  requirements:
183
183
  - - "~>"
184
184
  - !ruby/object:Gem::Version
185
- version: 0.60.2
185
+ version: 0.60.3
186
186
  - !ruby/object:Gem::Dependency
187
187
  name: ace-task
188
188
  requirement: !ruby/object:Gem::Requirement
189
189
  requirements:
190
190
  - - "~>"
191
191
  - !ruby/object:Gem::Version
192
- version: '0.37'
192
+ version: '0.40'
193
193
  type: :runtime
194
194
  prerelease: false
195
195
  version_requirements: !ruby/object:Gem::Requirement
196
196
  requirements:
197
197
  - - "~>"
198
198
  - !ruby/object:Gem::Version
199
- version: '0.37'
199
+ version: '0.40'
200
200
  - !ruby/object:Gem::Dependency
201
201
  name: ace-bundle
202
202
  requirement: !ruby/object:Gem::Requirement
203
203
  requirements:
204
204
  - - "~>"
205
205
  - !ruby/object:Gem::Version
206
- version: '0.45'
206
+ version: 0.45.1
207
207
  type: :runtime
208
208
  prerelease: false
209
209
  version_requirements: !ruby/object:Gem::Requirement
210
210
  requirements:
211
211
  - - "~>"
212
212
  - !ruby/object:Gem::Version
213
- version: '0.45'
213
+ version: 0.45.1
214
214
  - !ruby/object:Gem::Dependency
215
215
  name: ace-runtime
216
216
  requirement: !ruby/object:Gem::Requirement
@@ -245,14 +245,14 @@ dependencies:
245
245
  requirements:
246
246
  - - "~>"
247
247
  - !ruby/object:Gem::Version
248
- version: 0.5.1
248
+ version: '0.6'
249
249
  type: :runtime
250
250
  prerelease: false
251
251
  version_requirements: !ruby/object:Gem::Requirement
252
252
  requirements:
253
253
  - - "~>"
254
254
  - !ruby/object:Gem::Version
255
- version: 0.5.1
255
+ version: '0.6'
256
256
  - !ruby/object:Gem::Dependency
257
257
  name: ace-support-test-helpers
258
258
  requirement: !ruby/object:Gem::Requirement
@@ -418,6 +418,7 @@ files:
418
418
  - lib/ace/assign/atoms/step_numbering.rb
419
419
  - lib/ace/assign/atoms/step_sorter.rb
420
420
  - lib/ace/assign/atoms/tree_formatter.rb
421
+ - lib/ace/assign/authority/assignment_definition.rb
421
422
  - lib/ace/assign/authority/assignment_inventory.rb
422
423
  - lib/ace/assign/authority/campaign_consumer_policy.rb
423
424
  - lib/ace/assign/authority/campaign_execution.rb
@@ -460,6 +461,16 @@ files:
460
461
  - lib/ace/assign/authority/launch_steering.rb
461
462
  - lib/ace/assign/authority/launch_stop.rb
462
463
  - lib/ace/assign/authority/maintenance_workspace_retirement.rb
464
+ - lib/ace/assign/authority/ordinary_attempt_start.rb
465
+ - lib/ace/assign/authority/ordinary_client.rb
466
+ - lib/ace/assign/authority/ordinary_completion.rb
467
+ - lib/ace/assign/authority/ordinary_effect_settlement.rb
468
+ - lib/ace/assign/authority/ordinary_lifecycle.rb
469
+ - lib/ace/assign/authority/ordinary_peer.rb
470
+ - lib/ace/assign/authority/ordinary_progress.rb
471
+ - lib/ace/assign/authority/ordinary_queue.rb
472
+ - lib/ace/assign/authority/ordinary_results.rb
473
+ - lib/ace/assign/authority/ordinary_server.rb
463
474
  - lib/ace/assign/authority/original_inbox_selection.rb
464
475
  - lib/ace/assign/authority/posix_acl.rb
465
476
  - lib/ace/assign/authority/prepared_input.rb
@@ -478,6 +489,7 @@ files:
478
489
  - lib/ace/assign/authority/service_delivery_evidence.rb
479
490
  - lib/ace/assign/authority/service_evidence.rb
480
491
  - lib/ace/assign/authority/service_publication_evidence.rb
492
+ - lib/ace/assign/authority/socket_server.rb
481
493
  - lib/ace/assign/authority/task_context_entry.rb
482
494
  - lib/ace/assign/authority/transfer_codec.rb
483
495
  - lib/ace/assign/cli.rb
@@ -533,6 +545,7 @@ files:
533
545
  - lib/ace/assign/molecules/attempt_store.rb
534
546
  - lib/ace/assign/molecules/canonical_attempt_state.rb
535
547
  - lib/ace/assign/molecules/canonical_evidence.rb
548
+ - lib/ace/assign/molecules/canonical_read_commands.rb
536
549
  - lib/ace/assign/molecules/canonical_read_snapshot.rb
537
550
  - lib/ace/assign/molecules/evidence_calculator.rb
538
551
  - lib/ace/assign/molecules/evidence_journal.rb
@@ -540,6 +553,7 @@ files:
540
553
  - lib/ace/assign/molecules/execution_scope_lineage.rb
541
554
  - lib/ace/assign/molecules/fixed_effect_reference.rb
542
555
  - lib/ace/assign/molecules/fork_session_launcher.rb
556
+ - lib/ace/assign/molecules/installed_journal_read.rb
543
557
  - lib/ace/assign/molecules/journal_input_inhibition.rb
544
558
  - lib/ace/assign/molecules/journal_mutation.rb
545
559
  - lib/ace/assign/molecules/journal_prompt_mutation.rb