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.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/composition-rules.yml +0 -0
- data/.ace-defaults/assign/catalog/recipes/batch-tasks.recipe.yml +0 -0
- data/.ace-defaults/assign/catalog/recipes/documentation.recipe.yml +0 -0
- data/.ace-defaults/assign/catalog/recipes/fix-and-review.recipe.yml +0 -0
- data/.ace-defaults/assign/catalog/recipes/implement-simple.recipe.yml +0 -0
- data/.ace-defaults/assign/catalog/recipes/implement-with-pr.recipe.yml +0 -0
- data/.ace-defaults/assign/catalog/recipes/release-only.recipe.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/apply-feedback.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/commit.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/fix-tests.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/lint.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/onboard-base.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/push-to-remote.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/rebase-with-main.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/record-demo.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/reorganize-commits.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/research.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/security-audit.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/squash-changelog.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/task-load.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/update-docs.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +0 -0
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -0
- data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
- data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
- data/.ace-defaults/assign/presets/quick-implement.yml +0 -0
- data/.ace-defaults/assign/presets/release-only.yml +0 -0
- data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
- data/.ace-defaults/assign/presets/work-on-docs.yml +0 -0
- data/.ace-defaults/assign/presets/work-on-task-in-place.yml +0 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-assign.yml +0 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-assign.yml +0 -0
- data/CHANGELOG.md +17 -0
- data/Rakefile +0 -0
- data/docs/demo/canonical-skill-source.gif +0 -0
- data/docs/demo/fork-provider.cast +0 -0
- data/docs/demo/fork-provider.gif +0 -0
- data/docs/demo/fork-provider.recording.json +0 -0
- data/docs/demo/fork-provider.tape.yml +0 -0
- data/docs/getting-started.md +0 -0
- data/docs/handbook.md +0 -0
- data/handbook/guides/fork-context.g.md +0 -0
- data/handbook/skills/as-assign-add-task/SKILL.md +0 -0
- data/handbook/skills/as-assign-compose/SKILL.md +0 -0
- data/handbook/skills/as-assign-create/SKILL.md +0 -0
- data/handbook/skills/as-assign-drive/SKILL.md +0 -0
- data/handbook/skills/as-assign-prepare/SKILL.md +0 -0
- data/handbook/skills/as-assign-recover-fork/SKILL.md +0 -0
- data/handbook/skills/as-assign-run-in-batches/SKILL.md +0 -0
- data/handbook/skills/as-assign-start/SKILL.md +0 -0
- data/handbook/skills/as-create-retro-internal/SKILL.md +0 -0
- data/handbook/skills/as-mark-task-done-internal/SKILL.md +0 -0
- data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +0 -0
- data/handbook/skills/as-task-load-internal/SKILL.md +0 -0
- data/handbook/workflow-instructions/assign/add-task.wf.md +0 -0
- data/handbook/workflow-instructions/assign/compose.wf.md +0 -0
- data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +0 -0
- data/handbook/workflow-instructions/assign/create.wf.md +0 -0
- data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +0 -0
- data/handbook/workflow-instructions/assign/recover-fork.wf.md +0 -0
- data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +0 -0
- data/handbook/workflow-instructions/assign/run-in-batches.wf.md +0 -0
- data/handbook/workflow-instructions/assign/start.wf.md +0 -0
- data/handbook/workflow-instructions/assign/task-load-internal.wf.md +0 -0
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +0 -0
- data/lib/ace/assign/atoms/assign_frontmatter_parser.rb +0 -0
- data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
- data/lib/ace/assign/atoms/catalog_loader.rb +0 -0
- data/lib/ace/assign/atoms/composition_rules.rb +0 -0
- data/lib/ace/assign/atoms/number_generator.rb +0 -0
- data/lib/ace/assign/atoms/preset_expander.rb +0 -0
- data/lib/ace/assign/atoms/preset_loader.rb +0 -0
- data/lib/ace/assign/atoms/preset_step_resolver.rb +0 -0
- data/lib/ace/assign/atoms/step_file_parser.rb +0 -0
- data/lib/ace/assign/atoms/step_numbering.rb +0 -0
- data/lib/ace/assign/atoms/step_sorter.rb +0 -0
- data/lib/ace/assign/atoms/tree_formatter.rb +0 -0
- data/lib/ace/assign/authority/assignment_definition.rb +35 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +2 -2
- data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
- data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
- data/lib/ace/assign/authority/ordinary_client.rb +70 -0
- data/lib/ace/assign/authority/ordinary_completion.rb +94 -0
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +191 -0
- data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
- data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
- data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
- data/lib/ace/assign/authority/ordinary_results.rb +196 -0
- data/lib/ace/assign/authority/ordinary_server.rb +75 -0
- data/lib/ace/assign/authority/prepared_work.rb +2 -2
- data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
- data/lib/ace/assign/authority/router.rb +11 -6
- data/lib/ace/assign/authority/server.rb +11 -180
- data/lib/ace/assign/authority/socket_server.rb +255 -0
- data/lib/ace/assign/authority/transfer_codec.rb +2 -2
- data/lib/ace/assign/cli/commands/add.rb +0 -0
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
- data/lib/ace/assign/cli/commands/list.rb +0 -0
- data/lib/ace/assign/cli/commands/retry_cmd.rb +0 -0
- data/lib/ace/assign/models/assignment_info.rb +0 -0
- data/lib/ace/assign/models/queue_state.rb +0 -0
- data/lib/ace/assign/models/step.rb +0 -0
- data/lib/ace/assign/molecules/assignment_discoverer.rb +0 -0
- data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
- data/lib/ace/assign/molecules/evidence_journal.rb +141 -34
- data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +5 -3
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
- data/lib/ace/assign/molecules/preset_inferrer.rb +0 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +0 -0
- data/lib/ace/assign/molecules/step_renumberer.rb +0 -0
- data/lib/ace/assign/organisms/attempt_coordinator.rb +58 -36
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
- data/lib/ace/assign/version.rb +1 -1
- metadata +24 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9de34921fd3a299eabfd530c8df3c76fed9405606cb0e717a0458fed9ecf614e
|
|
4
|
+
data.tar.gz: a9a2d22448d0c100b5404ce9ca9666b72ed0e6fb3d9bf158a5051ce001b703b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f68696b349c145768cf15634945c0fe40a659a80394f6053b37fd4270c244d8884345dbc9ba438a257de66593a0e44d88feca77c3c2580ea0e5208221d896746
|
|
7
|
+
data.tar.gz: 07c573b0f8160c1929fbba896362180ad2bd08097ab306139ca0e5a28c00762650845840f8709e16f825660add6057b6569ce49a1f7595370c2f83a6740c4e07
|
|
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
|
|
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
|
|
@@ -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:
|
|
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:
|
|
40
|
+
- name: request-authorized-publication
|
|
41
41
|
number: "040"
|
|
42
42
|
instructions: |
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
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
|
data/docs/demo/fork-provider.gif
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/docs/getting-started.md
CHANGED
|
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
|
|
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
|
|
121
|
-
|
|
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
|