ace-assign 0.70.0 → 0.73.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/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 +3 -2099
- 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 -1
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
- 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 +2 -1
- 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 +4 -161
- 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/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/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_transfer.rb +0 -327
- 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/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -455
- 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 -207
- 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_settlement.rb +0 -152
- data/lib/ace/assign/authority/endcap_services.rb +0 -598
- 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/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 -94
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
- 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_results.rb +0 -196
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- 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 -304
- 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/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 -187
- 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 -153
- 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 -1912
- 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 -273
- 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 -314
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- 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,185 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
require "yaml"
|
|
6
|
-
require_relative "prepared_work"
|
|
7
|
-
require_relative "private_directory"
|
|
8
|
-
require_relative "../molecules/lifecycle_exclusion"
|
|
9
|
-
require_relative "../organisms/assignment_executor"
|
|
10
|
-
|
|
11
|
-
module Ace
|
|
12
|
-
module Assign
|
|
13
|
-
module Authority
|
|
14
|
-
# Existing assignment/progress files, rooted only in the authenticated
|
|
15
|
-
# original worker scratch. This cache never supplies authority or a graph.
|
|
16
|
-
class PreparedQueue
|
|
17
|
-
attr_reader :cache_base, :directory, :work, :descriptor
|
|
18
|
-
|
|
19
|
-
def initialize(work:, descriptor:)
|
|
20
|
-
@work, @descriptor = work, descriptor
|
|
21
|
-
@scratch = PrivateDirectory.verify!(descriptor.fetch("original_worker_scratch_root"))
|
|
22
|
-
%w[mapping_id attempt_id assignment_id].each do |field|
|
|
23
|
-
value = descriptor.fetch(field)
|
|
24
|
-
unavailable!("queue identity") unless value.is_a?(String) && value.match?(PreparedWork::TOKEN)
|
|
25
|
-
end
|
|
26
|
-
@cache_base = File.join(@scratch, "prepared-queues", descriptor.fetch("mapping_id"), descriptor.fetch("attempt_id"))
|
|
27
|
-
@directory = File.join(cache_base, descriptor.fetch("assignment_id"))
|
|
28
|
-
definition = JSON.parse(work.definition_bytes(head: descriptor.fetch("prepared_head"), tree: descriptor.fetch("prepared_tree")))
|
|
29
|
-
unavailable!("captured assignment metadata") unless %w[name source_config created_at].all? do |field|
|
|
30
|
-
definition[field].is_a?(String) && !definition[field].empty?
|
|
31
|
-
end
|
|
32
|
-
validate_timestamp!(definition.fetch("created_at"))
|
|
33
|
-
validate_timestamp!(definition["updated_at"]) if definition.key?("updated_at")
|
|
34
|
-
@metadata = Models::Assignment.from_h(definition, cache_dir: directory).to_h
|
|
35
|
-
lock_root = File.join(@scratch, "prepared-queue-exclusion")
|
|
36
|
-
private_directory!(lock_root, create: true)
|
|
37
|
-
@exclusion = Molecules::LifecycleExclusion.new(root: lock_root)
|
|
38
|
-
@key = @exclusion.assignment_key(descriptor.values_at("mapping_id", "attempt_id", "assignment_id").join(":"))
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Called ONLY after the fixed adapter proves actual original self. Later
|
|
42
|
-
# CLI consumers use with_executor and cannot recreate a missing queue.
|
|
43
|
-
def activate_original!
|
|
44
|
-
@exclusion.with_exclusive(@key) do
|
|
45
|
-
publish_initial! unless File.exist?(directory) || File.symlink?(directory)
|
|
46
|
-
executor = verified_executor
|
|
47
|
-
result = executor.status
|
|
48
|
-
state = result.fetch(:state)
|
|
49
|
-
unavailable!("selected queue has failed progress") unless state.failed.empty?
|
|
50
|
-
return nil if state.subtree_complete?(descriptor.fetch("scope"))
|
|
51
|
-
unless state.steps.all? { |step| step.status == :pending }
|
|
52
|
-
unavailable!("original adapter has already activated queue progress")
|
|
53
|
-
end
|
|
54
|
-
root = state.find_by_number(descriptor.fetch("scope"))
|
|
55
|
-
unavailable!("original selected fork root") unless root && root.fork?
|
|
56
|
-
executor.step_writer.mark_active(root.file_path)
|
|
57
|
-
# Same held-byte consumer validates the resulting progress before
|
|
58
|
-
# returning to the adapter that may now start its provider.
|
|
59
|
-
executor.status
|
|
60
|
-
root
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def with_executor
|
|
65
|
-
@exclusion.with_exclusive(@key) do
|
|
66
|
-
unavailable!("published queue is unavailable; no reconstruction") unless File.directory?(directory)
|
|
67
|
-
yield verified_executor
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def verified_executor
|
|
74
|
-
private_directory!(directory)
|
|
75
|
-
private_directory!(File.join(directory, "steps"))
|
|
76
|
-
private_directory!(File.join(directory, "reports"))
|
|
77
|
-
metadata = work.parse_queue_metadata!(held_read(File.join(directory, "assignment.yaml"), PreparedWork::MAX_MANIFEST))
|
|
78
|
-
unavailable!("queue assignment metadata differs") unless metadata.is_a?(Hash) &&
|
|
79
|
-
metadata.reject { |key, _| key == "updated_at" } == @metadata.reject { |key, _| key == "updated_at" }
|
|
80
|
-
validate_timestamp!(metadata["updated_at"]) if metadata.key?("updated_at")
|
|
81
|
-
unavailable!("queue job differs") unless held_read(File.join(directory, "job.yaml"), PreparedWork::MAX_TEXT) == work.files.fetch("job.yaml")
|
|
82
|
-
owner = self
|
|
83
|
-
scanner = Molecules::QueueScanner.new
|
|
84
|
-
parser = scanner.method(:scan_captured)
|
|
85
|
-
scanner.define_singleton_method(:scan) do |steps_dir, assignment:|
|
|
86
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared queue scanner path differs" unless steps_dir == File.join(owner.directory, "steps")
|
|
87
|
-
steps, reports = owner.send(:captured_queue)
|
|
88
|
-
parser.call(steps: steps, assignment: assignment, reports: reports)
|
|
89
|
-
end
|
|
90
|
-
Organisms::AssignmentExecutor.new(cache_base: cache_base,
|
|
91
|
-
selected_assignment: Models::Assignment.from_h(metadata, cache_dir: directory), queue_scanner: scanner)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def captured_queue
|
|
95
|
-
expected = work.manifest.fetch("steps").map { |entry| entry.fetch("filename") }
|
|
96
|
-
steps_dir = File.join(directory, "steps")
|
|
97
|
-
unavailable!("queue step inventory differs") unless Dir.children(steps_dir).sort == expected.sort
|
|
98
|
-
steps = expected.to_h do |filename|
|
|
99
|
-
bytes = held_read(File.join(steps_dir, filename), PreparedWork::MAX_TEXT)
|
|
100
|
-
parsed = work.parse_queue_step!(bytes)
|
|
101
|
-
accepted = work.parse_queue_step!(work.files.fetch("steps/" + filename))
|
|
102
|
-
projection = ->(value) { value.fetch(:frontmatter).reject { |key, _| PreparedWork::PROGRESS.include?(key) } }
|
|
103
|
-
unless projection.call(parsed) == projection.call(accepted) && parsed.fetch(:body) == accepted.fetch(:body) &&
|
|
104
|
-
%w[pending active done failed].include?(parsed.fetch(:frontmatter).fetch("status", "pending"))
|
|
105
|
-
unavailable!("queue work-bearing input differs")
|
|
106
|
-
end
|
|
107
|
-
[filename, bytes]
|
|
108
|
-
end
|
|
109
|
-
reports_dir = File.join(directory, "reports")
|
|
110
|
-
allowed = work.manifest.fetch("steps").map do |entry|
|
|
111
|
-
name = work.parse_queue_step!(work.files.fetch("steps/" + entry.fetch("filename"))).fetch(:frontmatter).fetch("name")
|
|
112
|
-
Atoms::StepFileParser.generate_report_filename(entry.fetch("number"), name)
|
|
113
|
-
end
|
|
114
|
-
reports = Dir.children(reports_dir).to_h do |filename|
|
|
115
|
-
unavailable!("queue report inventory differs") unless allowed.include?(filename)
|
|
116
|
-
bytes = held_read(File.join(reports_dir, filename), PreparedWork::MAX_TEXT)
|
|
117
|
-
work.parse_queue_step!(bytes)
|
|
118
|
-
[filename, bytes]
|
|
119
|
-
end
|
|
120
|
-
[steps.freeze, reports.freeze]
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def publish_initial!
|
|
124
|
-
private_directory!(File.join(@scratch, "prepared-queues"), create: true)
|
|
125
|
-
private_directory!(File.dirname(cache_base), create: true)
|
|
126
|
-
private_directory!(cache_base, create: true)
|
|
127
|
-
stage = File.join(cache_base, ".prepared-#{SecureRandom.hex(12)}")
|
|
128
|
-
Dir.mkdir(stage, 0700)
|
|
129
|
-
%w[steps reports].each { |name| Dir.mkdir(File.join(stage, name), 0700) }
|
|
130
|
-
durable_write(File.join(stage, "assignment.yaml"), @metadata.to_yaml)
|
|
131
|
-
durable_write(File.join(stage, "job.yaml"), work.files.fetch("job.yaml"))
|
|
132
|
-
work.manifest.fetch("steps").each do |entry|
|
|
133
|
-
name = entry.fetch("filename")
|
|
134
|
-
durable_write(File.join(stage, "steps", name), work.files.fetch("steps/" + name))
|
|
135
|
-
end
|
|
136
|
-
%w[steps reports].each { |name| File.open(File.join(stage, name), File::RDONLY) { |file| file.fsync } }
|
|
137
|
-
File.open(stage, File::RDONLY) { |file| file.fsync }
|
|
138
|
-
unavailable!("queue publication collision") if File.exist?(directory) || File.symlink?(directory)
|
|
139
|
-
File.rename(stage, directory)
|
|
140
|
-
File.open(cache_base, File::RDONLY) { |file| file.fsync }
|
|
141
|
-
ensure
|
|
142
|
-
FileUtils.remove_entry(stage) if stage && File.directory?(stage) && !File.symlink?(stage)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def private_directory!(path, create: false)
|
|
146
|
-
Dir.mkdir(path, 0700) if create && !File.exist?(path) && !File.symlink?(path)
|
|
147
|
-
PrivateDirectory.verify!(path)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def durable_write(path, bytes)
|
|
151
|
-
File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) do |file|
|
|
152
|
-
file.write(bytes); file.flush; file.fsync
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def held_read(path, limit)
|
|
157
|
-
File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
|
|
158
|
-
before = file.stat
|
|
159
|
-
unavailable!("queue held file kind/size") unless before.file? && before.uid == Process.uid &&
|
|
160
|
-
(before.mode & 0077).zero? && before.size.between?(1, limit)
|
|
161
|
-
bytes = file.read(limit + 1).force_encoding(Encoding::UTF_8)
|
|
162
|
-
after = file.stat
|
|
163
|
-
unavailable!("queue held file changed") unless bytes.valid_encoding? && !bytes.include?("\0") &&
|
|
164
|
-
bytes.bytesize == before.size && [before.dev, before.ino, before.size, before.mtime, before.ctime] ==
|
|
165
|
-
[after.dev, after.ino, after.size, after.mtime, after.ctime]
|
|
166
|
-
bytes.freeze
|
|
167
|
-
end
|
|
168
|
-
rescue SystemCallError
|
|
169
|
-
unavailable!("queue held file unavailable")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def unavailable!(reason)
|
|
173
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: #{reason}"
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def validate_timestamp!(value)
|
|
177
|
-
unavailable!("captured assignment timestamp") unless value.is_a?(String)
|
|
178
|
-
Time.iso8601(value)
|
|
179
|
-
rescue ArgumentError
|
|
180
|
-
unavailable!("captured assignment timestamp")
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "protected_assignment_context"
|
|
4
|
-
require_relative "task_context_entry"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# The selected immutable Lab owner sets the hook before entry evaluation.
|
|
10
|
-
# Its presence alone never grants scope; original fetch admission decides.
|
|
11
|
-
class PreparedTaskContext
|
|
12
|
-
SCHEMA = "ace.assign.prepared-task-context/v1"
|
|
13
|
-
MAX_RESPONSE = 6_307_840
|
|
14
|
-
PRINCIPAL_SCHEMA = "ace.assign.task-context-principal/v1"
|
|
15
|
-
SELECTION_SCHEMA = "ace.assign.task-context-selection/v1"
|
|
16
|
-
MAX_PRINCIPAL = 1_024
|
|
17
|
-
MAX_SELECTION = 65_536
|
|
18
|
-
|
|
19
|
-
def initialize(context: nil)
|
|
20
|
-
@context = context
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(mapping:, assignment:, attempt:, task:)
|
|
24
|
-
unavailable!("task-context task selector") unless task.is_a?(String) && task.match?(PreparedWork::TOKEN)
|
|
25
|
-
selected_entry = selected_entry!
|
|
26
|
-
input = resolved_input(mapping: mapping, assignment: assignment, attempt: attempt)
|
|
27
|
-
unavailable!("original protected task-context admission") unless input.descriptor.fetch("task_context_entry") == selected_entry
|
|
28
|
-
item = input.work.manifest.fetch("context").find { |entry| entry.fetch("task_id") == task }
|
|
29
|
-
unavailable!("task is outside captured original context") unless item
|
|
30
|
-
text = input.work.files.fetch(item.fetch("text").fetch("filename"))
|
|
31
|
-
record = input.descriptor.slice("mapping_id", "assignment_id", "attempt_id", "scope", "definition_digest", "selection_sha256")
|
|
32
|
-
.merge("schema" => SCHEMA, "task_id" => task, "text" => text)
|
|
33
|
-
response!(record, MAX_RESPONSE)
|
|
34
|
-
rescue ArgumentError, KeyError
|
|
35
|
-
unavailable!("task-context input or original entry differs")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def principal
|
|
39
|
-
selected_entry!
|
|
40
|
-
context = installed_context!
|
|
41
|
-
response!({"schema" => PRINCIPAL_SCHEMA, "uid" => context.uid, "protected_worker" => context.protected_worker?}, MAX_PRINCIPAL)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def selection(mapping:, assignment:, attempt:)
|
|
45
|
-
selected_entry!
|
|
46
|
-
# Current discovery selects a trusted classifier only. Original fetch
|
|
47
|
-
# independently selects the retained entry; no text leaves this mode.
|
|
48
|
-
input = resolved_input(mapping: mapping, assignment: assignment, attempt: attempt)
|
|
49
|
-
response!(input.descriptor.slice("mapping_id", "assignment_id", "attempt_id", "scope", "definition_digest", "selection_sha256", "task_context_entry")
|
|
50
|
-
.merge("schema" => SELECTION_SCHEMA), MAX_SELECTION)
|
|
51
|
-
rescue ArgumentError, KeyError
|
|
52
|
-
unavailable!("task-context original selection differs")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def inbox_principal
|
|
56
|
-
selected_entry!
|
|
57
|
-
context = installed_context!
|
|
58
|
-
response!({"schema" => "ace.assign.inbox-context-principal/v1", "uid" => context.uid,
|
|
59
|
-
"protected_participant" => context.protected_participant?}, MAX_PRINCIPAL)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def inbox_selection(project:, mapping:, inbox_context:)
|
|
63
|
-
selected_entry!
|
|
64
|
-
installed_context!.inbox_context_selection(project: project, mapping: mapping, inbox_context: inbox_context) do |record|
|
|
65
|
-
response!(record.merge("schema" => "ace.assign.inbox-context-selection/v1"), 16_384)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def selected_entry!
|
|
72
|
-
unless Object.const_defined?(:ACE_PROTECTED_TASK_CONTEXT_ENTRY, false)
|
|
73
|
-
unavailable!("selected original task-context entry is absent")
|
|
74
|
-
end
|
|
75
|
-
selected_entry = Object.const_get(:ACE_PROTECTED_TASK_CONTEXT_ENTRY, false)
|
|
76
|
-
TaskContextEntry.validate!(selected_entry)
|
|
77
|
-
objects = [selected_entry, *selected_entry.keys, *selected_entry.values]
|
|
78
|
-
selected_entry.values.each { |reference| objects.concat(reference.keys); objects.concat(reference.values) }
|
|
79
|
-
unavailable!("selected task-context entry is mutable") unless objects.all?(&:frozen?)
|
|
80
|
-
selected_entry
|
|
81
|
-
rescue ArgumentError, KeyError, TypeError
|
|
82
|
-
unavailable!("task-context selected entry differs")
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def installed_context!
|
|
86
|
-
@context ||= ProtectedAssignmentContext.load
|
|
87
|
-
unavailable!("published task-context installed owner is absent") unless @context.installed?
|
|
88
|
-
@context
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def resolved_input(mapping:, assignment:, attempt:)
|
|
92
|
-
unless [mapping, attempt].all? { |value| value.is_a?(String) && value.match?(PreparedWork::TOKEN) } && assignment.is_a?(String)
|
|
93
|
-
unavailable!("task-context selectors")
|
|
94
|
-
end
|
|
95
|
-
assignment_id, scope = assignment.split("@", 2)
|
|
96
|
-
unless assignment_id&.match?(PreparedWork::TOKEN) && scope&.match?(PreparedWork::SCOPE)
|
|
97
|
-
unavailable!("task-context scoped assignment")
|
|
98
|
-
end
|
|
99
|
-
input = installed_context!.resolve(options: {mapping: mapping, attempt: attempt}, assignment_id: assignment_id, scope: scope)
|
|
100
|
-
unavailable!("original protected task-context admission") unless input
|
|
101
|
-
input
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def response!(record, limit)
|
|
105
|
-
response = JSON.generate(record) + "\n"
|
|
106
|
-
unavailable!("task-context response bound") if response.bytesize > limit
|
|
107
|
-
response.freeze
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def unavailable!(reason)
|
|
111
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: #{reason}"
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "digest"
|
|
5
|
-
require "yaml"
|
|
6
|
-
require "date"
|
|
7
|
-
require_relative "../atoms/step_file_parser"
|
|
8
|
-
require_relative "candidate_transfer"
|
|
9
|
-
|
|
10
|
-
module Ace
|
|
11
|
-
module Assign
|
|
12
|
-
module Authority
|
|
13
|
-
# Closed prepared-input tree. Git transport and canonical publication are
|
|
14
|
-
# owned by CandidateTransfer/LaunchLifecycle; this object owns their shared
|
|
15
|
-
# byte contract, never a registration, attempt or progress ledger.
|
|
16
|
-
class PreparedWork
|
|
17
|
-
MAX_TEXT = 1024 * 1024
|
|
18
|
-
MAX_TOTAL = 64 * 1024 * 1024
|
|
19
|
-
MAX_MANIFEST = 32_768
|
|
20
|
-
TOKEN = /\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}\z/
|
|
21
|
-
SCOPE = /\A[0-9]+(?:\.[0-9]+)*\z/
|
|
22
|
-
SHA = /\A[0-9a-f]{64}\z/
|
|
23
|
-
TOP = %w[version assignment_id project_id task_id scope job steps context].freeze
|
|
24
|
-
RECORD = %w[bytes filename sha256].freeze
|
|
25
|
-
PROGRESS = %w[status started_at completed_at error stall_reason fork_launch_pid fork_tracked_pids fork_pid_updated_at fork_pid_file].freeze
|
|
26
|
-
attr_reader :manifest, :files, :definition, :selection_sha256, :manifest_sha256
|
|
27
|
-
|
|
28
|
-
def initialize(files:)
|
|
29
|
-
invalid!("file inventory") unless files.is_a?(Hash) && files.size.between?(4, 4096)
|
|
30
|
-
@files = files.to_h do |path, bytes|
|
|
31
|
-
invalid!("file path") unless path.is_a?(String) && !path.start_with?("/") && path.split("/", -1).all? { |part| !part.empty? && !%w[. .. .git].include?(part.downcase) }
|
|
32
|
-
invalid!("text size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
|
|
33
|
-
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
34
|
-
invalid!("UTF-8") unless text.valid_encoding? && !text.include?("\0")
|
|
35
|
-
[path.dup.freeze, text.freeze]
|
|
36
|
-
end.freeze
|
|
37
|
-
invalid!("expanded size") if @files.values.sum(&:bytesize) > MAX_TOTAL
|
|
38
|
-
manifest_bytes = @files.fetch("manifest.json")
|
|
39
|
-
invalid!("manifest size") if manifest_bytes.bytesize > MAX_MANIFEST
|
|
40
|
-
@manifest = parse_json(manifest_bytes)
|
|
41
|
-
closed!(@manifest, TOP)
|
|
42
|
-
invalid!("version") unless @manifest["version"].is_a?(Integer) && @manifest["version"] == 1
|
|
43
|
-
%w[assignment_id project_id task_id].each { |key| token!(@manifest[key]) }
|
|
44
|
-
scope!(@manifest["scope"])
|
|
45
|
-
canonical = self.class.canonical_manifest(@manifest)
|
|
46
|
-
invalid!("manifest serialization") unless manifest_bytes == canonical + "\n"
|
|
47
|
-
@selection_sha256 = Digest::SHA256.hexdigest(canonical).freeze
|
|
48
|
-
@manifest_sha256 = Digest::SHA256.hexdigest(manifest_bytes).freeze
|
|
49
|
-
@used = %w[manifest.json definition.json]
|
|
50
|
-
record!(@manifest["job"], "job.yaml")
|
|
51
|
-
@job = yaml!(@files.fetch("job.yaml"))
|
|
52
|
-
steps!
|
|
53
|
-
context!
|
|
54
|
-
task_closure!
|
|
55
|
-
selected_root!
|
|
56
|
-
invalid!("undeclared file") unless @used.sort == @files.keys.sort
|
|
57
|
-
definition_bytes = @files.fetch("definition.json")
|
|
58
|
-
invalid!("definition size") if definition_bytes.bytesize > MAX_MANIFEST
|
|
59
|
-
@definition = parse_json(definition_bytes)
|
|
60
|
-
invalid!("base definition") unless @definition.is_a?(Hash) && !@definition.key?("prepared_work") &&
|
|
61
|
-
@definition["session_id"] == @manifest["assignment_id"] && @definition["project_id"] == @manifest["project_id"] && @definition["task_id"] == @manifest["task_id"]
|
|
62
|
-
deep_freeze(@definition)
|
|
63
|
-
deep_freeze(@manifest)
|
|
64
|
-
freeze
|
|
65
|
-
rescue KeyError, JSON::ParserError, Psych::Exception => error
|
|
66
|
-
invalid!(error.message)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# The existing transport verifies complete Git objects before this
|
|
70
|
-
# shared byte contract parses anything. Preserve the original input for
|
|
71
|
-
# the eventual registration journal; CandidateTransfer may reexport it.
|
|
72
|
-
def self.admit(bytes:, head:, tree:, sha256:, size:, root:, deadline: nil)
|
|
73
|
-
transfer = CandidateTransfer.new(root: root)
|
|
74
|
-
transfer.admit(bytes: bytes, head: head, sha256: sha256, size: size, deadline: deadline) do |repository, deadline, result|
|
|
75
|
-
raise ArgumentError, "prepared_input_invalid: Git tree differs" unless result.fetch("tree") == tree
|
|
76
|
-
new(files: transfer.prepared_files(repository: repository, deadline: deadline, head: head))
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def reference(head:, tree:)
|
|
81
|
-
invalid!("Git identity") unless [head, tree].all? { |id| id.is_a?(String) && id.match?(/\A[0-9a-f]{40}\z/) }
|
|
82
|
-
{"version" => 1, "task_id" => manifest.fetch("task_id"), "scope" => manifest.fetch("scope"),
|
|
83
|
-
"prepared_head" => head, "prepared_tree" => tree, "manifest_bytes" => files.fetch("manifest.json").bytesize,
|
|
84
|
-
"manifest_sha256" => manifest_sha256, "selection_sha256" => selection_sha256}.freeze
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def definition_bytes(head:, tree:)
|
|
88
|
-
bytes = JSON.generate(definition.merge("prepared_work" => reference(head: head, tree: tree)))
|
|
89
|
-
invalid!("final definition size") if bytes.bytesize > MAX_MANIFEST
|
|
90
|
-
bytes.freeze
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Mutable queue progress still uses the accepted YAML grammar/bounds.
|
|
94
|
-
# Return the same parsed bytes that the queue decision will consume.
|
|
95
|
-
def parse_queue_step!(bytes)
|
|
96
|
-
invalid!("queue step size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
|
|
97
|
-
match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(bytes)
|
|
98
|
-
invalid!("queue step frontmatter") unless match
|
|
99
|
-
frontmatter = yaml!(match[1])
|
|
100
|
-
invalid!("queue step frontmatter") unless frontmatter.is_a?(Hash)
|
|
101
|
-
{frontmatter: frontmatter, body: bytes[match.end(0)..].strip}
|
|
102
|
-
rescue Psych::Exception => error
|
|
103
|
-
invalid!(error.message)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def parse_queue_metadata!(bytes)
|
|
107
|
-
invalid!("queue metadata size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_MANIFEST)
|
|
108
|
-
yaml!(bytes)
|
|
109
|
-
rescue Psych::Exception => error
|
|
110
|
-
invalid!(error.message)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# StepFileParser owns body/frontmatter semantics; only these progress
|
|
114
|
-
# fields differ from accepted work. Reports are separate mutable output.
|
|
115
|
-
def self.work_projection(bytes)
|
|
116
|
-
parsed = Atoms::StepFileParser.parse(bytes)
|
|
117
|
-
{"frontmatter" => parsed.fetch(:frontmatter).reject { |key, _| PROGRESS.include?(key) }, "body" => parsed.fetch(:body)}
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def self.canonical_manifest(value)
|
|
121
|
-
sorted = lambda do |item|
|
|
122
|
-
case item
|
|
123
|
-
when Hash then item.keys.sort.to_h { |key| [key, sorted.call(item.fetch(key))] }
|
|
124
|
-
when Array then item.map { |entry| sorted.call(entry) }
|
|
125
|
-
else item
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
JSON.generate(TOP.to_h { |key| [key, sorted.call(value.fetch(key))] })
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
def steps!
|
|
134
|
-
steps = @manifest["steps"]
|
|
135
|
-
invalid!("steps") unless steps.is_a?(Array) && steps.size.between?(1, 256)
|
|
136
|
-
@step_frontmatter = {}
|
|
137
|
-
numbers = steps.map do |step|
|
|
138
|
-
closed!(step, %w[number filename bytes sha256])
|
|
139
|
-
number = step.fetch("number"); scope!(number)
|
|
140
|
-
invalid!("step scope") unless number == @manifest["scope"] || number.start_with?(@manifest["scope"] + ".")
|
|
141
|
-
filename = step.fetch("filename")
|
|
142
|
-
record!(step.reject { |key, _| key == "number" }, "steps/" + filename)
|
|
143
|
-
# Step record filenames are basenames; inventory paths add steps/.
|
|
144
|
-
content = @files.fetch("steps/" + filename)
|
|
145
|
-
match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(content)
|
|
146
|
-
invalid!("step frontmatter") unless match
|
|
147
|
-
fm = yaml!(match[1])
|
|
148
|
-
invalid!("initial status") unless fm.is_a?(Hash) && fm.fetch("status", "pending") == "pending"
|
|
149
|
-
@step_frontmatter[number] = fm
|
|
150
|
-
invalid!("step name") unless fm["name"].is_a?(String) && !fm["name"].empty?
|
|
151
|
-
invalid!("step filename") unless filename == Atoms::StepFileParser.generate_filename(number, fm["name"])
|
|
152
|
-
number
|
|
153
|
-
end
|
|
154
|
-
invalid!("step ordering/root") unless numbers.uniq == numbers && numbers == numbers.sort_by { |n| n.split(".").map(&:to_i) } && numbers.first == @manifest["scope"]
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def context!
|
|
158
|
-
context = @manifest["context"]
|
|
159
|
-
invalid!("context") unless context.is_a?(Array) && context.size.between?(1, 256)
|
|
160
|
-
uris = context.map do |entry|
|
|
161
|
-
closed!(entry, %w[uri task_id spec text reports]); token!(entry["task_id"])
|
|
162
|
-
uri = "task://" + entry.fetch("task_id")
|
|
163
|
-
invalid!("context URI") unless entry["uri"] == uri
|
|
164
|
-
prefix = "context/" + entry.fetch("task_id")
|
|
165
|
-
record!(entry["spec"], prefix + "/spec.md")
|
|
166
|
-
record!(entry["text"], prefix + "/bundle.txt")
|
|
167
|
-
reports = entry["reports"]
|
|
168
|
-
invalid!("reports") unless reports.is_a?(Array)
|
|
169
|
-
identities = reports.map do |report|
|
|
170
|
-
closed!(report, %w[assignment_id number file]); token!(report["assignment_id"]); scope!(report["number"])
|
|
171
|
-
record!(report["file"], prefix + "/reports/" + report.fetch("assignment_id") + "/" + report.fetch("number") + ".r.md")
|
|
172
|
-
[report["assignment_id"], report["number"].split(".").map(&:to_i)]
|
|
173
|
-
end
|
|
174
|
-
invalid!("report ordering") unless identities.uniq == identities && identities == identities.sort
|
|
175
|
-
uri
|
|
176
|
-
end
|
|
177
|
-
invalid!("context ordering/selected task") unless uris.uniq == uris && uris == uris.sort && uris.include?("task://" + @manifest["task_id"])
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def selected_root!
|
|
181
|
-
scope = @manifest.fetch("scope"); task = @manifest.fetch("task_id")
|
|
182
|
-
root = @step_frontmatter.fetch(scope)
|
|
183
|
-
invalid!("selected fork root") unless root["context"] == "fork" && root["taskref"] == task
|
|
184
|
-
@step_frontmatter.each do |number, fm|
|
|
185
|
-
invalid!("selected taskref") if fm.key?("taskref") && fm["taskref"] != task
|
|
186
|
-
next if number == scope
|
|
187
|
-
parent = number.split(".")[0...-1].join(".")
|
|
188
|
-
invalid!("selected parent association") unless fm["parent"] == parent && @step_frontmatter.key?(parent)
|
|
189
|
-
end
|
|
190
|
-
invalid!("job steps") unless @job.is_a?(Hash) && @job["steps"].is_a?(Array)
|
|
191
|
-
roots = @job.fetch("steps").select { |step| step.is_a?(Hash) && step["context"] == "fork" && step["taskref"] == task }
|
|
192
|
-
invalid!("job selected root") unless roots.one? && roots.first.values_at("number", "taskref") == [scope, task]
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def task_closure!
|
|
196
|
-
specs = @manifest.fetch("context").to_h do |entry|
|
|
197
|
-
content = @files.fetch(entry.fetch("spec").fetch("filename"))
|
|
198
|
-
match = Atoms::StepFileParser::FRONTMATTER_REGEX.match(content)
|
|
199
|
-
invalid!("task frontmatter") unless match
|
|
200
|
-
task = yaml!(match[1])
|
|
201
|
-
invalid!("task association") unless task.is_a?(Hash) && task["id"] == entry["task_id"] && task.fetch("needs_review", false) == false
|
|
202
|
-
dependencies = task.fetch("dependencies", [])
|
|
203
|
-
invalid!("task dependencies") unless dependencies.is_a?(Array) && dependencies.uniq == dependencies
|
|
204
|
-
dependencies.each { |dependency| token!(dependency) }
|
|
205
|
-
[entry.fetch("task_id"), task]
|
|
206
|
-
end
|
|
207
|
-
selected = specs.fetch(@manifest.fetch("task_id"))
|
|
208
|
-
invalid!("selected reviewed status") unless %w[pending in-progress].include?(selected["status"])
|
|
209
|
-
visiting = []; visited = []
|
|
210
|
-
walk = lambda do |id|
|
|
211
|
-
invalid!("dependency cycle: " + (visiting + [id]).join(" -> ")) if visiting.include?(id)
|
|
212
|
-
return if visited.include?(id)
|
|
213
|
-
visiting << id
|
|
214
|
-
specs.fetch(id).fetch("dependencies", []).each { |dependency| walk.call(dependency) }
|
|
215
|
-
visiting.pop; visited << id
|
|
216
|
-
end
|
|
217
|
-
walk.call(@manifest.fetch("task_id"))
|
|
218
|
-
invalid!("undeclared context") unless visited.sort == specs.keys.sort
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def record!(record, path)
|
|
222
|
-
closed!(record, RECORD)
|
|
223
|
-
# Selected step record uses an exact basename; other records use the
|
|
224
|
-
# exact canonical relative artifact path.
|
|
225
|
-
expected = path.start_with?("steps/") ? path.delete_prefix("steps/") : path
|
|
226
|
-
invalid!("content reference") unless record["filename"] == expected && record["bytes"].is_a?(Integer) && record["bytes"].positive? && record["sha256"].is_a?(String) && record["sha256"].match?(SHA)
|
|
227
|
-
bytes = @files.fetch(path)
|
|
228
|
-
invalid!("content digest") unless record["bytes"] == bytes.bytesize && record["sha256"] == Digest::SHA256.hexdigest(bytes)
|
|
229
|
-
invalid!("duplicate file") if @used.include?(path)
|
|
230
|
-
@used << path
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def yaml!(bytes)
|
|
234
|
-
ast = Psych.parse_stream(bytes)
|
|
235
|
-
invalid!("YAML requires one document") unless ast.children.size == 1
|
|
236
|
-
scanner = Psych::ScalarScanner.new(Psych::ClassLoader::Restricted.new([], []))
|
|
237
|
-
pending = [[ast, 0]]
|
|
238
|
-
until pending.empty?
|
|
239
|
-
node, depth = pending.pop
|
|
240
|
-
invalid!("YAML nesting exceeds 32") if depth > 32
|
|
241
|
-
invalid!("YAML alias") if node.is_a?(Psych::Nodes::Alias)
|
|
242
|
-
if node.is_a?(Psych::Nodes::Mapping)
|
|
243
|
-
keys = node.children.each_slice(2).map(&:first)
|
|
244
|
-
decoded = keys.map do |key|
|
|
245
|
-
invalid!("YAML mapping keys") unless key.is_a?(Psych::Nodes::Scalar) && [nil, "tag:yaml.org,2002:str"].include?(key.tag)
|
|
246
|
-
value = key.plain && key.tag.nil? ? scanner.tokenize(key.value) : key.value
|
|
247
|
-
invalid!("YAML mapping keys must be strings") unless value.is_a?(String)
|
|
248
|
-
value
|
|
249
|
-
end
|
|
250
|
-
invalid!("duplicate YAML key") unless decoded.uniq.size == decoded.size
|
|
251
|
-
end
|
|
252
|
-
(node.children || []).each { |child| pending << [child, depth + 1] }
|
|
253
|
-
end
|
|
254
|
-
value = YAML.safe_load(bytes, permitted_classes: [Time, Date], aliases: false)
|
|
255
|
-
values = [value]
|
|
256
|
-
until values.empty?
|
|
257
|
-
item = values.pop
|
|
258
|
-
case item
|
|
259
|
-
when Hash
|
|
260
|
-
invalid!("YAML mapping keys must be strings") unless item.keys.all? { |key| key.is_a?(String) }
|
|
261
|
-
values.concat(item.values)
|
|
262
|
-
when Array then values.concat(item)
|
|
263
|
-
when String
|
|
264
|
-
invalid!("YAML string encoding") unless item.valid_encoding? && !item.include?("\0")
|
|
265
|
-
when Float then invalid!("YAML nonfinite number") unless item.finite?
|
|
266
|
-
when Integer, TrueClass, FalseClass, NilClass, Time, Date
|
|
267
|
-
# Managed files retain StepFileParser's standard timestamp types.
|
|
268
|
-
else invalid!("unsupported YAML value")
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
value
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def parse_json(bytes)
|
|
275
|
-
JSON.parse(bytes, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def closed!(value, keys)
|
|
279
|
-
invalid!("closed fields") unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def token!(value)
|
|
283
|
-
invalid!("identity") unless value.is_a?(String) && value.match?(TOKEN)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def scope!(value)
|
|
287
|
-
invalid!("scope") unless value.is_a?(String) && value.match?(SCOPE)
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def deep_freeze(value)
|
|
291
|
-
case value
|
|
292
|
-
when Hash then value.each { |key, item| deep_freeze(key); deep_freeze(item) }
|
|
293
|
-
when Array then value.each { |item| deep_freeze(item) }
|
|
294
|
-
end
|
|
295
|
-
value.freeze
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def invalid!(reason)
|
|
299
|
-
raise ArgumentError, "prepared_input_invalid: #{reason}"
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
end
|