ace-assign 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
  3. data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
  4. data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
  5. data/CHANGELOG.md +7 -0
  6. data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
  7. data/lib/ace/assign/authority/assignment_definition.rb +35 -0
  8. data/lib/ace/assign/authority/candidate_transfer.rb +2 -2
  9. data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
  10. data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
  11. data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
  12. data/lib/ace/assign/authority/ordinary_client.rb +70 -0
  13. data/lib/ace/assign/authority/ordinary_completion.rb +94 -0
  14. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
  15. data/lib/ace/assign/authority/ordinary_lifecycle.rb +191 -0
  16. data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
  17. data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
  18. data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
  19. data/lib/ace/assign/authority/ordinary_results.rb +196 -0
  20. data/lib/ace/assign/authority/ordinary_server.rb +75 -0
  21. data/lib/ace/assign/authority/prepared_work.rb +2 -2
  22. data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
  23. data/lib/ace/assign/authority/router.rb +11 -6
  24. data/lib/ace/assign/authority/server.rb +11 -180
  25. data/lib/ace/assign/authority/socket_server.rb +255 -0
  26. data/lib/ace/assign/authority/transfer_codec.rb +2 -2
  27. data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
  28. data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
  29. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
  30. data/lib/ace/assign/molecules/evidence_journal.rb +141 -34
  31. data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
  32. data/lib/ace/assign/molecules/journal_mutation.rb +5 -3
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
  34. data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
  35. data/lib/ace/assign/organisms/attempt_coordinator.rb +51 -36
  36. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
  37. data/lib/ace/assign/version.rb +1 -1
  38. metadata +23 -9
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "fileutils"
5
+ require_relative "transfer_codec"
6
+ require_relative "../molecules/evidence_journal"
7
+
8
+ module Ace
9
+ module Assign
10
+ module Authority
11
+ # Shared endpoint lifetime and bounded wire protocol. Concrete source
12
+ # compositions authenticate their own installed principals before dispatch.
13
+ # Protected deployment/kernel requirements remain in Server.
14
+ class SocketServer
15
+ CONNECTIONS = 16
16
+
17
+ def initialize(service:, lifecycle:, services: [])
18
+ unless services.is_a?(Array) && services.uniq.size == services.size &&
19
+ services.all? { |owned| owned.respond_to?(:run) && owned.respond_to?(:stop) }
20
+ raise ArgumentError, "authority services require distinct source-owned run/stop lifetimes"
21
+ end
22
+ @service, @lifecycle, @services = service, lifecycle, services.dup.freeze
23
+ @service_threads = []
24
+ @activation_gates = []
25
+ @mutex = Mutex.new
26
+ @connections = []
27
+ @handlers = []
28
+ @transfers = 0
29
+ @stopped = false
30
+ end
31
+
32
+ def serve
33
+ verify_listener!
34
+ directory = File.dirname(@service.fetch("socket_path"))
35
+ wire.root_path!(directory, directory: true, owner: @service.fetch("uid"))
36
+ lock = File.open(File.join(directory, "listener.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
37
+ unless lock.flock(File::LOCK_EX | File::LOCK_NB)
38
+ raise AttemptErrors::Conflict, "assignment authority already owns its listener"
39
+ end
40
+ path = @service.fetch("socket_path")
41
+ if File.exist?(path) || File.symlink?(path)
42
+ raise AttemptErrors::Conflict, "existing authority endpoint requires verified installer recovery"
43
+ end
44
+ @listener = UNIXServer.new(path)
45
+ @endpoint = wire.socket_identity(path)
46
+ File.chmod(0o660, path)
47
+ start_services!
48
+ verify_services!
49
+ @activation_gates.each { |gate| gate << :activate }
50
+ until @stopped
51
+ verify_services!
52
+ ready = IO.select([@listener], nil, nil, 0.25)
53
+ next unless ready
54
+ socket = @listener.accept_nonblock(exception: false)
55
+ next if socket == :wait_readable
56
+ admitted = @mutex.synchronize do
57
+ if @connections.size < CONNECTIONS
58
+ @connections << socket
59
+ true
60
+ end
61
+ end
62
+ unless admitted
63
+ refusal(socket, "busy")
64
+ socket.close
65
+ next
66
+ end
67
+ @mutex.synchronize do
68
+ @handlers.reject! { |handler| !handler.alive? }
69
+ @handlers << Thread.new(socket) { |connection| receive(connection) }
70
+ end
71
+ end
72
+ rescue IOError, Errno::EBADF
73
+ raise unless @stopped
74
+ ensure
75
+ begin
76
+ stop
77
+ stop_services!
78
+ ensure
79
+ # Closing a stream does not cancel an admitted transaction. Hold
80
+ # the owner lock and endpoint until every handler has ended.
81
+ begin
82
+ @mutex.synchronize { @handlers.dup }.each(&:join)
83
+ @lifecycle.close if @endpoint && @lifecycle.respond_to?(:close)
84
+ ensure
85
+ begin
86
+ File.unlink(path) if @endpoint && same_endpoint?(path)
87
+ ensure
88
+ lock&.close
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def request_stop
95
+ @stopped = true
96
+ end
97
+
98
+ def stop
99
+ @stopped = true
100
+ @listener&.close unless @listener&.closed?
101
+ @mutex.synchronize { @connections.each { |socket| socket.close unless socket.closed? } }
102
+ end
103
+
104
+ private
105
+
106
+ # Installed HITL and merge listeners share the assignment owner lock.
107
+ # Every selected boundary must be ready before assignment ingress opens;
108
+ # a stopped boundary closes admission until the complete owner restarts.
109
+ def start_services!
110
+ @services.each do |owned|
111
+ ready = Queue.new
112
+ activation = Queue.new
113
+ @activation_gates << activation
114
+ thread = Thread.new do
115
+ owned.run(on_ready: lambda do
116
+ ready << :ready
117
+ unless activation.pop == :activate
118
+ raise Ace::Runtime::RuntimeUnavailableError, "authority startup was cancelled"
119
+ end
120
+ end)
121
+ rescue Exception => error
122
+ ready << error
123
+ ensure
124
+ ready << :stopped
125
+ end
126
+ thread.report_on_exception = false
127
+ @service_threads << [owned, thread]
128
+ result = ready.pop(timeout: 5)
129
+ raise Ace::Runtime::RuntimeUnavailableError, "authority service startup did not become ready" unless result == :ready || result.is_a?(Exception)
130
+ raise result if result.is_a?(Exception)
131
+ end
132
+ end
133
+
134
+ def verify_services!
135
+ unless @service_threads.all? { |_, thread| thread.alive? }
136
+ raise Ace::Runtime::RuntimeUnavailableError, "an owned service stopped before its authority"
137
+ end
138
+ end
139
+
140
+ def stop_services!
141
+ errors = []
142
+ @activation_gates.each { |gate| gate << :cancel }
143
+ @service_threads.reverse_each do |owned, _|
144
+ owned.stop
145
+ rescue Exception => error
146
+ errors << error
147
+ end
148
+ @service_threads.each do |_, thread|
149
+ thread.join
150
+ rescue Exception => error
151
+ errors << error
152
+ end
153
+ raise errors.first unless errors.empty?
154
+ end
155
+
156
+ def same_endpoint?(path)
157
+ wire.socket_identity(path) == @endpoint
158
+ rescue StandardError
159
+ false
160
+ end
161
+
162
+ def verify_listener!
163
+ raise NotImplementedError, "source composition must verify its installed listener"
164
+ end
165
+
166
+ def dispatch_request(socket, request, peer, role, deadline, frame_bytes:)
167
+ params = request.fetch("params")
168
+ admitted_transfer = false
169
+ Molecules::EvidenceJournal.with_event_read_operation do
170
+ binding = @lifecycle.transfer_binding(request) if @lifecycle.respond_to?(:transfer_binding)
171
+ if binding
172
+ raise AttemptErrors::UnauthorizedIdentity, "transfer role differs" unless binding.fetch(:roles).include?(role)
173
+ raise ArgumentError, "transfer control header is oversized" if frame_bytes > 16_384
174
+ authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
175
+ admitted_transfer = @mutex.synchronize do
176
+ if @transfers < 2
177
+ @transfers += 1
178
+ true
179
+ end
180
+ end
181
+ raise AttemptErrors::Conflict, "transfer capacity is busy" unless admitted_transfer
182
+ codec = transfer_codec
183
+ if binding.fetch(:direction) == :upload
184
+ result = codec.receive(socket, descriptor: params.fetch("transfer"), purpose: binding.fetch(:purpose), deadline: deadline) do |input|
185
+ dispatch_lifecycle(request: request, peer: peer, role: role, transfer: input, deadline: deadline)
186
+ end
187
+ else
188
+ result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
189
+ parts = result.fetch(:transfer_parts)
190
+ descriptor = codec.descriptor(parts, purpose: binding.fetch(:purpose))
191
+ result = result.merge(data: result.fetch(:data).merge("transfer" => descriptor))
192
+ end
193
+ else
194
+ raise ArgumentError, "unexpected transfer descriptor" if params.key?("transfer")
195
+ result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
196
+ end
197
+ wire.write(socket, {"status" => "ok", "data" => result.fetch(:data),
198
+ "transport" => {"replayed" => result.fetch(:replayed, false)}}, deadline: deadline, limit: 16_384)
199
+ if binding && binding.fetch(:direction) == :download
200
+ codec.send(socket, parts: parts, descriptor: descriptor, purpose: binding.fetch(:purpose), deadline: deadline)
201
+ end
202
+ end
203
+ ensure
204
+ @mutex.synchronize { @transfers -= 1 } if admitted_transfer
205
+ end
206
+
207
+ def authorize_transfer!(request:, peer:, role:, deadline:)
208
+ @lifecycle.authorize_transfer!(request: request, peer: peer, role: role)
209
+ end
210
+
211
+ def dispatch_lifecycle(request:, peer:, role:, deadline:, transfer: nil)
212
+ options = {request: request, peer: peer, role: role}
213
+ options[:transfer] = transfer if transfer
214
+ @lifecycle.dispatch(**options)
215
+ end
216
+
217
+ # These closed read operations have no request body. Half-close proves
218
+ # framing completion before status or artifact data can be exposed.
219
+ def bodyless_read!(socket, deadline)
220
+ loop do
221
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
222
+ raise ArgumentError, "read request framing is incomplete" unless remaining.positive? && IO.select([socket], nil, nil, remaining)
223
+ bytes = socket.read_nonblock(1, exception: false)
224
+ next if bytes == :wait_readable
225
+ raise ArgumentError, "unexpected read request body" unless bytes.nil?
226
+ return
227
+ end
228
+ end
229
+
230
+ def transfer_codec
231
+ @mutex.synchronize do
232
+ @transfer_codec ||= begin
233
+ root = @service.fetch("state_root")
234
+ PrivateDirectory.verify!(root)
235
+ spool = File.join(root, "transfers")
236
+ Dir.mkdir(spool, 0o700) unless File.exist?(spool) || File.symlink?(spool)
237
+ TransferCodec.new(root: spool)
238
+ end
239
+ end
240
+ end
241
+
242
+ def refusal(socket, code)
243
+ wire.write(socket, {"status" => "error", "error" => {"code" => code, "message" => "Protected authority refused #{code}"}},
244
+ deadline: wire.deadline(0.1))
245
+ rescue StandardError
246
+ nil
247
+ end
248
+
249
+ def wire
250
+ Ace::Runtime::Molecules::ProtectedSocket
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -16,7 +16,7 @@ module Ace
16
16
  LIMITS = {prompt_text: [16_384, 1, 16_384], scope_boundary_observation: [65_536, 1, 65_536], candidate: [64 * 1024 * 1024, 1, 64 * 1024 * 1024],
17
17
  artifacts: [256 * 1024, 16, 64 * 1024],
18
18
  receipt_artifacts: [272 * 1024, 17, 64 * 1024],
19
- service_input: [64 * 1024, 1, 64 * 1024]}.freeze
19
+ service_input: [64 * 1024, 1, 64 * 1024], assignment_snapshot: [64 * 1024 * 1024, 1, 64 * 1024 * 1024]}.freeze
20
20
  SHA256 = /\A[0-9a-f]{64}\z/
21
21
 
22
22
  class Input
@@ -149,7 +149,7 @@ module Ace
149
149
  part["bytes"].is_a?(Integer) && part["bytes"].between?(0, each) &&
150
150
  part["sha256"].is_a?(String) && part["sha256"].match?(SHA256) } &&
151
151
  descriptor["bytes"] == descriptor["parts"].sum { |part| part["bytes"] } &&
152
- (!%i[candidate service_input prompt_text].include?(purpose) || descriptor["bytes"].positive?)
152
+ (!%i[candidate service_input prompt_text assignment_snapshot].include?(purpose) || descriptor["bytes"].positive?)
153
153
  reject!("Invalid or oversized transfer descriptor")
154
154
  end
155
155
  if purpose == :receipt_artifacts
@@ -7,12 +7,12 @@ module Ace
7
7
  module Attempt
8
8
  class Evidence < Ace::Support::Cli::Command
9
9
  include Attempt::Base
10
- desc "Read coordinator-accepted live execution or historical review authority"
10
+ desc "Read coordinator-accepted live execution or historical execution authority"
11
11
  option :attempt, required: true, desc: "Accepted execution attempt ID"
12
12
  option :receipt_digest, required: true, desc: "Accepted receipt digest"
13
13
  option :kind, default: "check", desc: "Evidence purpose: check, review-collection or review-approval"
14
14
  option :check_name, default: "tests", desc: "Required check name (tests binds operation test)"
15
- option :historical_head, desc: "Recorded collection/approval head for historical authority only"
15
+ option :historical_head, desc: "Exact recorded head for historical authority only (never current-head evidence)"
16
16
  option :format, default: "json", desc: "JSON evidence projection"
17
17
 
18
18
  def call(**options)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Molecules
6
+ # Closed bounded read dialect shared by isolated protected snapshots and
7
+ # trusted installed journals. Neither boundary can dispatch a Git write.
8
+ module CanonicalReadCommands
9
+ BYTE_LIMIT = 256 * 1_048_576
10
+ OID = /\A[0-9a-f]{40}\z/
11
+
12
+ attr_reader :commit
13
+
14
+ # EvidenceJournal uses this only as its existing read command boundary.
15
+ def call(argv, stdin_data: "", output_limit: BYTE_LIMIT)
16
+ refuse!("read_outside_snapshot") unless @view && @commit
17
+ unless read_command?(argv, stdin_data)
18
+ refuse!("write_or_unsupported_command")
19
+ end
20
+ result = read_result(argv, stdin_data: stdin_data, limit: [output_limit, BYTE_LIMIT].min)
21
+ [result.stdout.to_s.strip, result.stderr.to_s.strip, result.status]
22
+ end
23
+
24
+ def blob_batch(entries, output_limit:)
25
+ refuse!("read_outside_snapshot") unless @view && @commit
26
+ unless entries.is_a?(Array) && entries.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
27
+ refuse!("unsupported_command")
28
+ end
29
+ result = run!(["cat-file", "--batch"], stdin_data: entries.join("\n") + "\n",
30
+ limit: [output_limit, BYTE_LIMIT].min)
31
+ result.stdout.to_s.b
32
+ end
33
+
34
+ def history_diff(commits, paths, output_limit:)
35
+ refuse!("read_outside_snapshot") unless @view && @commit
36
+ unless commits.is_a?(Array) && commits.size.between?(1, 64) && commits.uniq.size == commits.size &&
37
+ commits.all? { |commit| commit.is_a?(String) && commit.match?(OID) } &&
38
+ paths.is_a?(Array) && !paths.empty? && paths.uniq.size == paths.size &&
39
+ (paths == ["execution/"] || paths.all? { |path| path.is_a?(String) && path.match?(%r{\Aexecution/[a-zA-Z0-9_.:-]+/events/\z}) }) &&
40
+ output_limit.is_a?(Integer) && output_limit.positive?
41
+ refuse!("unsupported_command")
42
+ end
43
+ result = run!(%w[diff-tree --stdin --always --root -r --raw -z --no-renames --no-ext-diff
44
+ --no-textconv --no-abbrev --] + paths, stdin_data: commits.join("\n") + "\n",
45
+ limit: [output_limit, BYTE_LIMIT].min)
46
+ result.stdout.to_s.b
47
+ end
48
+
49
+ def blob_sizes(oids)
50
+ refuse!("read_outside_snapshot") unless @view && @commit
51
+ unless oids.is_a?(Array) && oids.size.between?(1, 64) && oids.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
52
+ refuse!("unsupported_command")
53
+ end
54
+ run!(%w[cat-file --batch-check], stdin_data: oids.join("\n") + "\n", limit: oids.size * 128).stdout.to_s.b
55
+ end
56
+
57
+ def blob(path, commit:, max_bytes:)
58
+ refuse!("read_outside_snapshot") unless @view && @commit
59
+ unless commit.is_a?(String) && commit.match?(OID) && max_bytes.is_a?(Integer) && max_bytes.positive?
60
+ refuse!("unsupported_command")
61
+ end
62
+ result = run!(["cat-file", "blob", "#{commit}:#{path}"], limit: [max_bytes, BYTE_LIMIT].min)
63
+ result.stdout.to_s.b
64
+ end
65
+
66
+ private
67
+
68
+ def read_result(argv, **options)
69
+ run!(argv, **options)
70
+ end
71
+
72
+ def read_command?(argv, stdin)
73
+ return false unless argv.is_a?(Array) && argv.all? { |item| item.is_a?(String) && !item.include?("\0") } && stdin.empty?
74
+ allowed = {"rev-parse" => %w[--verify --quiet --git-dir], "cat-file" => %w[-t -s blob],
75
+ "rev-list" => %w[--first-parent --parents], "ls-tree" => %w[-r -l -d --name-only --],
76
+ "show" => [], "log" => %w[--diff-filter=A -1 --format=%H --]}
77
+ flags = allowed[argv.first]
78
+ flags && argv.drop(1).all? { |item| !item.start_with?("-") || flags.include?(item) || item == "--max-count=100001" }
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+ end
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "tmpdir"
6
6
  require "ace/herdr/molecules/bounded_process"
7
7
  require_relative "evidence_journal"
8
+ require_relative "canonical_read_commands"
8
9
  require_relative "../authority/private_directory"
9
10
 
10
11
  module Ace
@@ -13,6 +14,7 @@ module Ace
13
14
  # Isolates read-only Git from an installed authority's live configuration.
14
15
  # The copied view never publishes evidence or mutates the source journal.
15
16
  class CanonicalReadSnapshot
17
+ include CanonicalReadCommands
16
18
  class Unavailable < AttemptErrors::EvidenceUnavailable; end
17
19
  BYTE_LIMIT = 256 * 1_048_576
18
20
  ENTRY_LIMIT = 4096
@@ -94,60 +96,6 @@ module Ace
94
96
  end
95
97
  private :with_snapshot
96
98
 
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
99
  private
152
100
 
153
101
  def refuse!(reason)
@@ -352,15 +300,6 @@ module Ace
352
300
  File.binwrite(ref, "#{@commit}\n")
353
301
  end
354
302
 
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
303
  def run!(argv, stdin_data: "", limit:)
365
304
  result = Herdr::Molecules::BoundedProcess.call([@git, "--git-dir=#{@view}", "--no-pager", *argv],
366
305
  environment: ENVIRONMENT, chdir: @view, stdin_data: stdin_data,