ace-assign 0.63.1 → 0.66.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 +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +398 -0
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +59 -0
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +286 -0
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +187 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +38 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +56 -4
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "ace/herdr/molecules/bounded_process"
|
|
7
|
+
require_relative "evidence_journal"
|
|
8
|
+
require_relative "../authority/private_directory"
|
|
9
|
+
|
|
10
|
+
module Ace
|
|
11
|
+
module Assign
|
|
12
|
+
module Molecules
|
|
13
|
+
# Isolates read-only Git from an installed authority's live configuration.
|
|
14
|
+
# The copied view never publishes evidence or mutates the source journal.
|
|
15
|
+
class CanonicalReadSnapshot
|
|
16
|
+
class Unavailable < AttemptErrors::EvidenceUnavailable; end
|
|
17
|
+
BYTE_LIMIT = 256 * 1_048_576
|
|
18
|
+
ENTRY_LIMIT = 4096
|
|
19
|
+
OID = /\A[0-9a-f]{40}\z/
|
|
20
|
+
ENVIRONMENT = {"PATH" => "/usr/bin:/bin", "LC_ALL" => "C",
|
|
21
|
+
"GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_SYSTEM" => "/dev/null",
|
|
22
|
+
"GIT_CONFIG_GLOBAL" => "/dev/null", "GIT_TERMINAL_PROMPT" => "0",
|
|
23
|
+
"GIT_ALLOW_PROTOCOL" => "", "GIT_NO_REPLACE_OBJECTS" => "1",
|
|
24
|
+
"GIT_NO_LAZY_FETCH" => "1", "GIT_OPTIONAL_LOCKS" => "0"}.freeze
|
|
25
|
+
|
|
26
|
+
# These selections come from installed source composition, not frames.
|
|
27
|
+
def initialize(repo_root:, checkout_root:, ref:, private_root:, git_executable: "/usr/bin/git")
|
|
28
|
+
paths = [repo_root, checkout_root, private_root, git_executable]
|
|
29
|
+
unless paths.all? { |path| path.is_a?(String) && path.start_with?("/") &&
|
|
30
|
+
!path.include?("\0") && File.expand_path(path) == path } &&
|
|
31
|
+
ref.is_a?(String) && ref.match?(/\Arefs\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)+\z/)
|
|
32
|
+
raise ArgumentError, "canonical snapshot source selection is malformed"
|
|
33
|
+
end
|
|
34
|
+
@repo_root, @checkout_root, @private_root, @git = paths.map { |path| path.dup.freeze }
|
|
35
|
+
@ref = ref.dup.freeze
|
|
36
|
+
@verification = Mutex.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def with(deadline:)
|
|
40
|
+
refuse!("snapshot_busy") unless @verification.try_lock
|
|
41
|
+
begin
|
|
42
|
+
with_snapshot(deadline: deadline) { |snapshot| yield snapshot }
|
|
43
|
+
ensure
|
|
44
|
+
@verification.unlock
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def with_snapshot(deadline:)
|
|
49
|
+
validate_deadline!(deadline)
|
|
50
|
+
verify_private_root!
|
|
51
|
+
private_stat = directory!(@private_root)
|
|
52
|
+
unless private_stat.uid == Process.uid && (private_stat.mode & 0o7777) == 0o700
|
|
53
|
+
refuse!("private_storage_unprotected")
|
|
54
|
+
end
|
|
55
|
+
Dir.mktmpdir("canonical-read-", @private_root) do |view|
|
|
56
|
+
verify_private_root!
|
|
57
|
+
refuse!("private_storage_unprotected") unless directory_identity(File.lstat(@private_root)) == directory_identity(private_stat)
|
|
58
|
+
@view, @deadline = view, deadline
|
|
59
|
+
@held, @directories, @inventory, @inventory_files, @metadata_files = [], [], [], [], []
|
|
60
|
+
@bytes, @entries = 0, 0
|
|
61
|
+
git_dir = File.join(@repo_root, ".git")
|
|
62
|
+
directory!(git_dir)
|
|
63
|
+
%w[commondir reftable shallow].each do |name|
|
|
64
|
+
refuse!("unsupported_source") if exists?(File.join(git_dir, name))
|
|
65
|
+
end
|
|
66
|
+
lock, original_lock = held_file!(File.join(@checkout_root, ".evidence.lock"), limit: 128)
|
|
67
|
+
begin
|
|
68
|
+
acquire!(lock)
|
|
69
|
+
begin
|
|
70
|
+
source_config!(git_dir)
|
|
71
|
+
@commit = select_ref!(git_dir)
|
|
72
|
+
objects = File.join(git_dir, "objects")
|
|
73
|
+
inventory = inventory!(objects)
|
|
74
|
+
inventory.each { |relative, handle| copy!(relative, handle) }
|
|
75
|
+
verify_held!
|
|
76
|
+
refuse!("source_changed") unless inventory_names!(objects) == @inventory &&
|
|
77
|
+
select_ref!(git_dir) == @commit && identity(File.lstat(lock.path)) == original_lock
|
|
78
|
+
verify_held!
|
|
79
|
+
ensure
|
|
80
|
+
lock.flock(File::LOCK_UN)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
prepare_view!
|
|
84
|
+
run!(%w[fsck --full --strict --no-reflogs], limit: 65_536)
|
|
85
|
+
yield self
|
|
86
|
+
ensure
|
|
87
|
+
@held&.each { |handle| handle.close unless handle.closed? }
|
|
88
|
+
@held = @directories = @inventory = @inventory_files = @metadata_files = @view = @deadline = @commit = nil
|
|
89
|
+
end
|
|
90
|
+
rescue Timeout::Error
|
|
91
|
+
refuse!("resource_limit")
|
|
92
|
+
rescue IOError, SystemCallError, Herdr::Molecules::BoundedProcess::PostLaunchError
|
|
93
|
+
refuse!("source_unavailable")
|
|
94
|
+
end
|
|
95
|
+
private :with_snapshot
|
|
96
|
+
|
|
97
|
+
attr_reader :commit
|
|
98
|
+
|
|
99
|
+
# EvidenceJournal uses this only as its existing read command boundary.
|
|
100
|
+
def call(argv, stdin_data: "", output_limit: BYTE_LIMIT)
|
|
101
|
+
refuse!("read_outside_snapshot") unless @view && @commit
|
|
102
|
+
unless read_command?(argv, stdin_data)
|
|
103
|
+
refuse!("write_or_unsupported_command")
|
|
104
|
+
end
|
|
105
|
+
result = run!(argv, stdin_data: stdin_data, limit: [output_limit, BYTE_LIMIT].min)
|
|
106
|
+
[result.stdout.to_s.strip, result.stderr.to_s.strip, result.status]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def blob_batch(entries, output_limit:)
|
|
110
|
+
refuse!("read_outside_snapshot") unless @view && @commit
|
|
111
|
+
unless entries.is_a?(Array) && entries.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
|
|
112
|
+
refuse!("unsupported_command")
|
|
113
|
+
end
|
|
114
|
+
result = run!(["cat-file", "--batch"], stdin_data: entries.join("\n") + "\n",
|
|
115
|
+
limit: [output_limit, BYTE_LIMIT].min)
|
|
116
|
+
result.stdout.to_s.b
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def history_diff(commits, paths, output_limit:)
|
|
120
|
+
refuse!("read_outside_snapshot") unless @view && @commit
|
|
121
|
+
unless commits.is_a?(Array) && commits.size.between?(1, 64) && commits.uniq.size == commits.size &&
|
|
122
|
+
commits.all? { |commit| commit.is_a?(String) && commit.match?(OID) } &&
|
|
123
|
+
paths.is_a?(Array) && !paths.empty? && paths.uniq.size == paths.size &&
|
|
124
|
+
(paths == ["execution/"] || paths.all? { |path| path.is_a?(String) && path.match?(%r{\Aexecution/[a-zA-Z0-9_.:-]+/events/\z}) }) &&
|
|
125
|
+
output_limit.is_a?(Integer) && output_limit.positive?
|
|
126
|
+
refuse!("unsupported_command")
|
|
127
|
+
end
|
|
128
|
+
result = run!(%w[diff-tree --stdin --always --root -r --raw -z --no-renames --no-ext-diff
|
|
129
|
+
--no-textconv --no-abbrev --] + paths, stdin_data: commits.join("\n") + "\n",
|
|
130
|
+
limit: [output_limit, BYTE_LIMIT].min)
|
|
131
|
+
result.stdout.to_s.b
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def blob_sizes(oids)
|
|
135
|
+
refuse!("read_outside_snapshot") unless @view && @commit
|
|
136
|
+
unless oids.is_a?(Array) && oids.size.between?(1, 64) && oids.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
|
|
137
|
+
refuse!("unsupported_command")
|
|
138
|
+
end
|
|
139
|
+
run!(%w[cat-file --batch-check], stdin_data: oids.join("\n") + "\n", limit: oids.size * 128).stdout.to_s.b
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def blob(path, commit:, max_bytes:)
|
|
143
|
+
refuse!("read_outside_snapshot") unless @view && @commit
|
|
144
|
+
unless commit.is_a?(String) && commit.match?(OID) && max_bytes.is_a?(Integer) && max_bytes.positive?
|
|
145
|
+
refuse!("unsupported_command")
|
|
146
|
+
end
|
|
147
|
+
result = run!(["cat-file", "blob", "#{commit}:#{path}"], limit: [max_bytes, BYTE_LIMIT].min)
|
|
148
|
+
result.stdout.to_s.b
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def refuse!(reason)
|
|
154
|
+
raise Unavailable, "canonical read snapshot unavailable: #{reason}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def validate_deadline!(deadline)
|
|
158
|
+
unless deadline.is_a?(Numeric) && deadline.finite? && deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
159
|
+
refuse!("resource_limit")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def remaining
|
|
164
|
+
value = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
165
|
+
refuse!("resource_limit") unless value.positive?
|
|
166
|
+
value
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def acquire!(lock)
|
|
170
|
+
until lock.flock(File::LOCK_SH | File::LOCK_NB)
|
|
171
|
+
sleep [remaining, 0.005].min
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def identity(stat)
|
|
176
|
+
[stat.dev, stat.ino, stat.mode, stat.uid, stat.gid, stat.size, stat.mtime, stat.ctime]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def verify_private_root!
|
|
180
|
+
Authority::PrivateDirectory.verify!(@private_root)
|
|
181
|
+
rescue AttemptErrors::ReceiptRejected
|
|
182
|
+
refuse!("private_storage_unprotected")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def directory_identity(stat)
|
|
186
|
+
[stat.dev, stat.ino, stat.mode, stat.uid, stat.gid]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def directory!(path)
|
|
190
|
+
stat = File.lstat(path)
|
|
191
|
+
refuse!("unsupported_source") unless stat.directory? && !stat.symlink?
|
|
192
|
+
stat
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def exists?(path)
|
|
196
|
+
File.lstat(path)
|
|
197
|
+
true
|
|
198
|
+
rescue Errno::ENOENT
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def held_file!(path, limit:)
|
|
203
|
+
remaining
|
|
204
|
+
parent = File.dirname(path)
|
|
205
|
+
until parent == "/"
|
|
206
|
+
stat = directory!(parent)
|
|
207
|
+
@directories << [parent, directory_identity(stat)]
|
|
208
|
+
parent = File.dirname(parent)
|
|
209
|
+
end
|
|
210
|
+
handle = File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
211
|
+
@held << handle
|
|
212
|
+
stat = handle.stat
|
|
213
|
+
refuse!("unsupported_source") unless stat.file?
|
|
214
|
+
refuse!("resource_limit") if stat.size > limit
|
|
215
|
+
[handle, identity(stat)]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def select_ref!(git_dir)
|
|
219
|
+
loose = File.join(git_dir, @ref)
|
|
220
|
+
if exists?(loose)
|
|
221
|
+
handle, before = held_file!(loose, limit: 128)
|
|
222
|
+
bytes = handle.read(129)
|
|
223
|
+
@metadata_files << [loose, handle, before]
|
|
224
|
+
refuse!("source_changed") unless identity(handle.stat) == before
|
|
225
|
+
refuse!("unsupported_ref") unless bytes.match?(/\A[0-9a-f]{40}\n?\z/)
|
|
226
|
+
return bytes.strip
|
|
227
|
+
end
|
|
228
|
+
packed = File.join(git_dir, "packed-refs")
|
|
229
|
+
handle, before = held_file!(packed, limit: 65_536)
|
|
230
|
+
bytes = handle.read(65_537)
|
|
231
|
+
@metadata_files << [packed, handle, before]
|
|
232
|
+
refuse!("source_changed") unless identity(handle.stat) == before
|
|
233
|
+
entries = {}
|
|
234
|
+
bytes.lines.each do |line|
|
|
235
|
+
next if line.start_with?("#") || line.match?(/\A\^[0-9a-f]{40}\n?\z/)
|
|
236
|
+
match = /\A([0-9a-f]{40}) (refs\/[^\s\0]+)\n?\z/.match(line)
|
|
237
|
+
refuse!("unsupported_ref") unless match && !entries.key?(match[2])
|
|
238
|
+
entries[match[2]] = match[1]
|
|
239
|
+
end
|
|
240
|
+
entries.fetch(@ref) { refuse!("unsupported_ref") }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def source_config!(git_dir)
|
|
244
|
+
path = File.join(git_dir, "config")
|
|
245
|
+
handle, before = held_file!(path, limit: 65_536)
|
|
246
|
+
bytes = handle.read(65_537)
|
|
247
|
+
@metadata_files << [path, handle, before]
|
|
248
|
+
refuse!("source_changed") unless identity(handle.stat) == before
|
|
249
|
+
# Standalone file parsing does not select the target repository,
|
|
250
|
+
# follow includes or execute aliases/hooks/helpers. No source config
|
|
251
|
+
# is installed into the private administrative view.
|
|
252
|
+
result = run!(["config", "--no-includes", "--null", "--list", "--file", "/dev/stdin"],
|
|
253
|
+
stdin_data: bytes, limit: 65_536)
|
|
254
|
+
versions = []
|
|
255
|
+
result.stdout.split("\0").each do |entry|
|
|
256
|
+
key, value = entry.split("\n", 2)
|
|
257
|
+
refuse!("unsupported_source") if key.start_with?("extensions.") || key == "core.worktree"
|
|
258
|
+
versions << value if key == "core.repositoryformatversion"
|
|
259
|
+
end
|
|
260
|
+
refuse!("unsupported_source") unless versions.empty? || versions == ["0"]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def inventory_names!(objects)
|
|
264
|
+
names = []
|
|
265
|
+
walk = lambda do |path, relative|
|
|
266
|
+
remaining
|
|
267
|
+
stat = File.lstat(path)
|
|
268
|
+
names << [relative, stat.directory? ? "directory" : "file"]
|
|
269
|
+
refuse!("resource_limit") if names.size > ENTRY_LIMIT
|
|
270
|
+
refuse!("unsupported_source") if stat.symlink? || !(stat.directory? || stat.file?)
|
|
271
|
+
if stat.directory?
|
|
272
|
+
unless relative.empty? || %w[pack info].include?(relative) || relative.match?(/\A[0-9a-f]{2}\z/)
|
|
273
|
+
refuse!("unsupported_source")
|
|
274
|
+
end
|
|
275
|
+
children = []
|
|
276
|
+
Dir.each_child(path) do |name|
|
|
277
|
+
remaining
|
|
278
|
+
children << name
|
|
279
|
+
refuse!("resource_limit") if children.size + names.size > ENTRY_LIMIT
|
|
280
|
+
end
|
|
281
|
+
children.sort.each { |name| walk.call(File.join(path, name), relative.empty? ? name : "#{relative}/#{name}") }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
walk.call(objects, "")
|
|
285
|
+
names
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def inventory!(objects)
|
|
289
|
+
@inventory = inventory_names!(objects)
|
|
290
|
+
files = @inventory.select { |_relative, kind| kind == "file" }.map(&:first)
|
|
291
|
+
packs = files.grep(/\Apack\/pack-[0-9a-f]{40}\.pack\z/)
|
|
292
|
+
unless packs.all? { |pack| files.include?(pack.sub(/\.pack\z/, ".idx")) } &&
|
|
293
|
+
files.grep(/\Apack\/pack-[0-9a-f]{40}\.(?:idx|rev)\z/).all? { |file| packs.include?(file.sub(/\.(?:idx|rev)\z/, ".pack")) }
|
|
294
|
+
refuse!("unsupported_source")
|
|
295
|
+
end
|
|
296
|
+
@inventory.filter_map do |relative, kind|
|
|
297
|
+
path = relative.empty? ? objects : File.join(objects, relative)
|
|
298
|
+
if kind == "directory"
|
|
299
|
+
unless relative.empty? || relative == "pack" || relative == "info" || relative.match?(/\A[0-9a-f]{2}\z/)
|
|
300
|
+
refuse!("unsupported_source")
|
|
301
|
+
end
|
|
302
|
+
@directories << [path, directory_identity(File.lstat(path))]
|
|
303
|
+
next
|
|
304
|
+
end
|
|
305
|
+
# These ordinary Git indexes/retention hints do not redirect
|
|
306
|
+
# storage. The clean view never reads them; only object bytes and
|
|
307
|
+
# pack indexes are copied. Redirect/lazy-fetch metadata refuses.
|
|
308
|
+
next if relative.match?(/\Ainfo\/(?:packs|commit-graph)\z/) ||
|
|
309
|
+
relative.match?(/\Apack\/pack-[0-9a-f]{40}\.(?:bitmap|keep)\z/)
|
|
310
|
+
unless relative.match?(/\A[0-9a-f]{2}\/[0-9a-f]{38}\z/) ||
|
|
311
|
+
relative.match?(/\Apack\/pack-[0-9a-f]{40}\.(?:pack|idx|rev)\z/)
|
|
312
|
+
refuse!("unsupported_source")
|
|
313
|
+
end
|
|
314
|
+
handle, before = held_file!(path, limit: BYTE_LIMIT)
|
|
315
|
+
@bytes += handle.stat.size
|
|
316
|
+
refuse!("resource_limit") if @bytes > BYTE_LIMIT
|
|
317
|
+
@inventory_files << [path, handle, before]
|
|
318
|
+
[relative, handle]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def copy!(relative, source)
|
|
323
|
+
destination = File.join(@view, "objects", relative)
|
|
324
|
+
FileUtils.mkdir_p(File.dirname(destination), mode: 0o700)
|
|
325
|
+
File.open(destination, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |target|
|
|
326
|
+
copied = 0
|
|
327
|
+
expected = source.stat.size
|
|
328
|
+
while (chunk = source.read(65_536))
|
|
329
|
+
remaining
|
|
330
|
+
copied += chunk.bytesize
|
|
331
|
+
refuse!("source_changed") if copied > expected
|
|
332
|
+
target.write(chunk)
|
|
333
|
+
end
|
|
334
|
+
refuse!("source_changed") unless copied == expected
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def verify_held!
|
|
339
|
+
@directories.each { |path, before| refuse!("source_changed") unless directory_identity(File.lstat(path)) == before }
|
|
340
|
+
(@inventory_files + @metadata_files).each do |path, handle, before|
|
|
341
|
+
refuse!("source_changed") unless identity(handle.stat) == before && identity(File.lstat(path)) == before
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def prepare_view!
|
|
346
|
+
FileUtils.mkdir_p(File.join(@view, "objects"), mode: 0o700)
|
|
347
|
+
FileUtils.mkdir_p(File.join(@view, "refs"), mode: 0o700)
|
|
348
|
+
File.binwrite(File.join(@view, "HEAD"), "#{@commit}\n")
|
|
349
|
+
File.binwrite(File.join(@view, "config"), "[core]\nrepositoryformatversion = 0\nbare = true\n")
|
|
350
|
+
ref = File.join(@view, @ref)
|
|
351
|
+
FileUtils.mkdir_p(File.dirname(ref), mode: 0o700)
|
|
352
|
+
File.binwrite(ref, "#{@commit}\n")
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def read_command?(argv, stdin)
|
|
356
|
+
return false unless argv.is_a?(Array) && argv.all? { |item| item.is_a?(String) && !item.include?("\0") } && stdin.empty?
|
|
357
|
+
allowed = {"rev-parse" => %w[--verify --quiet --git-dir], "cat-file" => %w[-t -s blob],
|
|
358
|
+
"rev-list" => %w[--first-parent --parents], "ls-tree" => %w[-r -l -d --name-only --],
|
|
359
|
+
"show" => [], "log" => %w[--diff-filter=A -1 --format=%H --]}
|
|
360
|
+
flags = allowed[argv.first]
|
|
361
|
+
flags && argv.drop(1).all? { |item| !item.start_with?("-") || flags.include?(item) || item == "--max-count=100001" }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def run!(argv, stdin_data: "", limit:)
|
|
365
|
+
result = Herdr::Molecules::BoundedProcess.call([@git, "--git-dir=#{@view}", "--no-pager", *argv],
|
|
366
|
+
environment: ENVIRONMENT, chdir: @view, stdin_data: stdin_data,
|
|
367
|
+
timeout_s: remaining, output_limit: limit, stderr_limit: 65_536)
|
|
368
|
+
refuse!(result.oversized ? "resource_limit" : "git_unavailable") unless result.status.success? && !result.oversized
|
|
369
|
+
result
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|