ace-assign 0.71.0 → 0.73.1
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/steps/auto-merge.step.yml +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +5 -2116
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -4
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +22 -176
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
- data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/command_execution.rb +0 -180
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -457
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -41
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
- data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
- data/lib/ace/assign/authority/ordinary_results.rb +0 -206
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
- data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -309
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
- data/lib/ace/assign/authority/retained_input.rb +0 -53
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -189
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../atoms/candidate_limits"
|
|
4
|
-
|
|
5
|
-
require "json"
|
|
6
|
-
require "digest"
|
|
7
|
-
require "yaml"
|
|
8
|
-
require "date"
|
|
9
|
-
require_relative "../atoms/step_file_parser"
|
|
10
|
-
require_relative "candidate_transfer"
|
|
11
|
-
|
|
12
|
-
module Ace
|
|
13
|
-
module Assign
|
|
14
|
-
module Authority
|
|
15
|
-
# Closed prepared-input tree. Git transport and canonical publication are
|
|
16
|
-
# owned by CandidateTransfer/LaunchLifecycle; this object owns their shared
|
|
17
|
-
# byte contract, never a registration, attempt or progress ledger.
|
|
18
|
-
class PreparedWork
|
|
19
|
-
MAX_TEXT = 1024 * 1024
|
|
20
|
-
MAX_TOTAL = Atoms::CandidateLimits::PREPARED_BYTES
|
|
21
|
-
MAX_MANIFEST = 32_768
|
|
22
|
-
TOKEN = /\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}\z/
|
|
23
|
-
SCOPE = /\A[0-9]+(?:\.[0-9]+)*\z/
|
|
24
|
-
SHA = /\A[0-9a-f]{64}\z/
|
|
25
|
-
TOP = %w[version assignment_id project_id task_id scope job steps context].freeze
|
|
26
|
-
RECORD = %w[bytes filename sha256].freeze
|
|
27
|
-
PROGRESS = %w[status started_at completed_at error stall_reason fork_launch_pid fork_tracked_pids fork_pid_updated_at fork_pid_file].freeze
|
|
28
|
-
attr_reader :manifest, :files, :definition, :selection_sha256, :manifest_sha256
|
|
29
|
-
|
|
30
|
-
def initialize(files:)
|
|
31
|
-
invalid!("file inventory") unless files.is_a?(Hash) && files.size.between?(4, 4096)
|
|
32
|
-
@files = files.to_h do |path, bytes|
|
|
33
|
-
invalid!("file path") unless path.is_a?(String) && !path.start_with?("/") &&
|
|
34
|
-
path.bytesize <= Atoms::CandidateLimits::PATH_BYTES &&
|
|
35
|
-
path.split("/", -1).all? { |part| !part.empty? && part.bytesize <= Atoms::CandidateLimits::PATH_COMPONENT_BYTES &&
|
|
36
|
-
!%w[. .. .git].include?(part.downcase) }
|
|
37
|
-
invalid!("text size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
|
|
38
|
-
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
39
|
-
invalid!("UTF-8") unless text.valid_encoding? && !text.include?("\0")
|
|
40
|
-
[path.dup.freeze, text.freeze]
|
|
41
|
-
end.freeze
|
|
42
|
-
invalid!("expanded size") if @files.values.sum(&:bytesize) > MAX_TOTAL
|
|
43
|
-
manifest_bytes = @files.fetch("manifest.json")
|
|
44
|
-
invalid!("manifest size") if manifest_bytes.bytesize > MAX_MANIFEST
|
|
45
|
-
@manifest = parse_json(manifest_bytes)
|
|
46
|
-
closed!(@manifest, TOP)
|
|
47
|
-
invalid!("version") unless @manifest["version"].is_a?(Integer) && @manifest["version"] == 1
|
|
48
|
-
%w[assignment_id project_id task_id].each { |key| token!(@manifest[key]) }
|
|
49
|
-
scope!(@manifest["scope"])
|
|
50
|
-
canonical = self.class.canonical_manifest(@manifest)
|
|
51
|
-
invalid!("manifest serialization") unless manifest_bytes == canonical + "\n"
|
|
52
|
-
@selection_sha256 = Digest::SHA256.hexdigest(canonical).freeze
|
|
53
|
-
@manifest_sha256 = Digest::SHA256.hexdigest(manifest_bytes).freeze
|
|
54
|
-
@used = %w[manifest.json definition.json]
|
|
55
|
-
record!(@manifest["job"], "job.yaml")
|
|
56
|
-
@job = yaml!(@files.fetch("job.yaml"))
|
|
57
|
-
steps!
|
|
58
|
-
context!
|
|
59
|
-
task_closure!
|
|
60
|
-
selected_root!
|
|
61
|
-
invalid!("undeclared file") unless @used.sort == @files.keys.sort
|
|
62
|
-
definition_bytes = @files.fetch("definition.json")
|
|
63
|
-
invalid!("definition size") if definition_bytes.bytesize > MAX_MANIFEST
|
|
64
|
-
@definition = parse_json(definition_bytes)
|
|
65
|
-
invalid!("base definition") unless @definition.is_a?(Hash) && !@definition.key?("prepared_work") &&
|
|
66
|
-
@definition["session_id"] == @manifest["assignment_id"] && @definition["project_id"] == @manifest["project_id"] && @definition["task_id"] == @manifest["task_id"]
|
|
67
|
-
deep_freeze(@definition)
|
|
68
|
-
deep_freeze(@manifest)
|
|
69
|
-
freeze
|
|
70
|
-
rescue KeyError, JSON::ParserError, Psych::Exception => error
|
|
71
|
-
invalid!(error.message)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# The existing transport verifies complete Git objects before this
|
|
75
|
-
# shared byte contract parses anything. Preserve the original input for
|
|
76
|
-
# the eventual registration journal; CandidateTransfer may reexport it.
|
|
77
|
-
def self.admit(bytes:, head:, tree:, sha256:, size:, root:, deadline: nil)
|
|
78
|
-
transfer = CandidateTransfer.new(root: root)
|
|
79
|
-
transfer.admit(bytes: bytes, head: head, sha256: sha256, size: size, deadline: deadline) do |repository, deadline, result|
|
|
80
|
-
raise ArgumentError, "prepared_input_invalid: Git tree differs" unless result.fetch("tree") == tree
|
|
81
|
-
new(files: transfer.prepared_files(repository: repository, deadline: deadline, head: head))
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def reference(head:, tree:)
|
|
86
|
-
invalid!("Git identity") unless [head, tree].all? { |id| id.is_a?(String) && id.match?(/\A[0-9a-f]{40}\z/) }
|
|
87
|
-
{"version" => 1, "task_id" => manifest.fetch("task_id"), "scope" => manifest.fetch("scope"),
|
|
88
|
-
"prepared_head" => head, "prepared_tree" => tree, "manifest_bytes" => files.fetch("manifest.json").bytesize,
|
|
89
|
-
"manifest_sha256" => manifest_sha256, "selection_sha256" => selection_sha256}.freeze
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def definition_bytes(head:, tree:)
|
|
93
|
-
bytes = JSON.generate(definition.merge("prepared_work" => reference(head: head, tree: tree)))
|
|
94
|
-
invalid!("final definition size") if bytes.bytesize > MAX_MANIFEST
|
|
95
|
-
bytes.freeze
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Mutable queue progress still uses the accepted YAML grammar/bounds.
|
|
99
|
-
# Return the same parsed bytes that the queue decision will consume.
|
|
100
|
-
def parse_queue_step!(bytes)
|
|
101
|
-
invalid!("queue step size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
|
|
102
|
-
match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(bytes)
|
|
103
|
-
invalid!("queue step frontmatter") unless match
|
|
104
|
-
frontmatter = yaml!(match[1])
|
|
105
|
-
invalid!("queue step frontmatter") unless frontmatter.is_a?(Hash)
|
|
106
|
-
{frontmatter: frontmatter, body: bytes[match.end(0)..].strip}
|
|
107
|
-
rescue Psych::Exception => error
|
|
108
|
-
invalid!(error.message)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def parse_queue_metadata!(bytes)
|
|
112
|
-
invalid!("queue metadata size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_MANIFEST)
|
|
113
|
-
yaml!(bytes)
|
|
114
|
-
rescue Psych::Exception => error
|
|
115
|
-
invalid!(error.message)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# StepFileParser owns body/frontmatter semantics; only these progress
|
|
119
|
-
# fields differ from accepted work. Reports are separate mutable output.
|
|
120
|
-
def self.work_projection(bytes)
|
|
121
|
-
parsed = Atoms::StepFileParser.parse(bytes)
|
|
122
|
-
{"frontmatter" => parsed.fetch(:frontmatter).reject { |key, _| PROGRESS.include?(key) }, "body" => parsed.fetch(:body)}
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def self.canonical_manifest(value)
|
|
126
|
-
sorted = lambda do |item|
|
|
127
|
-
case item
|
|
128
|
-
when Hash then item.keys.sort.to_h { |key| [key, sorted.call(item.fetch(key))] }
|
|
129
|
-
when Array then item.map { |entry| sorted.call(entry) }
|
|
130
|
-
else item
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
JSON.generate(TOP.to_h { |key| [key, sorted.call(value.fetch(key))] })
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
private
|
|
137
|
-
|
|
138
|
-
def steps!
|
|
139
|
-
steps = @manifest["steps"]
|
|
140
|
-
invalid!("steps") unless steps.is_a?(Array) && steps.size.between?(1, 256)
|
|
141
|
-
@step_frontmatter = {}
|
|
142
|
-
numbers = steps.map do |step|
|
|
143
|
-
closed!(step, %w[number filename bytes sha256])
|
|
144
|
-
number = step.fetch("number"); scope!(number)
|
|
145
|
-
invalid!("step scope") unless number == @manifest["scope"] || number.start_with?(@manifest["scope"] + ".")
|
|
146
|
-
filename = step.fetch("filename")
|
|
147
|
-
record!(step.reject { |key, _| key == "number" }, "steps/" + filename)
|
|
148
|
-
# Step record filenames are basenames; inventory paths add steps/.
|
|
149
|
-
content = @files.fetch("steps/" + filename)
|
|
150
|
-
match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(content)
|
|
151
|
-
invalid!("step frontmatter") unless match
|
|
152
|
-
fm = yaml!(match[1])
|
|
153
|
-
invalid!("initial status") unless fm.is_a?(Hash) && fm.fetch("status", "pending") == "pending"
|
|
154
|
-
@step_frontmatter[number] = fm
|
|
155
|
-
invalid!("step name") unless fm["name"].is_a?(String) && !fm["name"].empty?
|
|
156
|
-
invalid!("step filename") unless filename == Atoms::StepFileParser.generate_filename(number, fm["name"])
|
|
157
|
-
number
|
|
158
|
-
end
|
|
159
|
-
invalid!("step ordering/root") unless numbers.uniq == numbers && numbers == numbers.sort_by { |n| n.split(".").map(&:to_i) } && numbers.first == @manifest["scope"]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def context!
|
|
163
|
-
context = @manifest["context"]
|
|
164
|
-
invalid!("context") unless context.is_a?(Array) && context.size.between?(1, 256)
|
|
165
|
-
uris = context.map do |entry|
|
|
166
|
-
closed!(entry, %w[uri task_id spec text reports]); token!(entry["task_id"])
|
|
167
|
-
uri = "task://" + entry.fetch("task_id")
|
|
168
|
-
invalid!("context URI") unless entry["uri"] == uri
|
|
169
|
-
prefix = "context/" + entry.fetch("task_id")
|
|
170
|
-
record!(entry["spec"], prefix + "/spec.md")
|
|
171
|
-
record!(entry["text"], prefix + "/bundle.txt")
|
|
172
|
-
reports = entry["reports"]
|
|
173
|
-
invalid!("reports") unless reports.is_a?(Array)
|
|
174
|
-
identities = reports.map do |report|
|
|
175
|
-
closed!(report, %w[assignment_id number file]); token!(report["assignment_id"]); scope!(report["number"])
|
|
176
|
-
record!(report["file"], prefix + "/reports/" + report.fetch("assignment_id") + "/" + report.fetch("number") + ".r.md")
|
|
177
|
-
[report["assignment_id"], report["number"].split(".").map(&:to_i)]
|
|
178
|
-
end
|
|
179
|
-
invalid!("report ordering") unless identities.uniq == identities && identities == identities.sort
|
|
180
|
-
uri
|
|
181
|
-
end
|
|
182
|
-
invalid!("context ordering/selected task") unless uris.uniq == uris && uris == uris.sort && uris.include?("task://" + @manifest["task_id"])
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def selected_root!
|
|
186
|
-
scope = @manifest.fetch("scope"); task = @manifest.fetch("task_id")
|
|
187
|
-
root = @step_frontmatter.fetch(scope)
|
|
188
|
-
invalid!("selected fork root") unless root["context"] == "fork" && root["taskref"] == task
|
|
189
|
-
@step_frontmatter.each do |number, fm|
|
|
190
|
-
invalid!("selected taskref") if fm.key?("taskref") && fm["taskref"] != task
|
|
191
|
-
next if number == scope
|
|
192
|
-
parent = number.split(".")[0...-1].join(".")
|
|
193
|
-
invalid!("selected parent association") unless fm["parent"] == parent && @step_frontmatter.key?(parent)
|
|
194
|
-
end
|
|
195
|
-
invalid!("job steps") unless @job.is_a?(Hash) && @job["steps"].is_a?(Array)
|
|
196
|
-
roots = @job.fetch("steps").select { |step| step.is_a?(Hash) && step["context"] == "fork" && step["taskref"] == task }
|
|
197
|
-
invalid!("job selected root") unless roots.one? && roots.first.values_at("number", "taskref") == [scope, task]
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def task_closure!
|
|
201
|
-
specs = @manifest.fetch("context").to_h do |entry|
|
|
202
|
-
content = @files.fetch(entry.fetch("spec").fetch("filename"))
|
|
203
|
-
match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(content)
|
|
204
|
-
invalid!("task frontmatter") unless match
|
|
205
|
-
task = yaml!(match[1])
|
|
206
|
-
invalid!("task association") unless task.is_a?(Hash) && task["id"] == entry["task_id"] && task.fetch("needs_review", false) == false
|
|
207
|
-
dependencies = task.fetch("dependencies", [])
|
|
208
|
-
invalid!("task dependencies") unless dependencies.is_a?(Array) && dependencies.uniq == dependencies
|
|
209
|
-
dependencies.each { |dependency| token!(dependency) }
|
|
210
|
-
[entry.fetch("task_id"), task]
|
|
211
|
-
end
|
|
212
|
-
selected = specs.fetch(@manifest.fetch("task_id"))
|
|
213
|
-
invalid!("selected reviewed status") unless %w[pending in-progress].include?(selected["status"])
|
|
214
|
-
visiting = []; visited = []
|
|
215
|
-
walk = lambda do |id|
|
|
216
|
-
invalid!("dependency cycle: " + (visiting + [id]).join(" -> ")) if visiting.include?(id)
|
|
217
|
-
return if visited.include?(id)
|
|
218
|
-
visiting << id
|
|
219
|
-
specs.fetch(id).fetch("dependencies", []).each { |dependency| walk.call(dependency) }
|
|
220
|
-
visiting.pop; visited << id
|
|
221
|
-
end
|
|
222
|
-
walk.call(@manifest.fetch("task_id"))
|
|
223
|
-
invalid!("undeclared context") unless visited.sort == specs.keys.sort
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def record!(record, path)
|
|
227
|
-
closed!(record, RECORD)
|
|
228
|
-
# Selected step record uses an exact basename; other records use the
|
|
229
|
-
# exact canonical relative artifact path.
|
|
230
|
-
expected = path.start_with?("steps/") ? path.delete_prefix("steps/") : path
|
|
231
|
-
invalid!("content reference") unless record["filename"] == expected && record["bytes"].is_a?(Integer) && record["bytes"].positive? && record["sha256"].is_a?(String) && record["sha256"].match?(SHA)
|
|
232
|
-
bytes = @files.fetch(path)
|
|
233
|
-
invalid!("content digest") unless record["bytes"] == bytes.bytesize && record["sha256"] == Digest::SHA256.hexdigest(bytes)
|
|
234
|
-
invalid!("duplicate file") if @used.include?(path)
|
|
235
|
-
@used << path
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def yaml!(bytes)
|
|
239
|
-
ast = Psych.parse_stream(bytes)
|
|
240
|
-
invalid!("YAML requires one document") unless ast.children.size == 1
|
|
241
|
-
scanner = Psych::ScalarScanner.new(Psych::ClassLoader::Restricted.new([], []))
|
|
242
|
-
pending = [[ast, 0]]
|
|
243
|
-
until pending.empty?
|
|
244
|
-
node, depth = pending.pop
|
|
245
|
-
invalid!("YAML nesting exceeds 32") if depth > 32
|
|
246
|
-
invalid!("YAML alias") if node.is_a?(Psych::Nodes::Alias)
|
|
247
|
-
if node.is_a?(Psych::Nodes::Mapping)
|
|
248
|
-
keys = node.children.each_slice(2).map(&:first)
|
|
249
|
-
decoded = keys.map do |key|
|
|
250
|
-
invalid!("YAML mapping keys") unless key.is_a?(Psych::Nodes::Scalar) && [nil, "tag:yaml.org,2002:str"].include?(key.tag)
|
|
251
|
-
value = key.plain && key.tag.nil? ? scanner.tokenize(key.value) : key.value
|
|
252
|
-
invalid!("YAML mapping keys must be strings") unless value.is_a?(String)
|
|
253
|
-
value
|
|
254
|
-
end
|
|
255
|
-
invalid!("duplicate YAML key") unless decoded.uniq.size == decoded.size
|
|
256
|
-
end
|
|
257
|
-
(node.children || []).each { |child| pending << [child, depth + 1] }
|
|
258
|
-
end
|
|
259
|
-
value = YAML.safe_load(bytes, permitted_classes: [Time, Date], aliases: false)
|
|
260
|
-
values = [value]
|
|
261
|
-
until values.empty?
|
|
262
|
-
item = values.pop
|
|
263
|
-
case item
|
|
264
|
-
when Hash
|
|
265
|
-
invalid!("YAML mapping keys must be strings") unless item.keys.all? { |key| key.is_a?(String) }
|
|
266
|
-
values.concat(item.values)
|
|
267
|
-
when Array then values.concat(item)
|
|
268
|
-
when String
|
|
269
|
-
invalid!("YAML string encoding") unless item.valid_encoding? && !item.include?("\0")
|
|
270
|
-
when Float then invalid!("YAML nonfinite number") unless item.finite?
|
|
271
|
-
when Integer, TrueClass, FalseClass, NilClass, Time, Date
|
|
272
|
-
# Managed files retain StepFileParser's standard timestamp types.
|
|
273
|
-
else invalid!("unsupported YAML value")
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
value
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def parse_json(bytes)
|
|
280
|
-
JSON.parse(bytes, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def closed!(value, keys)
|
|
284
|
-
invalid!("closed fields") unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def token!(value)
|
|
288
|
-
invalid!("identity") unless value.is_a?(String) && value.match?(TOKEN)
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
def scope!(value)
|
|
292
|
-
invalid!("scope") unless value.is_a?(String) && value.match?(SCOPE)
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def deep_freeze(value)
|
|
296
|
-
case value
|
|
297
|
-
when Hash then value.each { |key, item| deep_freeze(key); deep_freeze(item) }
|
|
298
|
-
when Array then value.each { |item| deep_freeze(item) }
|
|
299
|
-
end
|
|
300
|
-
value.freeze
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def invalid!(reason)
|
|
304
|
-
raise ArgumentError, "prepared_input_invalid: #{reason}"
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "prepared_input"
|
|
4
|
-
require_relative "prepared_queue"
|
|
5
|
-
require_relative "../molecules/fork_session_launcher"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Authority
|
|
10
|
-
# Fixed gate target. Environment identifiers are untrusted lookup hints;
|
|
11
|
-
# the original issued capability and actual kernel birth decide admission.
|
|
12
|
-
class PreparedWorker
|
|
13
|
-
def initialize(kernel: Ace::Runtime::Molecules::ProtectedLinux.new, client_factory: nil, launcher: nil, env: ENV,
|
|
14
|
-
workspace_reader_factory: nil)
|
|
15
|
-
@kernel, @launcher, @env = kernel, launcher, env
|
|
16
|
-
@client_factory = client_factory || ->(mapping) { Client.new(mapping_id: mapping, kernel: @kernel) }
|
|
17
|
-
@workspace_reader_factory = workspace_reader_factory || ->(projection) {
|
|
18
|
-
Molecules::LifecycleExclusion.workspace_reader(projection: projection)
|
|
19
|
-
}
|
|
20
|
-
@started = false
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def run
|
|
24
|
-
raise AttemptErrors::EvidenceUnavailable, "original adapter invocation already activated" if @started
|
|
25
|
-
mapping, assignment, attempt = %w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ASSIGNMENT_ID ACE_ASSIGN_ATTEMPT_ID].map do |key|
|
|
26
|
-
value = @env[key]
|
|
27
|
-
unless value.is_a?(String) && value.match?(PreparedWork::TOKEN)
|
|
28
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original worker selector"
|
|
29
|
-
end
|
|
30
|
-
value
|
|
31
|
-
end
|
|
32
|
-
input = PreparedInput.fetch(client: @client_factory.call(mapping), assignment_id: assignment, attempt_id: attempt)
|
|
33
|
-
self_identity = @kernel.capture(Process.pid)
|
|
34
|
-
@kernel.live!(self_identity)
|
|
35
|
-
original = input.descriptor.fetch("original_worker_identity")
|
|
36
|
-
unless @kernel.same?(self_identity, original)
|
|
37
|
-
raise AttemptErrors::UnauthorizedIdentity, "prepared adapter requires exact original worker self"
|
|
38
|
-
end
|
|
39
|
-
input.drive_prompt
|
|
40
|
-
@workspace_lease = @workspace_reader_factory.call(input.descriptor.fetch("workspace_exclusion"))
|
|
41
|
-
@workspace_lease.acquire!
|
|
42
|
-
@started = true
|
|
43
|
-
queue = PreparedQueue.new(work: input.work, descriptor: input.descriptor)
|
|
44
|
-
root = queue.activate_original!
|
|
45
|
-
unless root
|
|
46
|
-
@workspace_lease.verify_unchanged!
|
|
47
|
-
@workspace_lease.close!
|
|
48
|
-
return {"state" => "completed"}
|
|
49
|
-
end
|
|
50
|
-
launcher = @launcher || Molecules::ForkSessionLauncher.new(config: {})
|
|
51
|
-
result = launcher.launch_provider_session(assignment_id: assignment, fork_root: input.descriptor.fetch("scope"),
|
|
52
|
-
provider: root.fork_provider || Molecules::ForkSessionLauncher::DEFAULT_PROVIDER, cache_dir: queue.directory,
|
|
53
|
-
prepared_input: input)
|
|
54
|
-
queue.with_executor do |executor|
|
|
55
|
-
state = executor.status.fetch(:state)
|
|
56
|
-
unless state.failed.empty? && state.subtree_complete?(input.descriptor.fetch("scope"))
|
|
57
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared provider exited before selected queue completed"
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
@workspace_lease.verify_unchanged!
|
|
61
|
-
@workspace_lease.close!
|
|
62
|
-
result
|
|
63
|
-
rescue SystemCallError, IOError
|
|
64
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: published worker queue or provider output is unavailable"
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
module PrivateDirectory
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
# Source configuration chooses this path. Each ancestor must prevent
|
|
10
|
-
# another principal from replacing the private leaf or its contents.
|
|
11
|
-
def verify!(root)
|
|
12
|
-
path = File.expand_path(root)
|
|
13
|
-
cursor = path
|
|
14
|
-
loop do
|
|
15
|
-
stat = File.lstat(cursor)
|
|
16
|
-
unless stat.directory? && !stat.symlink? && [0, Process.uid].include?(stat.uid) && (stat.mode & 0022).zero?
|
|
17
|
-
raise AttemptErrors::ReceiptRejected, "Private directory ancestry is not protected"
|
|
18
|
-
end
|
|
19
|
-
break if cursor == "/"
|
|
20
|
-
cursor = File.dirname(cursor)
|
|
21
|
-
end
|
|
22
|
-
stat = File.stat(path)
|
|
23
|
-
unless stat.uid == Process.uid && (stat.mode & 0077).zero?
|
|
24
|
-
raise AttemptErrors::ReceiptRejected, "Private directory must be owned by this peer and private"
|
|
25
|
-
end
|
|
26
|
-
path
|
|
27
|
-
rescue SystemCallError, TypeError
|
|
28
|
-
raise AttemptErrors::ReceiptRejected, "Private directory is unavailable"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "deployment_history"
|
|
4
|
-
require_relative "prepared_input"
|
|
5
|
-
require_relative "prepared_queue"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Authority
|
|
10
|
-
# Installed and retained deployment owners determine protected principals.
|
|
11
|
-
# A removed original mapping never silently restores ordinary local mode.
|
|
12
|
-
class ProtectedAssignmentContext
|
|
13
|
-
attr_reader :uid
|
|
14
|
-
|
|
15
|
-
def self.load(uid: Process.uid)
|
|
16
|
-
paths = [Deployment::PATH, DeploymentHistory::PATH]
|
|
17
|
-
installed = paths.any? do |path|
|
|
18
|
-
begin
|
|
19
|
-
File.lstat(path)
|
|
20
|
-
true
|
|
21
|
-
rescue Errno::ENOENT
|
|
22
|
-
false
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
return new(deployment: nil, history: nil, uid: uid) unless installed
|
|
26
|
-
deployment = Deployment.load
|
|
27
|
-
history = DeploymentHistory.load
|
|
28
|
-
unless history.selects?(deployment)
|
|
29
|
-
raise AttemptErrors::EvidenceUnavailable, "installed protected descriptor is not retained"
|
|
30
|
-
end
|
|
31
|
-
new(deployment: deployment, history: history, uid: uid)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def initialize(deployment:, history:, uid: Process.uid, kernel: nil, env: ENV,
|
|
35
|
-
artifacts_factory: -> { Ace::Runtime::Molecules::ProtectedArtifactSet.new })
|
|
36
|
-
@deployment, @history, @uid, @kernel, @env = deployment, history, uid, kernel, env
|
|
37
|
-
@artifacts_factory = artifacts_factory
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# A declared installed account never falls through to local authority.
|
|
41
|
-
# This is classification only; each wire operation still authenticates
|
|
42
|
-
# the current kernel peer and its exact permitted role.
|
|
43
|
-
def protected_participant?
|
|
44
|
-
return false unless @deployment
|
|
45
|
-
raise AttemptErrors::EvidenceUnavailable, "protected retained owners are unavailable" unless @history
|
|
46
|
-
[@deployment, *@history.descriptors].any? do |owner|
|
|
47
|
-
data = owner.data
|
|
48
|
-
principals = data.fetch("authorities").values.map { |authority| authority.fetch("uid") }
|
|
49
|
-
principals.concat(data.fetch("launch_mappings").values.flat_map { |map| map.values_at("launcher_uid", "worker_uid") })
|
|
50
|
-
data.fetch("projects").each_value do |project|
|
|
51
|
-
%w[launcher_uids reviewer_uids worker_uids service_executor_uids supervisor_uids].each do |key|
|
|
52
|
-
principals.concat(project.fetch(key, []))
|
|
53
|
-
end
|
|
54
|
-
principals.concat(project.fetch("service_receivers", {}).values.map { |receiver| receiver.fetch("executor_uid") })
|
|
55
|
-
principals.concat(project.fetch("inbox_contexts", {}).values.map { |context| context.fetch("owner_credentials").fetch("uid") })
|
|
56
|
-
end
|
|
57
|
-
principals.include?(@uid)
|
|
58
|
-
end
|
|
59
|
-
rescue KeyError, TypeError, NoMethodError
|
|
60
|
-
raise AttemptErrors::EvidenceUnavailable, "protected installed principal inventory is malformed"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Read-only current selection; no role grant or private Inbox access.
|
|
64
|
-
def inbox_context_selection(project:, mapping:, inbox_context:)
|
|
65
|
-
unless installed? && [project, mapping, inbox_context].all? { |id| id.is_a?(String) && Deployment::TOKEN.match?(id) }
|
|
66
|
-
raise AttemptErrors::EvidenceUnavailable, "installed inbox selectors differ"
|
|
67
|
-
end
|
|
68
|
-
descriptor = @deployment.artifact_reference
|
|
69
|
-
history = @history.artifact_reference
|
|
70
|
-
unless descriptor && history && @history.selects?(@deployment)
|
|
71
|
-
raise AttemptErrors::EvidenceUnavailable, "installed inbox references are unavailable"
|
|
72
|
-
end
|
|
73
|
-
@artifacts_factory.call.with do |held|
|
|
74
|
-
held.read!(descriptor)
|
|
75
|
-
held.read!(history)
|
|
76
|
-
map = @deployment.mapping(mapping)
|
|
77
|
-
unless map.fetch("project_id") == project
|
|
78
|
-
raise AttemptErrors::EvidenceUnavailable, "installed inbox project differs"
|
|
79
|
-
end
|
|
80
|
-
selected = @deployment.inbox_context(mapping, inbox_context)
|
|
81
|
-
authority = @deployment.authority(map.fetch("authority_id"))
|
|
82
|
-
result = {"uid" => @uid, "project_id" => project, "mapping_id" => mapping,
|
|
83
|
-
"inbox_context_id" => inbox_context, "descriptor" => descriptor, "history" => history,
|
|
84
|
-
"context" => selected.slice("control_socket_path", "owner_credentials", "native_mapping_id"),
|
|
85
|
-
"authority" => authority.slice("socket_path", "uid", "gid", "groups")
|
|
86
|
-
.merge("authority_id" => map.fetch("authority_id"))}
|
|
87
|
-
held.verify_unchanged!
|
|
88
|
-
value = yield result
|
|
89
|
-
held.verify_unchanged!
|
|
90
|
-
value
|
|
91
|
-
end
|
|
92
|
-
rescue ArgumentError, KeyError, TypeError, Ace::Runtime::Error
|
|
93
|
-
raise AttemptErrors::EvidenceUnavailable, "installed inbox selection is unavailable"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def protected_worker?
|
|
97
|
-
return false unless @deployment
|
|
98
|
-
[@deployment, *@history.descriptors].any? do |owner|
|
|
99
|
-
owner.data.fetch("projects").values.any? { |project| project.fetch("worker_uids").include?(@uid) }
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def installed?
|
|
104
|
-
!@deployment.nil? && !@history.nil?
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def refuse_graph_mutation!(options:)
|
|
108
|
-
selected = options.values_at(:mapping, :attempt).any? { |value| value && !value.to_s.empty? } ||
|
|
109
|
-
%w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ATTEMPT_ID].any? { |key| !@env[key].to_s.empty? }
|
|
110
|
-
if protected_worker? || selected
|
|
111
|
-
raise AttemptErrors::EvidenceUnavailable, "protected queue graph changes require a reviewed new prepared version and attempt"
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def resolve(options:, assignment_id:, scope:)
|
|
116
|
-
selected = options.values_at(:mapping, :attempt).any? { |value| value && !value.to_s.empty? } ||
|
|
117
|
-
%w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ATTEMPT_ID].any? { |key| !@env[key].to_s.empty? }
|
|
118
|
-
return nil unless protected_worker? || selected
|
|
119
|
-
unless @deployment && assignment_id && scope && scope.match?(PreparedWork::SCOPE)
|
|
120
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: explicit scoped assignment required"
|
|
121
|
-
end
|
|
122
|
-
attempt = selected_hint(options[:attempt], "ACE_ASSIGN_ATTEMPT_ID")
|
|
123
|
-
hint = @env["ACE_ASSIGN_ASSIGNMENT_ID"].to_s
|
|
124
|
-
unless hint.empty? || hint == assignment_id
|
|
125
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared assignment hint differs"
|
|
126
|
-
end
|
|
127
|
-
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
128
|
-
input = PreparedInput.fetch(client: client(options: options),
|
|
129
|
-
assignment_id: assignment_id, attempt_id: attempt)
|
|
130
|
-
unless input.descriptor.fetch("scope") == scope
|
|
131
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_mismatch: scoped CLI selection"
|
|
132
|
-
end
|
|
133
|
-
input
|
|
134
|
-
rescue KeyError
|
|
135
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original installed mapping is unavailable"
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def verify_attempt_hints!(assignment_id:, attempt_id:)
|
|
139
|
-
selected_hint(assignment_id, "ACE_ASSIGN_ASSIGNMENT_ID")
|
|
140
|
-
selected_hint(attempt_id, "ACE_ASSIGN_ATTEMPT_ID")
|
|
141
|
-
true
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def client(options:)
|
|
145
|
-
raise AttemptErrors::EvidenceUnavailable, "protected installed authority is unavailable" unless @deployment
|
|
146
|
-
mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
147
|
-
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
148
|
-
Client.new(mapping_id: mapping, deployment: @deployment, kernel: @kernel)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def mapping_hint?
|
|
152
|
-
!@env["ACE_ASSIGN_LAUNCH_MAPPING"].to_s.empty?
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Observer/signer commands reuse the installed selection and held
|
|
156
|
-
# descriptor pair. This grants no effect; endpoints authorize the peer.
|
|
157
|
-
def with_inbox_workflow(options:)
|
|
158
|
-
mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
159
|
-
project, inbox_context = options.values_at(:project, :inbox_context)
|
|
160
|
-
inbox_context_selection(project: project, mapping: mapping, inbox_context: inbox_context) do |_selection|
|
|
161
|
-
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
162
|
-
yield @deployment, @kernel, @deployment.mapping(mapping)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Consumers call only after original PreparedInput admission. These
|
|
167
|
-
# existing owners select transport; they do not confer a new grant.
|
|
168
|
-
def with_installed_selection(options:, input:)
|
|
169
|
-
raise AttemptErrors::EvidenceUnavailable, "protected installed authority is unavailable" unless @deployment
|
|
170
|
-
mapping = selected_hint(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
171
|
-
begin
|
|
172
|
-
map = @deployment.mapping(mapping)
|
|
173
|
-
unless input && input.descriptor.fetch("mapping_id") == mapping &&
|
|
174
|
-
input.descriptor.fetch("project_id") == map.fetch("project_id")
|
|
175
|
-
raise AttemptErrors::EvidenceUnavailable, "original prepared installed selection differs"
|
|
176
|
-
end
|
|
177
|
-
rescue KeyError
|
|
178
|
-
raise AttemptErrors::EvidenceUnavailable, "original installed mapping is unavailable"
|
|
179
|
-
end
|
|
180
|
-
@kernel ||= Ace::Runtime::Molecules::ProtectedLinux.new
|
|
181
|
-
yield @deployment, @kernel, mapping
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
private
|
|
185
|
-
|
|
186
|
-
def selected_hint(explicit, key)
|
|
187
|
-
hint = @env[key].to_s
|
|
188
|
-
value = explicit || hint
|
|
189
|
-
unless value.is_a?(String) && value.match?(PreparedWork::TOKEN) && (hint.empty? || hint == value)
|
|
190
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared selector hint differs"
|
|
191
|
-
end
|
|
192
|
-
value
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Each supported operation holds the existing private queue exclusion and
|
|
197
|
-
# consumes its authenticated held snapshot. No graph-changing API exists.
|
|
198
|
-
class PreparedExecutor
|
|
199
|
-
def initialize(input)
|
|
200
|
-
@queue = PreparedQueue.new(work: input.work, descriptor: input.descriptor)
|
|
201
|
-
@scope = input.descriptor.fetch("scope")
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def status
|
|
205
|
-
@queue.with_executor(&:status)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
%i[start_step finish_step fail].each do |operation|
|
|
209
|
-
define_method(operation) do |*args, **options|
|
|
210
|
-
unless options[:fork_root] == @scope && (!options[:step_number] ||
|
|
211
|
-
options[:step_number] == @scope || options[:step_number].start_with?(@scope + "."))
|
|
212
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared operation scope differs"
|
|
213
|
-
end
|
|
214
|
-
@queue.with_executor { |executor| executor.public_send(operation, *args, **options) }
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def method_missing(*)
|
|
219
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared queue graph changes require a reviewed new prepared version and attempt"
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def respond_to_missing?(*); false; end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|