ace-assign 0.71.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 -2119
- 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/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 -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,139 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# Compare retained Git objects in a private bare repository. Historical
|
|
7
|
-
# links are object bytes, never paths to materialize or follow. This view
|
|
8
|
-
# supplies evidence; it does not classify a change or grant acceptance.
|
|
9
|
-
# Host: CandidateTransfer supplies bounded Git/admission, byte encoding,
|
|
10
|
-
# private-root storage, monotonic deadlines and rejection helpers.
|
|
11
|
-
module CandidateHeadComparison
|
|
12
|
-
class ReadView
|
|
13
|
-
def initialize(owner:, repository:, reviewed:, candidate:, deadline:)
|
|
14
|
-
@owner, @repository, @reviewed, @candidate, @deadline = owner, repository, reviewed, candidate, deadline
|
|
15
|
-
@thread, @active = Thread.current, true
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def close = @active = false
|
|
19
|
-
|
|
20
|
-
def context
|
|
21
|
-
unless @active && Thread.current.equal?(@thread)
|
|
22
|
-
raise AttemptErrors::EvidenceUnavailable, "head comparison escaped its source lifetime"
|
|
23
|
-
end
|
|
24
|
-
@owner.send(:head_comparison_context, @repository, @deadline, @reviewed, @candidate)
|
|
25
|
-
rescue Timeout::Error
|
|
26
|
-
raise AttemptErrors::EvidenceUnavailable, "head comparison deadline expired"
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def with_head_comparison(before:, after:, before_base:, after_base:)
|
|
31
|
-
raise ArgumentError, "head comparison requires a block" unless block_given?
|
|
32
|
-
[before, after].each do |input|
|
|
33
|
-
unless input.is_a?(Hash) && input.keys.sort == %i[bytes head sha256 size tree] &&
|
|
34
|
-
input[:tree].is_a?(String) && input[:tree].match?(self.class::SHA)
|
|
35
|
-
reject!("Head comparison candidate binding is invalid")
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
[before_base, after_base].each do |base|
|
|
39
|
-
reject!("Head comparison base is invalid") unless base.is_a?(String) && base.match?(self.class::SHA)
|
|
40
|
-
end
|
|
41
|
-
deadline = monotonic + self.class::DEADLINE
|
|
42
|
-
# Each complete bundle is independently verified against an empty
|
|
43
|
-
# repository before either can contribute objects to the comparison.
|
|
44
|
-
admit(**before.reject { |key, _| key == :tree }, deadline: deadline) do |_old_repo, _old_deadline, reviewed|
|
|
45
|
-
reject!("Reviewed tree differs from accepted tree") unless reviewed.fetch("tree") == before[:tree]
|
|
46
|
-
admit(**after.reject { |key, _| key == :tree }, deadline: deadline) do |repository, _new_deadline, candidate|
|
|
47
|
-
reject!("Candidate tree differs from accepted tree") unless candidate.fetch("tree") == after[:tree]
|
|
48
|
-
Tempfile.create(["reviewed-", ".bundle"], @root) do |file|
|
|
49
|
-
file.binmode
|
|
50
|
-
file.write(reviewed.fetch("bundle")); file.flush
|
|
51
|
-
git(repository, deadline, "fetch", "--no-tags", file.path, "#{before[:head]}:refs/heads/reviewed")
|
|
52
|
-
reader = ReadView.new(owner: self, repository: repository,
|
|
53
|
-
reviewed: {"head" => before[:head], "base" => before_base, "tree" => before[:tree]},
|
|
54
|
-
candidate: {"head" => after[:head], "base" => after_base, "tree" => after[:tree]}, deadline: deadline)
|
|
55
|
-
yield reader
|
|
56
|
-
ensure
|
|
57
|
-
reader&.close
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def head_comparison_context(repository, deadline, reviewed, candidate)
|
|
66
|
-
context = {"schema" => "ace.candidate-head-comparison/v1", "reviewed" => reviewed,
|
|
67
|
-
"candidate" => candidate, "inventories" => {}, "objects" => {}, "changes" => {}}
|
|
68
|
-
remaining = self.class::MAX_REVIEW_BYTES
|
|
69
|
-
{"reviewed_head" => reviewed["head"], "reviewed_base" => reviewed["base"],
|
|
70
|
-
"candidate_head" => candidate["head"], "candidate_base" => candidate["base"]}.each do |label, commit|
|
|
71
|
-
git(repository, deadline, "cat-file", "-e", "#{commit}^{commit}")
|
|
72
|
-
tree = git(repository, deadline, "rev-parse", "#{commit}^{tree}").strip
|
|
73
|
-
inventory = head_comparison_inventory(repository, deadline, tree)
|
|
74
|
-
remaining -= JSON.generate(inventory).bytesize
|
|
75
|
-
reject!("Complete head comparison context is oversized") if remaining.negative?
|
|
76
|
-
context.fetch("inventories")[label] = {"tree" => tree, "entries" => inventory}
|
|
77
|
-
end
|
|
78
|
-
oids = context.fetch("inventories").values.flat_map { |inventory| inventory.fetch("entries").map { |entry| entry.fetch("oid") } }.uniq
|
|
79
|
-
sizes = oids.zip(candidate_blob_sizes(repository, deadline, oids)).to_h
|
|
80
|
-
reject!("Complete head comparison object content is oversized") if sizes.values.sum > remaining
|
|
81
|
-
each_candidate_blob(repository, deadline, sizes.to_a) do |oid, content|
|
|
82
|
-
object = {"size" => content.bytesize, "sha256" => Digest::SHA256.hexdigest(content), "content" => review_bytes(content)}
|
|
83
|
-
remaining -= JSON.generate(object).bytesize
|
|
84
|
-
reject!("Complete head comparison context is oversized") if remaining.negative?
|
|
85
|
-
context.fetch("objects")[oid] = object
|
|
86
|
-
end
|
|
87
|
-
{"candidate_delta" => [reviewed["head"], candidate["head"]],
|
|
88
|
-
"base_delta" => [reviewed["base"], candidate["base"]],
|
|
89
|
-
"original_scope" => [reviewed["base"], reviewed["head"]],
|
|
90
|
-
"current_scope" => [candidate["base"], candidate["head"]]}.each do |label, revisions|
|
|
91
|
-
delta = head_comparison_delta(repository, deadline, revisions, remaining)
|
|
92
|
-
remaining -= JSON.generate(delta).bytesize
|
|
93
|
-
reject!("Complete head comparison context is oversized") if remaining.negative?
|
|
94
|
-
context.fetch("changes")[label] = delta
|
|
95
|
-
end
|
|
96
|
-
# Include structural JSON overhead in the final bound as well as all
|
|
97
|
-
# original bytes. No partial inventory or truncated patch is returned.
|
|
98
|
-
reject!("Complete head comparison context is oversized") if JSON.generate(context).bytesize > self.class::MAX_REVIEW_BYTES
|
|
99
|
-
context
|
|
100
|
-
rescue AttemptErrors::ReceiptRejected => error
|
|
101
|
-
reject!("Head comparison read failed: #{error.message}")
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def head_comparison_inventory(repository, deadline, tree)
|
|
105
|
-
entries = git(repository, deadline, "ls-tree", "-r", "-z", tree, limit: self.class::MAX_BYTES).b.split("\0")
|
|
106
|
-
reject!("Complete head comparison inventory is oversized") if entries.size > self.class::MAX_OBJECTS
|
|
107
|
-
entries.map do |entry|
|
|
108
|
-
metadata, path = entry.split("\t", 2)
|
|
109
|
-
mode, kind, oid = metadata.to_s.split(" ")
|
|
110
|
-
# Inventory entries carry whole paths only; bound their bytes
|
|
111
|
-
# before JSON encoding or delta cross-checks consume them.
|
|
112
|
-
reject!("Complete head comparison path exceeds bounded length") unless path && !path.empty? &&
|
|
113
|
-
path.bytesize <= Atoms::CandidateLimits::PATH_BYTES
|
|
114
|
-
unless kind == "blob" && %w[100644 100755 120000].include?(mode) && oid&.match?(self.class::SHA)
|
|
115
|
-
reject!("Complete head comparison contains an unsupported Git entry")
|
|
116
|
-
end
|
|
117
|
-
{"path" => review_bytes(path), "mode" => mode, "oid" => oid}
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def head_comparison_delta(repository, deadline, revisions, remaining)
|
|
122
|
-
options = ["--no-ext-diff", "--no-textconv", "--no-renames", "--ignore-submodules=none"]
|
|
123
|
-
patch = git(repository, deadline, "diff", *options, "--binary", "--full-index", "--src-prefix=a/", "--dst-prefix=b/",
|
|
124
|
-
*revisions, "--", limit: [remaining, 1].max).b
|
|
125
|
-
paths = git(repository, deadline, "diff", *options, "--name-only", "-z", *revisions, "--", limit: [remaining, 1].max).b.split("\0")
|
|
126
|
-
stats = git(repository, deadline, "diff", *options, "--numstat", "-z", *revisions, "--", limit: [remaining, 1].max).b.split("\0")
|
|
127
|
-
binary = stats.filter_map do |entry|
|
|
128
|
-
added, removed, path = entry.split("\t", 3)
|
|
129
|
-
reject!("Complete head comparison diff inventory is invalid") unless path && paths.include?(path)
|
|
130
|
-
path if added == "-" || removed == "-"
|
|
131
|
-
end
|
|
132
|
-
{"from" => revisions.first, "to" => revisions.last, "patch" => review_bytes(patch),
|
|
133
|
-
"patch_sha256" => Digest::SHA256.hexdigest(patch), "paths" => paths.map { |path| review_bytes(path) },
|
|
134
|
-
"binary_paths" => binary.map { |path| review_bytes(path) }}
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "json"
|
|
5
|
-
require "fileutils"
|
|
6
|
-
require "tmpdir"
|
|
7
|
-
require "tempfile"
|
|
8
|
-
require "ace/herdr/molecules/bounded_process"
|
|
9
|
-
require "ace/review/atoms/campaign_contract"
|
|
10
|
-
require_relative "private_directory"
|
|
11
|
-
require_relative "../atoms/candidate_limits"
|
|
12
|
-
require_relative "candidate_head_comparison"
|
|
13
|
-
require_relative "candidate_blob_batch"
|
|
14
|
-
|
|
15
|
-
module Ace
|
|
16
|
-
module Assign
|
|
17
|
-
module Authority
|
|
18
|
-
# Untrusted bundles enter a clean bare quarantine. Only complete verified
|
|
19
|
-
# objects are retained; no uploader configuration or worktree is read.
|
|
20
|
-
class CandidateTransfer
|
|
21
|
-
include CandidateHeadComparison
|
|
22
|
-
include CandidateBlobBatch
|
|
23
|
-
MAX_BYTES = Atoms::CandidateLimits::BYTES
|
|
24
|
-
MAX_REVIEW_BYTES = Ace::Review::Atoms::CampaignContract::MAX_HEAD_CONTEXT_BYTES
|
|
25
|
-
# Measured on the real 82,809,675-byte candidate (9,203 entries,
|
|
26
|
-
# 71,583,200 expanded bytes): two full admits alone need ~14s of
|
|
27
|
-
# bounded Git work and the complete comparison ~23s on a loaded host
|
|
28
|
-
# (evidence in .ace-tasks/8xf.t.ei9-wave-3 evidence). 30s failed
|
|
29
|
-
# closed under legitimate load; 60s keeps a finite secondary guard
|
|
30
|
-
# with measured headroom. Inputs stay byte/count-bounded separately.
|
|
31
|
-
DEADLINE = 60
|
|
32
|
-
SHA = /\A[0-9a-f]{40}\z/
|
|
33
|
-
|
|
34
|
-
class CampaignReadView
|
|
35
|
-
def initialize(owner:, repository:, base:, deadline:)
|
|
36
|
-
@owner, @repository, @base, @deadline = owner, repository, base, deadline
|
|
37
|
-
@thread, @active = Thread.current, true
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def close = @active = false
|
|
41
|
-
def head = command("rev-parse", "HEAD").strip
|
|
42
|
-
def base
|
|
43
|
-
commit!(@base)
|
|
44
|
-
@base
|
|
45
|
-
end
|
|
46
|
-
def commit!(revision)
|
|
47
|
-
raise ArgumentError, "campaign revision is invalid" unless revision.is_a?(String) && revision.match?(SHA)
|
|
48
|
-
command("cat-file", "-e", "#{revision}^{commit}")
|
|
49
|
-
true
|
|
50
|
-
end
|
|
51
|
-
def full_diff?(base, head)
|
|
52
|
-
commit!(base)
|
|
53
|
-
commit!(head)
|
|
54
|
-
!command("diff", "--raw", "--no-ext-diff", "--no-textconv", base, head, "--").empty?
|
|
55
|
-
end
|
|
56
|
-
def clean?
|
|
57
|
-
command("status", "--porcelain", "-z", "--untracked-files=all").split("\0")
|
|
58
|
-
.all? { |entry| entry.start_with?("?? .ace-local/") }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def command(*arguments)
|
|
64
|
-
unless @active && Thread.current.equal?(@thread)
|
|
65
|
-
raise AttemptErrors::EvidenceUnavailable, "campaign Git read view escaped its source lifetime"
|
|
66
|
-
end
|
|
67
|
-
@owner.send(:git, @repository, @deadline, *arguments)
|
|
68
|
-
rescue Timeout::Error
|
|
69
|
-
raise AttemptErrors::EvidenceUnavailable, "campaign Git read deadline expired"
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def with_campaign_repository(bytes:, sha256:, size:, head:, tree:, base:)
|
|
74
|
-
raise ArgumentError, "campaign repository requires a block" unless block_given?
|
|
75
|
-
deadline = monotonic + DEADLINE
|
|
76
|
-
materialized = materialize_repository(bytes: bytes, sha256: sha256, size: size, head: head, tree: tree, root: @root)
|
|
77
|
-
directory = materialized.fetch("directory")
|
|
78
|
-
reader = CampaignReadView.new(owner: self, repository: directory, base: base, deadline: deadline)
|
|
79
|
-
unless reader.head == head
|
|
80
|
-
raise AttemptErrors::EvidenceUnavailable, "campaign Git head differs from original candidate"
|
|
81
|
-
end
|
|
82
|
-
reader.base
|
|
83
|
-
yield reader
|
|
84
|
-
ensure
|
|
85
|
-
reader&.close
|
|
86
|
-
FileUtils.remove_entry(directory) if directory && File.directory?(directory)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def initialize(root:)
|
|
90
|
-
@root = File.expand_path(root)
|
|
91
|
-
verify_root!
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Export the already validated prepared tree using the same isolated Git
|
|
95
|
-
# boundary as admission. The returned bytes are retained by the producer;
|
|
96
|
-
# registration must not rebuild them after a lost response.
|
|
97
|
-
def export_prepared(work:)
|
|
98
|
-
require_relative "prepared_work"
|
|
99
|
-
reject!("Prepared export requires validated work") unless work.is_a?(PreparedWork)
|
|
100
|
-
verify_root!
|
|
101
|
-
deadline = monotonic + DEADLINE
|
|
102
|
-
Dir.mktmpdir("prepared-export-", @root) do |directory|
|
|
103
|
-
File.chmod(0700, directory)
|
|
104
|
-
repository = File.join(directory, "repository")
|
|
105
|
-
git(directory, deadline, "init", "--template=", "--initial-branch=prepared", repository)
|
|
106
|
-
work.files.each do |path, bytes|
|
|
107
|
-
target = File.join(repository, path)
|
|
108
|
-
FileUtils.mkdir_p(File.dirname(target), mode: 0700)
|
|
109
|
-
File.open(target, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) do |file|
|
|
110
|
-
file.write(bytes)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
git(repository, deadline, "add", "--", ".")
|
|
114
|
-
git(repository, deadline, "-c", "user.name=ACE prepared input", "-c", "user.email=prepared@ace.invalid",
|
|
115
|
-
"-c", "commit.gpgSign=false", "commit", "--no-verify", "-m", "Prepared assignment input")
|
|
116
|
-
head = git(repository, deadline, "rev-parse", "HEAD").strip
|
|
117
|
-
tree = git(repository, deadline, "rev-parse", "HEAD^{tree}").strip
|
|
118
|
-
bundle = File.join(directory, "prepared.bundle")
|
|
119
|
-
git(repository, deadline, "bundle", "create", bundle, "refs/heads/prepared")
|
|
120
|
-
bytes = read_bundle(bundle).freeze
|
|
121
|
-
{"head" => head.freeze, "tree" => tree.freeze, "bundle" => bytes,
|
|
122
|
-
"bytes" => bytes.bytesize, "sha256" => Digest::SHA256.hexdigest(bytes).freeze,
|
|
123
|
-
"definition_bytes" => work.definition_bytes(head: head, tree: tree)}.freeze
|
|
124
|
-
end
|
|
125
|
-
rescue Timeout::Error
|
|
126
|
-
reject!("Prepared export deadline exceeded")
|
|
127
|
-
rescue SystemCallError
|
|
128
|
-
reject!("Prepared export filesystem is unavailable")
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def admit(bytes:, sha256:, size:, head:, deadline: nil, &consumer)
|
|
132
|
-
unless bytes.is_a?(String) && size.is_a?(Integer) && size.positive? && size <= MAX_BYTES &&
|
|
133
|
-
size == bytes.bytesize && sha256.is_a?(String) &&
|
|
134
|
-
Digest::SHA256.hexdigest(bytes.b) == sha256 && head.is_a?(String) && head.match?(SHA)
|
|
135
|
-
reject!("Candidate bundle binding or size is invalid")
|
|
136
|
-
end
|
|
137
|
-
verify_root!
|
|
138
|
-
deadline = deadline ? [deadline, monotonic + DEADLINE].min : monotonic + DEADLINE
|
|
139
|
-
Dir.mktmpdir("quarantine-", @root) do |directory|
|
|
140
|
-
File.chmod(0700, directory)
|
|
141
|
-
bundle = File.join(directory, "candidate.bundle")
|
|
142
|
-
File.open(bundle, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) { |f| f.write(bytes.b) }
|
|
143
|
-
repository = File.join(directory, "repository")
|
|
144
|
-
git(directory, deadline, "init", "--bare", "--template=", repository)
|
|
145
|
-
# Verification against an empty repository rejects incremental
|
|
146
|
-
# bundles requiring any caller-supplied prerequisites.
|
|
147
|
-
git(repository, deadline, "bundle", "verify", bundle)
|
|
148
|
-
advertised = git(repository, deadline, "bundle", "list-heads", bundle).lines.map { |line| line.split.first }
|
|
149
|
-
reject!("Candidate head is not advertised") unless advertised.include?(head)
|
|
150
|
-
git(repository, deadline, "fetch", "--no-tags", bundle, "#{head}:refs/heads/candidate")
|
|
151
|
-
actual = git(repository, deadline, "rev-parse", "refs/heads/candidate^{commit}").strip
|
|
152
|
-
reject!("Candidate commit differs from declared head") unless actual == head
|
|
153
|
-
git(repository, deadline, "fsck", "--full", "--strict", "--no-reflogs")
|
|
154
|
-
tree = git(repository, deadline, "rev-parse", "#{head}^{tree}").strip
|
|
155
|
-
exported = File.join(directory, "verified.bundle")
|
|
156
|
-
git(repository, deadline, "bundle", "create", exported, "refs/heads/candidate")
|
|
157
|
-
verified = read_bundle(exported)
|
|
158
|
-
# The journal owns candidate identity/generation. This object is
|
|
159
|
-
# only immutable transport bytes, not another authority ledger.
|
|
160
|
-
result = {"head" => head, "tree" => tree, "bundle" => verified,
|
|
161
|
-
"sha256" => Digest::SHA256.hexdigest(verified), "bytes" => verified.bytesize}
|
|
162
|
-
consumer ? consumer.call(repository, deadline, result) : result
|
|
163
|
-
end
|
|
164
|
-
rescue Timeout::Error
|
|
165
|
-
reject!("Candidate transfer deadline exceeded")
|
|
166
|
-
rescue SystemCallError
|
|
167
|
-
reject!("Candidate transfer filesystem is unavailable")
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# root is selected from installed peer credentials by the receiver,
|
|
171
|
-
# never a request field. Materialization reads objects rather than Git
|
|
172
|
-
# checkout, so .gitattributes, filters and uploader hooks cannot run.
|
|
173
|
-
def materialize(bytes:, sha256:, size:, head:, tree:, root:)
|
|
174
|
-
self.class.new(root: root)
|
|
175
|
-
admit(bytes: bytes, sha256: sha256, size: size, head: head) do |repository, deadline, result|
|
|
176
|
-
reject!("Candidate tree differs from accepted tree") unless result["tree"] == tree
|
|
177
|
-
destination = Dir.mktmpdir("candidate-", root)
|
|
178
|
-
File.chmod(0700, destination)
|
|
179
|
-
begin
|
|
180
|
-
entries = git(repository, deadline, "ls-tree", "-r", "-z", head, limit: MAX_BYTES).b.split("\0")
|
|
181
|
-
reject!("Candidate file inventory is oversized") if entries.size > MAX_OBJECTS
|
|
182
|
-
parsed = entries.map do |entry|
|
|
183
|
-
metadata, path = entry.split("\t", 2)
|
|
184
|
-
mode, kind, oid = metadata.to_s.split(" ")
|
|
185
|
-
# Total path bytes are bounded before any component or
|
|
186
|
-
# filesystem prefix work consumes them.
|
|
187
|
-
reject!("Candidate path exceeds bounded length") unless path && !path.start_with?("/") &&
|
|
188
|
-
path.bytesize <= Atoms::CandidateLimits::PATH_BYTES
|
|
189
|
-
parts = path.split("/", -1)
|
|
190
|
-
unless parts.none? { |part| part.empty? || part.bytesize > Atoms::CandidateLimits::PATH_COMPONENT_BYTES ||
|
|
191
|
-
%w[. .. .git].include?(part.downcase) } &&
|
|
192
|
-
kind == "blob" && %w[100644 100755 120000].include?(mode) && oid&.match?(SHA)
|
|
193
|
-
reject!("Candidate contains unsafe filesystem entry")
|
|
194
|
-
end
|
|
195
|
-
[path, mode, oid]
|
|
196
|
-
end
|
|
197
|
-
by_oid = parsed.group_by(&:last)
|
|
198
|
-
sizes = by_oid.keys.zip(candidate_blob_sizes(repository, deadline, by_oid.keys)).to_h
|
|
199
|
-
reject!("Candidate expanded content is oversized") if parsed.sum { |_path, _mode, oid| sizes.fetch(oid) } > MAX_BYTES
|
|
200
|
-
links = []
|
|
201
|
-
ordered = parsed.each
|
|
202
|
-
each_candidate_blob(repository, deadline, parsed.map { |_path, _mode, oid| [oid, sizes.fetch(oid)] }) do |oid, content|
|
|
203
|
-
path, mode, _oid = ordered.next
|
|
204
|
-
target = File.join(destination, path)
|
|
205
|
-
FileUtils.mkdir_p(File.dirname(target), mode: 0700)
|
|
206
|
-
if mode == "120000"
|
|
207
|
-
resolved = File.expand_path(content, File.dirname(target))
|
|
208
|
-
unless !content.include?("\0") && !content.start_with?("/") &&
|
|
209
|
-
resolved.start_with?(destination + "/")
|
|
210
|
-
reject!("Candidate symlink escapes private checkout")
|
|
211
|
-
end
|
|
212
|
-
links << [target, content]
|
|
213
|
-
else
|
|
214
|
-
File.open(target, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW,
|
|
215
|
-
mode == "100755" ? 0700 : 0600) { |file| file.write(content) }
|
|
216
|
-
end
|
|
217
|
-
yield(path.freeze, mode.freeze, oid.freeze, content.freeze) if block_given?
|
|
218
|
-
end
|
|
219
|
-
links.each { |target, content| File.symlink(content, target) }
|
|
220
|
-
links.each do |target, _content|
|
|
221
|
-
resolved = File.realpath(target)
|
|
222
|
-
reject!("Candidate symlink escapes private checkout") unless resolved.start_with?(destination + "/")
|
|
223
|
-
end
|
|
224
|
-
result.reject { |key, _value| key == "bundle" }.merge("directory" => destination)
|
|
225
|
-
rescue Exception
|
|
226
|
-
# Only this freshly created private disposable directory is
|
|
227
|
-
# removed. An existing peer scratch tree is never cleaned.
|
|
228
|
-
FileUtils.rm_rf(destination)
|
|
229
|
-
raise
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
rescue SystemCallError, ArgumentError
|
|
233
|
-
reject!("Candidate filesystem materialization is unverifiable")
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# Executing campaign children need local Git history as well as files.
|
|
237
|
-
# Create only our own empty administration; no uploader configuration,
|
|
238
|
-
# checkout filters or hooks enter this private repository.
|
|
239
|
-
def materialize_repository(bytes:, sha256:, size:, head:, tree:, root:)
|
|
240
|
-
deadline = monotonic + DEADLINE
|
|
241
|
-
result = materialize(bytes: bytes, sha256: sha256, size: size, head: head, tree: tree, root: root)
|
|
242
|
-
directory = result.fetch("directory")
|
|
243
|
-
Tempfile.create(["campaign-candidate-", ".bundle"], root) do |file|
|
|
244
|
-
file.binmode
|
|
245
|
-
file.write(bytes); file.flush
|
|
246
|
-
git(directory, deadline, "init", "--template=", "--initial-branch=work")
|
|
247
|
-
git(directory, deadline, "fetch", "--no-tags", file.path, "#{head}:refs/heads/candidate")
|
|
248
|
-
git(directory, deadline, "symbolic-ref", "HEAD", "refs/heads/candidate")
|
|
249
|
-
git(directory, deadline, "read-tree", head)
|
|
250
|
-
reject!("Campaign private repository differs") unless git(directory, deadline, "rev-parse", "HEAD^{tree}").strip == tree
|
|
251
|
-
end
|
|
252
|
-
result
|
|
253
|
-
rescue Exception
|
|
254
|
-
FileUtils.remove_entry(directory) if directory && File.directory?(directory) && !File.symlink?(directory)
|
|
255
|
-
raise
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Full immutable review input comes from the same admitted Git objects
|
|
259
|
-
# used for materialization, never a second read of the scratch checkout.
|
|
260
|
-
def review_snapshot(bytes:, sha256:, size:, head:, tree:, root:)
|
|
261
|
-
subject = JSON.generate("schema" => "ace.candidate-review-snapshot/v1", "head" => head, "tree" => tree)
|
|
262
|
-
.delete_suffix("}") + ',"files":['
|
|
263
|
-
first = true
|
|
264
|
-
result = materialize(bytes: bytes, sha256: sha256, size: size, head: head, tree: tree, root: root) do |path, mode, oid, content|
|
|
265
|
-
entry = JSON.generate("path" => review_bytes(path), "mode" => mode, "oid" => oid, "content" => review_bytes(content))
|
|
266
|
-
separator = first ? "" : ","
|
|
267
|
-
reject!("Complete candidate review snapshot is oversized") if subject.bytesize + separator.bytesize + entry.bytesize + 2 > Atoms::CandidateLimits::REVIEW_SNAPSHOT_BYTES
|
|
268
|
-
subject << separator << entry
|
|
269
|
-
first = false
|
|
270
|
-
end
|
|
271
|
-
subject << "]}"
|
|
272
|
-
result.merge("subject" => subject.freeze)
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# Prepared instructions have a narrower tree contract than candidates:
|
|
276
|
-
# no executable entries or symlinks, and bounded UTF-8 files. Read only
|
|
277
|
-
# verified Git objects; never run checkout filters or reopen uploader paths.
|
|
278
|
-
def prepared_files(repository:, deadline:, head:)
|
|
279
|
-
entries = git(repository, deadline, "ls-tree", "-r", "-z", head, limit: MAX_BYTES).b.split("\0")
|
|
280
|
-
reject!("Prepared file inventory is oversized") unless entries.size.between?(4, 4096)
|
|
281
|
-
parsed = entries.map do |entry|
|
|
282
|
-
metadata, path = entry.split("\t", 2)
|
|
283
|
-
mode, kind, oid = metadata.to_s.split(" ")
|
|
284
|
-
reject!("Prepared tree path exceeds bounded length") unless path && !path.start_with?("/") &&
|
|
285
|
-
path.bytesize <= Atoms::CandidateLimits::PATH_BYTES
|
|
286
|
-
unless mode == "100644" && kind == "blob" && oid&.match?(SHA) &&
|
|
287
|
-
path.split("/", -1).none? { |part| part.empty? || part.bytesize > Atoms::CandidateLimits::PATH_COMPONENT_BYTES ||
|
|
288
|
-
%w[. .. .git].include?(part.downcase) }
|
|
289
|
-
reject!("Prepared tree contains unsafe entry")
|
|
290
|
-
end
|
|
291
|
-
[path, oid]
|
|
292
|
-
end
|
|
293
|
-
by_oid = parsed.group_by(&:last)
|
|
294
|
-
sizes = by_oid.keys.zip(candidate_blob_sizes(repository, deadline, by_oid.keys)).to_h
|
|
295
|
-
reject!("Prepared text is oversized") unless sizes.values.all? { |size| size.between?(1, 1024 * 1024) }
|
|
296
|
-
reject!("Prepared expanded content is oversized") if parsed.sum { |_path, oid| sizes.fetch(oid) } > Atoms::CandidateLimits::PREPARED_BYTES
|
|
297
|
-
files = {}
|
|
298
|
-
ordered = parsed.each
|
|
299
|
-
each_candidate_blob(repository, deadline, parsed.map { |_path, oid| [oid, sizes.fetch(oid)] }) do |_oid, content|
|
|
300
|
-
files[ordered.next.first] = content
|
|
301
|
-
end
|
|
302
|
-
reject!("Prepared inventory has duplicate paths") unless files.size == entries.size
|
|
303
|
-
files
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
private
|
|
307
|
-
|
|
308
|
-
def review_bytes(bytes)
|
|
309
|
-
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
310
|
-
if text.valid_encoding? && !text.include?("\0")
|
|
311
|
-
{"encoding" => "utf-8", "bytes" => text}
|
|
312
|
-
else
|
|
313
|
-
{"encoding" => "base64", "bytes" => [bytes].pack("m0")}
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def verify_root!
|
|
318
|
-
PrivateDirectory.verify!(@root)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def git(repository, deadline, *arguments, limit: 65_536, stdin_data: "")
|
|
322
|
-
remaining = deadline - monotonic
|
|
323
|
-
raise Timeout::Error if remaining <= 0
|
|
324
|
-
# Git variables may redirect object/config storage even in a clean
|
|
325
|
-
# repository. Strip every inherited Git setting and global config.
|
|
326
|
-
environment = ENV.keys.to_h { |key| [key, nil] }
|
|
327
|
-
environment.merge!("GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_GLOBAL" => "/dev/null",
|
|
328
|
-
"GIT_ATTR_NOSYSTEM" => "1",
|
|
329
|
-
"GIT_TERMINAL_PROMPT" => "0", "GIT_ALLOW_PROTOCOL" => "file",
|
|
330
|
-
"GIT_NO_REPLACE_OBJECTS" => "1", "PATH" => "/usr/bin:/bin", "LC_ALL" => "C")
|
|
331
|
-
result = Herdr::Molecules::BoundedProcess.call([environment, "/usr/bin/git", "-C", repository,
|
|
332
|
-
"-c", "core.hooksPath=/dev/null", "-c", "core.attributesFile=/dev/null", "-c", "protocol.file.allow=always", *arguments],
|
|
333
|
-
stdin_data: stdin_data, timeout_s: remaining, output_limit: limit)
|
|
334
|
-
reject!("Candidate Git verification failed") unless result.status.success? && !result.oversized
|
|
335
|
-
result.stdout
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def read_bundle(path)
|
|
339
|
-
File.open(path, File::RDONLY | File::NOFOLLOW) do |file|
|
|
340
|
-
reject!("Verified bundle is oversized") unless file.stat.file? && file.stat.size <= MAX_BYTES
|
|
341
|
-
bytes = file.read(MAX_BYTES + 1).b
|
|
342
|
-
reject!("Verified bundle is oversized") if bytes.bytesize > MAX_BYTES
|
|
343
|
-
bytes
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def monotonic
|
|
348
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def reject!(message)
|
|
352
|
-
raise AttemptErrors::ReceiptRejected, message
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fiddle"
|
|
4
|
-
require "ace/runtime/molecules/protected_artifact_set"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# Extends the existing held artifact protection solely for the bounded
|
|
10
|
-
# root result view. Private archive storage is never admitted here.
|
|
11
|
-
class CleanupResultProtection < Ace::Runtime::Molecules::ProtectedArtifactSet::Protection
|
|
12
|
-
class LinuxACL
|
|
13
|
-
def absent!(handle, attribute)
|
|
14
|
-
unless RUBY_PLATFORM.include?("linux")
|
|
15
|
-
raise Ace::Runtime::RuntimeUnavailableError, "cleanup result ACL evidence requires Linux"
|
|
16
|
-
end
|
|
17
|
-
function = Fiddle::Function.new(Fiddle::Handle::DEFAULT["fgetxattr"],
|
|
18
|
-
[Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T], Fiddle::TYPE_SSIZE_T)
|
|
19
|
-
size = function.call(handle.fileno, attribute, 0, 0)
|
|
20
|
-
error = Fiddle.last_error
|
|
21
|
-
unless size == -1 && error == Errno::ENODATA::Errno
|
|
22
|
-
raise Ace::Runtime::RuntimeUnavailableError, "cleanup result ACL protection is unavailable"
|
|
23
|
-
end
|
|
24
|
-
rescue Fiddle::DLError
|
|
25
|
-
raise Ace::Runtime::RuntimeUnavailableError, "cleanup result ACL reader is unavailable"
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def initialize(result_path:, authority_gid:, acl: LinuxACL.new, **options)
|
|
30
|
-
unless result_path.is_a?(String) && result_path.start_with?("/") && !result_path.include?("\0") &&
|
|
31
|
-
File.expand_path(result_path) == result_path && authority_gid.is_a?(Integer) && authority_gid.positive?
|
|
32
|
-
raise ArgumentError, "invalid fixed cleanup result view"
|
|
33
|
-
end
|
|
34
|
-
super(**options)
|
|
35
|
-
@result_path, @parent, @authority_gid, @acl = result_path.dup.freeze, File.dirname(result_path).freeze, authority_gid, acl
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def verify!(path, handle, directory:)
|
|
39
|
-
super
|
|
40
|
-
return unless path == @result_path || path == @parent
|
|
41
|
-
parent = path == @parent
|
|
42
|
-
stat = handle.stat
|
|
43
|
-
unless directory == parent && stat.gid == @authority_gid &&
|
|
44
|
-
(stat.mode & 0o7777) == (parent ? 0o2750 : 0o640)
|
|
45
|
-
raise Ace::Runtime::RuntimeUnavailableError, "cleanup result view mode or group differs"
|
|
46
|
-
end
|
|
47
|
-
@acl.absent!(handle, "system.posix_acl_access")
|
|
48
|
-
@acl.absent!(handle, "system.posix_acl_default") if parent
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|