ace-assign 0.68.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/composition-rules.yml +0 -0
  3. data/.ace-defaults/assign/catalog/recipes/batch-tasks.recipe.yml +0 -0
  4. data/.ace-defaults/assign/catalog/recipes/documentation.recipe.yml +0 -0
  5. data/.ace-defaults/assign/catalog/recipes/fix-and-review.recipe.yml +0 -0
  6. data/.ace-defaults/assign/catalog/recipes/implement-simple.recipe.yml +0 -0
  7. data/.ace-defaults/assign/catalog/recipes/implement-with-pr.recipe.yml +0 -0
  8. data/.ace-defaults/assign/catalog/recipes/release-only.recipe.yml +0 -0
  9. data/.ace-defaults/assign/catalog/steps/apply-feedback.step.yml +0 -0
  10. data/.ace-defaults/assign/catalog/steps/commit.step.yml +0 -0
  11. data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +0 -0
  12. data/.ace-defaults/assign/catalog/steps/fix-tests.step.yml +0 -0
  13. data/.ace-defaults/assign/catalog/steps/lint.step.yml +0 -0
  14. data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +0 -0
  15. data/.ace-defaults/assign/catalog/steps/onboard-base.step.yml +0 -0
  16. data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -0
  17. data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -0
  18. data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +0 -0
  19. data/.ace-defaults/assign/catalog/steps/push-to-remote.step.yml +0 -0
  20. data/.ace-defaults/assign/catalog/steps/rebase-with-main.step.yml +0 -0
  21. data/.ace-defaults/assign/catalog/steps/record-demo.step.yml +0 -0
  22. data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +0 -0
  23. data/.ace-defaults/assign/catalog/steps/reorganize-commits.step.yml +0 -0
  24. data/.ace-defaults/assign/catalog/steps/research.step.yml +0 -0
  25. data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -0
  26. data/.ace-defaults/assign/catalog/steps/security-audit.step.yml +0 -0
  27. data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +0 -0
  28. data/.ace-defaults/assign/catalog/steps/squash-changelog.step.yml +0 -0
  29. data/.ace-defaults/assign/catalog/steps/task-load.step.yml +0 -0
  30. data/.ace-defaults/assign/catalog/steps/update-docs.step.yml +0 -0
  31. data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +0 -0
  32. data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +0 -0
  33. data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -0
  34. data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
  35. data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
  36. data/.ace-defaults/assign/presets/quick-implement.yml +0 -0
  37. data/.ace-defaults/assign/presets/release-only.yml +0 -0
  38. data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
  39. data/.ace-defaults/assign/presets/work-on-docs.yml +0 -0
  40. data/.ace-defaults/assign/presets/work-on-task-in-place.yml +0 -0
  41. data/.ace-defaults/nav/protocols/skill-sources/ace-assign.yml +0 -0
  42. data/.ace-defaults/nav/protocols/wfi-sources/ace-assign.yml +0 -0
  43. data/CHANGELOG.md +17 -0
  44. data/Rakefile +0 -0
  45. data/docs/demo/canonical-skill-source.gif +0 -0
  46. data/docs/demo/fork-provider.cast +0 -0
  47. data/docs/demo/fork-provider.gif +0 -0
  48. data/docs/demo/fork-provider.recording.json +0 -0
  49. data/docs/demo/fork-provider.tape.yml +0 -0
  50. data/docs/getting-started.md +0 -0
  51. data/docs/handbook.md +0 -0
  52. data/handbook/guides/fork-context.g.md +0 -0
  53. data/handbook/skills/as-assign-add-task/SKILL.md +0 -0
  54. data/handbook/skills/as-assign-compose/SKILL.md +0 -0
  55. data/handbook/skills/as-assign-create/SKILL.md +0 -0
  56. data/handbook/skills/as-assign-drive/SKILL.md +0 -0
  57. data/handbook/skills/as-assign-prepare/SKILL.md +0 -0
  58. data/handbook/skills/as-assign-recover-fork/SKILL.md +0 -0
  59. data/handbook/skills/as-assign-run-in-batches/SKILL.md +0 -0
  60. data/handbook/skills/as-assign-start/SKILL.md +0 -0
  61. data/handbook/skills/as-create-retro-internal/SKILL.md +0 -0
  62. data/handbook/skills/as-mark-task-done-internal/SKILL.md +0 -0
  63. data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +0 -0
  64. data/handbook/skills/as-task-load-internal/SKILL.md +0 -0
  65. data/handbook/workflow-instructions/assign/add-task.wf.md +0 -0
  66. data/handbook/workflow-instructions/assign/compose.wf.md +0 -0
  67. data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +0 -0
  68. data/handbook/workflow-instructions/assign/create.wf.md +0 -0
  69. data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +0 -0
  70. data/handbook/workflow-instructions/assign/recover-fork.wf.md +0 -0
  71. data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +0 -0
  72. data/handbook/workflow-instructions/assign/run-in-batches.wf.md +0 -0
  73. data/handbook/workflow-instructions/assign/start.wf.md +0 -0
  74. data/handbook/workflow-instructions/assign/task-load-internal.wf.md +0 -0
  75. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +0 -0
  76. data/lib/ace/assign/atoms/assign_frontmatter_parser.rb +0 -0
  77. data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
  78. data/lib/ace/assign/atoms/catalog_loader.rb +0 -0
  79. data/lib/ace/assign/atoms/composition_rules.rb +0 -0
  80. data/lib/ace/assign/atoms/number_generator.rb +0 -0
  81. data/lib/ace/assign/atoms/preset_expander.rb +0 -0
  82. data/lib/ace/assign/atoms/preset_loader.rb +0 -0
  83. data/lib/ace/assign/atoms/preset_step_resolver.rb +0 -0
  84. data/lib/ace/assign/atoms/step_file_parser.rb +0 -0
  85. data/lib/ace/assign/atoms/step_numbering.rb +0 -0
  86. data/lib/ace/assign/atoms/step_sorter.rb +0 -0
  87. data/lib/ace/assign/atoms/tree_formatter.rb +0 -0
  88. data/lib/ace/assign/authority/assignment_definition.rb +35 -0
  89. data/lib/ace/assign/authority/candidate_transfer.rb +2 -2
  90. data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
  91. data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
  92. data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
  93. data/lib/ace/assign/authority/ordinary_client.rb +70 -0
  94. data/lib/ace/assign/authority/ordinary_completion.rb +94 -0
  95. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
  96. data/lib/ace/assign/authority/ordinary_lifecycle.rb +191 -0
  97. data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
  98. data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
  99. data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
  100. data/lib/ace/assign/authority/ordinary_results.rb +196 -0
  101. data/lib/ace/assign/authority/ordinary_server.rb +75 -0
  102. data/lib/ace/assign/authority/prepared_work.rb +2 -2
  103. data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
  104. data/lib/ace/assign/authority/router.rb +11 -6
  105. data/lib/ace/assign/authority/server.rb +11 -180
  106. data/lib/ace/assign/authority/socket_server.rb +255 -0
  107. data/lib/ace/assign/authority/transfer_codec.rb +2 -2
  108. data/lib/ace/assign/cli/commands/add.rb +0 -0
  109. data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
  110. data/lib/ace/assign/cli/commands/list.rb +0 -0
  111. data/lib/ace/assign/cli/commands/retry_cmd.rb +0 -0
  112. data/lib/ace/assign/models/assignment_info.rb +0 -0
  113. data/lib/ace/assign/models/queue_state.rb +0 -0
  114. data/lib/ace/assign/models/step.rb +0 -0
  115. data/lib/ace/assign/molecules/assignment_discoverer.rb +0 -0
  116. data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
  117. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
  118. data/lib/ace/assign/molecules/evidence_journal.rb +141 -34
  119. data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
  120. data/lib/ace/assign/molecules/journal_mutation.rb +5 -3
  121. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
  122. data/lib/ace/assign/molecules/preset_inferrer.rb +0 -0
  123. data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
  124. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +0 -0
  125. data/lib/ace/assign/molecules/step_renumberer.rb +0 -0
  126. data/lib/ace/assign/organisms/attempt_coordinator.rb +58 -36
  127. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
  128. data/lib/ace/assign/version.rb +1 -1
  129. metadata +24 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e21e0378ad7810b04e9a92cbac849a7e7ce09b9add705c668a826dc9e3cd87f6
4
- data.tar.gz: 0d56587d4589765dbc83758e882a9482a9ce4a12dc9c803aaeacc7cc0314cb57
3
+ metadata.gz: 9de34921fd3a299eabfd530c8df3c76fed9405606cb0e717a0458fed9ecf614e
4
+ data.tar.gz: a9a2d22448d0c100b5404ce9ca9666b72ed0e6fb3d9bf158a5051ce001b703b1
5
5
  SHA512:
6
- metadata.gz: 93380ddd286cd79c67fad691e875f703e60282f585746dbc3da8b04b5145310cc94ff1577416ba4164db25e90a30a1481588ae1b070ce196daea709ba8c55c25
7
- data.tar.gz: e9ac0d6ef4ac39f6812e1c8f276f64dab32d15aa023830b2bf563a2211addd4353d875736506d2103a6d2b4c01efbdcf66bb205dd2e091e08e361a4146140476
6
+ metadata.gz: f68696b349c145768cf15634945c0fe40a659a80394f6053b37fd4270c244d8884345dbc9ba438a257de66593a0e44d88feca77c3c2580ea0e5208221d896746
7
+ data.tar.gz: 07c573b0f8160c1929fbba896362180ad2bd08097ab306139ca0e5a28c00762650845840f8709e16f825660add6057b6569ce49a1f7595370c2f83a6740c4e07
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -2,7 +2,7 @@
2
2
  # Usage: ace-assign create --task TASK[,TASK...] --preset deliver-release
3
3
 
4
4
  name: deliver-release
5
- description: Prepare and bind a stable release cut without exercising publication authority
5
+ description: Deliver a bound release cut through the project-authorized publisher and installation owners
6
6
 
7
7
  parameters:
8
8
  taskrefs:
@@ -37,15 +37,18 @@ steps:
37
37
  final artifact digest, and update the manifest before it is accepted. Preparation
38
38
  is not publication, installation, deployment, or proof of any of those effects.
39
39
 
40
- - name: publication-authority-placeholder
40
+ - name: request-authorized-publication
41
41
  number: "040"
42
42
  instructions: |
43
- Publication placeholder only. Do not run a publish command in this preset. Route
44
- the exact manifest to the separate publication authority named by project policy.
45
- Missing credentials, publisher identity, or admission is
46
- BLOCKED(provision, owner). If that authority accepts a request and returns a
47
- durable operation reference plus reconciliation path, record
48
- WAIT(reference, next-check); otherwise preserve uncertainty and do not retry.
43
+ Resolve the concrete publication workflow and executor from project policy and
44
+ invoke that existing workflow with the exact accepted manifest through the separate
45
+ publication authority. This step grants no credentials or permission by itself.
46
+ Missing publisher identity, executable binding, or admission is BLOCKED(provision, owner).
47
+ Use the project's existing publisher, dependency order and scoped HITL OTP route.
48
+ Record the accepted operation reference and reconciliation path before WAIT.
49
+ Track published, pending and uncertain artifacts separately. Verify registry version
50
+ and digest before retry; never republish an already matching artifact or repeat an
51
+ uncertain effect blindly. Keep delivery open after partial or unknown publication.
49
52
 
50
53
  - name: record-release-outcome
51
54
  number: "050"
@@ -32,11 +32,16 @@ steps:
32
32
  - name: delegate-renewed-evidence
33
33
  number: "030"
34
34
  instructions: |
35
- After any candidate movement, delegate renewed evidence for the new exact head:
36
- prove replay content identity with range-diff or a stronger project-approved
37
- comparison, run fresh relevant tests, and obtain an independent reviewer verdict.
38
- The reviewer must be independent of the executor. Content beyond the declared
39
- replay delta requires the full project review gate, not a verdict-stands shortcut.
35
+ Require an initial independent reviewer verdict, then record an accepted-head
36
+ assessment after every candidate movement. Retain old/new head and base SHAs,
37
+ the executed range-diff/content comparison, full repository context, risks and
38
+ fresh relevant tests. Proven identical rebases may carry the initial review;
39
+ patch-id alone is insufficient. An integrator may accept a minor safe correction
40
+ with recorded reasoning and tests. Material or uncertain changes, including small
41
+ authority/security changes, require independent delta review. Keep the initial
42
+ review and formal forge approval actor/state/SHA separate from this assessment.
43
+ Post and read back the evidence before merge; use the exact accepted head in the
44
+ server-side merge guard. Failed publication of evidence blocks integration.
40
45
 
41
46
  - name: reevaluate-integration-readiness
42
47
  number: "040"
File without changes
File without changes
@@ -20,8 +20,8 @@ steps:
20
20
 
21
21
  These are references to producer tasks; never reparent, duplicate, reopen, or
22
22
  change producer task status. A PR-ready worker remains terminal even while this
23
- delivery assignment stays open. No second queue, journal, daemon, or cache may
24
- be created. Treat an empty observation as current input only: Transiently empty
23
+ delivery assignment stays open. A local objective queue may store goals, next actions, blockers and references;
24
+ no second execution queue, journal, daemon, or cache may be created. Treat an empty observation as current input only: Transiently empty
25
25
  is not accepted completion.
26
26
 
27
27
  - name: supervise-delivery-queue
@@ -42,8 +42,10 @@ steps:
42
42
  - policy/plan revision and evidence references used for the decision.
43
43
 
44
44
  For every entry, evaluate dependency state, exact candidate head SHA, fresh
45
- test and independent-review evidence bound to that head, required authority and
46
- transport, and grouping constraints. Re-record the decision after dependency,
45
+ tests, initial independent review and a documented accepted-head assessment.
46
+ An identical rebase needs executed content-identity proof; a minor safe correction
47
+ needs integrator reasoning and tests; material or uncertain delta needs independent
48
+ delta review. Evaluate required authority, transport and grouping constraints. Re-record the decision after dependency,
47
49
  candidate head, evidence, authority, transport, or target-branch movement.
48
50
 
49
51
  - name: reconcile-integration-operation
File without changes
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.70.0] - 2026-10-16
11
+
12
+ ### Added
13
+
14
+ - Admit ordinary assignments, native starts, progress, submitted results and terminal settlement through the installed canonical owner. Retain original mutations and immutable snapshots across restart, isolate sibling scopes, and join current HITL, service and Inbox evidence.
15
+ - Preserve historical review checks at their recorded head and align delivery presets with accepted-head assessment and serial integration.
16
+
17
+ ## [0.69.0] - 2026-10-16
18
+
19
+ ### Changed
20
+
21
+ - Require ace-review 0.60.2 to keep the coordinated Forgejo 0.7 dependency graph compatible.
22
+
23
+ ### Added
24
+
25
+ - Expose read-only `AttemptCoordinator#fixed_merge_records` so the ordinary merge receiver can reconcile its pending canonical records after restart without another journal or repeated effect.
26
+
10
27
  ## [0.68.0] - 2026-10-14
11
28
 
12
29
  ### Added
data/Rakefile CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/docs/handbook.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -47,6 +47,11 @@ module Ace
47
47
  rescue Ace::Assign::AttemptErrors::InvalidScope
48
48
  false
49
49
  end
50
+
51
+ # An owner of a subtree also owns every descendant scope.
52
+ def self.overlap?(left, right)
53
+ equal?(left, right) || left.start_with?("#{right}.") || right.start_with?("#{left}.")
54
+ end
50
55
  end
51
56
  end
52
57
  end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require_relative "campaign_execution"
6
+
7
+ module Ace
8
+ module Assign
9
+ module Authority
10
+ # Shared byte/schema validation; each lifecycle separately authenticates
11
+ # the registration's owner, campaign provenance and original selection.
12
+ module AssignmentDefinition
13
+ def self.parse(bytes:, digest:, assignment_id:, project_id:)
14
+ unless bytes.is_a?(String) && bytes.bytesize <= 32_768 && digest.is_a?(String) &&
15
+ Digest::SHA256.hexdigest(bytes) == digest
16
+ raise ArgumentError, "invalid assignment definition digest or size"
17
+ end
18
+ value = JSON.parse(bytes)
19
+ allowed = %w[session_id name description created_at updated_at source_config parent task_id project_id prepared_work review_campaign campaign_execution]
20
+ unless value.is_a?(Hash) && (value.keys - allowed).empty? &&
21
+ %w[session_id name created_at source_config task_id project_id].all? { |key| value[key].is_a?(String) && !value[key].empty? } &&
22
+ value["session_id"] == assignment_id && value["project_id"] == project_id
23
+ raise ArgumentError, "invalid managed assignment definition"
24
+ end
25
+ CampaignExecution.validate_parent!(value.fetch("review_campaign")) if value.key?("review_campaign")
26
+ assignment = Models::Assignment.from_h(value)
27
+ unless assignment.managed? && assignment.task_id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/)
28
+ raise ArgumentError, "definition is not managed or task identity is invalid"
29
+ end
30
+ assignment
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -115,14 +115,14 @@ module Ace
115
115
  reject!("Prepared export filesystem is unavailable")
116
116
  end
117
117
 
118
- def admit(bytes:, sha256:, size:, head:, &consumer)
118
+ def admit(bytes:, sha256:, size:, head:, deadline: nil, &consumer)
119
119
  unless bytes.is_a?(String) && size.is_a?(Integer) && size.positive? && size <= MAX_BYTES &&
120
120
  size == bytes.bytesize && sha256.is_a?(String) &&
121
121
  Digest::SHA256.hexdigest(bytes.b) == sha256 && head.is_a?(String) && head.match?(SHA)
122
122
  reject!("Candidate bundle binding or size is invalid")
123
123
  end
124
124
  verify_root!
125
- deadline = monotonic + DEADLINE
125
+ deadline = deadline ? [deadline, monotonic + DEADLINE].min : monotonic + DEADLINE
126
126
  Dir.mktmpdir("quarantine-", @root) do |directory|
127
127
  File.chmod(0700, directory)
128
128
  bundle = File.join(directory, "candidate.bundle")
@@ -179,7 +179,7 @@ module Ace
179
179
  @launch.campaign_children_settled!(journal: selected, commit: prefix, params: params, map: map)
180
180
  @launch.completion_scope!(journal: selected, events: fresh, params: params, map: map, commit: prefix)
181
181
  service_settlement_evidence!(journal: selected, events: fresh, params: params, map: map, commit: prefix)
182
- plan = Organisms::AttemptCoordinator.finished_transition_plan(events: fresh, receipt: result.fetch("receipt"))
182
+ plan = Organisms::AttemptCoordinator.finished_transition_plan(events: fresh, receipt: result.fetch("receipt"), authority: :protected)
183
183
  plan.merge(data: params.slice("attempt_id", "result_id", "head", "candidate_generation").merge(
184
184
  "receipt_digest" => result.fetch("receipt_digest"), "state" => result.dig("receipt", "verdict")))
185
185
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "../molecules/protected_control_exclusion"
3
3
  require_relative "campaign_execution"
4
+ require_relative "assignment_definition"
4
5
 
5
6
  module Ace
6
7
  module Assign
@@ -117,23 +118,8 @@ module Ace
117
118
  end
118
119
 
119
120
  def validated_registration_definition!(params, map)
120
- bytes = params.fetch("definition_bytes")
121
- unless bytes.is_a?(String) && bytes.bytesize <= 32_768 && params["definition_digest"].is_a?(String) &&
122
- Digest::SHA256.hexdigest(bytes) == params["definition_digest"]
123
- raise ArgumentError, "invalid assignment definition digest or size"
124
- end
125
- value = JSON.parse(bytes)
126
- allowed = %w[session_id name description created_at updated_at source_config parent task_id project_id prepared_work review_campaign campaign_execution]
127
- unless value.is_a?(Hash) && (value.keys - allowed).empty? &&
128
- %w[session_id name created_at source_config task_id project_id].all? { |key| value[key].is_a?(String) && !value[key].empty? } &&
129
- value["session_id"] == params["assignment_id"] && value["project_id"] == map["project_id"]
130
- raise ArgumentError, "invalid managed assignment definition"
131
- end
132
- CampaignExecution.validate_parent!(value.fetch("review_campaign")) if value.key?("review_campaign")
133
- assignment = Models::Assignment.from_h(value)
134
- raise ArgumentError, "definition is not managed" unless assignment.managed?
135
- token!(assignment.task_id)
136
- assignment
121
+ AssignmentDefinition.parse(bytes: params.fetch("definition_bytes"), digest: params["definition_digest"],
122
+ assignment_id: params["assignment_id"], project_id: map["project_id"])
137
123
  end
138
124
  end
139
125
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Authority
6
+ module OrdinaryAttemptStart
7
+ START_KEYS = %w[assignment_id attempt_id expected_generation head pane scope].freeze
8
+
9
+ private
10
+
11
+ def start_attempt(request, peer, role, deadline)
12
+ params = request.fetch("params")
13
+ strict!(params, START_KEYS)
14
+ %w[assignment_id attempt_id].each { |key| token!(params.fetch(key)) }
15
+ if params.fetch("attempt_id").start_with?("definition-")
16
+ raise ArgumentError, "ordinary attempt ID uses the registration namespace"
17
+ end
18
+ token!(request.fetch("mutation_id"))
19
+ unless params.fetch("head").is_a?(String) && params.fetch("head").match?(/\A[0-9a-f]{40}\z/)
20
+ raise ArgumentError, "ordinary start HEAD is invalid"
21
+ end
22
+ unless %i[supervisor worker].include?(role) && params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation") == 0
23
+ raise AttemptErrors::UnauthorizedIdentity, "ordinary start requires an installed worker and a new attempt"
24
+ end
25
+ commit = @journal.ref_value
26
+ registered = registration(params.fetch("assignment_id"), commit: commit)
27
+ raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
28
+ work = registered_work(registered, commit, deadline)
29
+ scope = params.fetch("scope")
30
+ unless work.manifest.fetch("steps").any? { |step| step.fetch("number") == scope }
31
+ raise AttemptErrors::InvalidScope, "ordinary scope is not in the accepted work"
32
+ end
33
+ keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
34
+ @exclusion.with_shared_multi(keys, deadline: deadline) do
35
+ if @journal.mutation_result(request.fetch("mutation_id"))
36
+ # Work may legitimately have advanced since an acknowledged or
37
+ # lost start reply. Recover its original head, never recapture it.
38
+ identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
39
+ return @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
40
+ mutation_id: request.fetch("mutation_id"), operation: "start_attempt", parameters_digest: start_digest(params, peer, identity),
41
+ expected_generation: 0, with_replay: true, deadline: deadline,
42
+ replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do
43
+ raise AttemptErrors::EvidenceUnavailable, "accepted ordinary start disappeared"
44
+ end
45
+ end
46
+ # Only the installed admission can observe the actual native owner
47
+ # and worker checkout. It performs no launch or delivery effect.
48
+ observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: params.fetch("head"), deadline: deadline)
49
+ identity = observed.fetch(:identity)
50
+ principal = {"uid" => peer.fetch("uid"), "actor" => identity.actor}
51
+ digest = start_digest(params, peer, identity)
52
+ authorize!(request, peer, role, deadline)
53
+ @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
54
+ mutation_id: request.fetch("mutation_id"), operation: "start_attempt", parameters_digest: digest,
55
+ expected_generation: 0, with_replay: true, deadline: deadline,
56
+ replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, _generation|
57
+ authorize!(request, peer, role, deadline)
58
+ unless @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline) == identity
59
+ raise AttemptErrors::UnauthorizedIdentity, "ordinary native worker changed before start acceptance"
60
+ end
61
+ unless events.empty? && registration(params.fetch("assignment_id"), commit: prefix) == registered
62
+ raise AttemptErrors::Conflict, "ordinary start registration or attempt changed"
63
+ end
64
+ if @journal.assignment_ids(commit: prefix).any? { |assignment|
65
+ assignment != params.fetch("assignment_id") && @journal.read_events(assignment, commit: prefix).any? { |event|
66
+ event.fetch("attempt_id") == params.fetch("attempt_id") } }
67
+ raise AttemptErrors::Conflict, "ordinary attempt ID belongs to another assignment"
68
+ end
69
+ # Sibling scopes may run independently. A different request ID
70
+ # never repeats an already consumed scope or any part of it.
71
+ if @journal.derived_attempts(params.fetch("assignment_id"), commit: prefix).any? { |attempt|
72
+ Atoms::AssignmentScope.overlap?(attempt.binding.scope, scope) }
73
+ raise AttemptErrors::Conflict, "ordinary scope already has an attempt; reconcile its original execution"
74
+ end
75
+ record = current_queue_record(registered, prefix)
76
+ queue = OrdinaryQueue.new(work: work, root: @input_root, bytes: queue_bytes(record, prefix))
77
+ .transition(operation: :activate, scope: scope)
78
+ queue_sha = Digest::SHA256.hexdigest(queue.bytes)
79
+ queue_path = "execution/definitions/#{params.fetch('assignment_id')}-queue-#{queue_sha}.json"
80
+ {events: [
81
+ {type: "intent", payload: {"assignment_id" => params.fetch("assignment_id"), "scope" => scope,
82
+ "project_id" => @project, "task_id" => registered.fetch("task_id"), "base_head" => observed.fetch(:checkout).fetch("head")}},
83
+ {type: "process_start", payload: {"actor" => identity.actor, "role" => identity.role, "runtime" => identity.runtime,
84
+ "pid" => identity.process_pid, "process_identity" => identity.runtime_binding.fetch("process_identity"),
85
+ "runtime_binding" => identity.runtime_binding, "checkout" => observed.fetch(:checkout)}}
86
+ ], blobs: {queue_path => queue.bytes}, data: {
87
+ "authority" => "ordinary", "assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
88
+ "project_id" => @project, "scope" => scope, "state" => "running", "started_by" => principal,
89
+ "base_head" => observed.fetch(:checkout).fetch("head"), "runtime_binding" => identity.runtime_binding,
90
+ "queue_ref" => queue_path, "queue_sha256" => queue_sha}}
91
+ end
92
+ end
93
+ end
94
+
95
+ def start_digest(params, peer, identity)
96
+ Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project,
97
+ "principal" => {"uid" => peer.fetch("uid"), "actor" => identity.actor}})
98
+ end
99
+
100
+ def authorize_start_replay!(request, peer, role, deadline)
101
+ authorize!(request, peer, role, deadline)
102
+ params = request.fetch("params")
103
+ identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
104
+ start_replay_owner!(params, identity)
105
+ end
106
+
107
+ def registered_work(registered, commit, deadline)
108
+ reference = registered.fetch("prepared_work")
109
+ bytes = @journal.bounded_blob(registered.fetch("prepared_bundle_ref"), commit: commit,
110
+ max_bytes: registered.fetch("prepared_bundle_bytes"))
111
+ work = PreparedWork.admit(bytes: bytes, head: reference.fetch("prepared_head"), tree: reference.fetch("prepared_tree"),
112
+ sha256: registered.fetch("prepared_bundle_sha256"), size: registered.fetch("prepared_bundle_bytes"),
113
+ root: @input_root, deadline: deadline)
114
+ unless work.reference(head: reference.fetch("prepared_head"), tree: reference.fetch("prepared_tree")) == reference &&
115
+ work.manifest.values_at("assignment_id", "project_id", "task_id") == registered.values_at("assignment_id", "project_id", "task_id") &&
116
+ Digest::SHA256.hexdigest(work.definition_bytes(head: reference.fetch("prepared_head"), tree: reference.fetch("prepared_tree"))) == registered.fetch("definition_digest")
117
+ raise AttemptErrors::EvidenceUnavailable, "ordinary prepared registration bytes differ"
118
+ end
119
+ work
120
+ end
121
+
122
+ def start_replay_owner!(params, identity)
123
+ events = @journal.read_events(params.fetch("assignment_id"))
124
+ .select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
125
+ unless Models::EvidenceEvent.chain_valid?(events) && events.count { |event| event["type"] == "process_start" } == 1
126
+ raise AttemptErrors::EvidenceUnavailable, "ordinary start evidence is unavailable"
127
+ end
128
+ owner = events.find { |event| event["type"] == "process_start" }.fetch("payload")
129
+ unless owner.values_at("actor", "role", "runtime", "runtime_binding") ==
130
+ [identity.actor, identity.role, identity.runtime, identity.runtime_binding]
131
+ raise AttemptErrors::UnauthorizedIdentity, "ordinary replay belongs to its original native worker"
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end