ace-bundle 0.44.2 → 0.45.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/CHANGELOG.md +10 -0
- data/lib/ace/bundle/cli/commands/load.rb +3 -0
- data/lib/ace/bundle/molecules/protected_task_context.rb +178 -0
- data/lib/ace/bundle/organisms/bundle_loader.rb +74 -33
- data/lib/ace/bundle/version.rb +1 -1
- metadata +31 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 499c5ba424482d3c33a912bbf5fd65128e5963326095445b5a0fd081c9e30ae4
|
|
4
|
+
data.tar.gz: 6da09d023b7cb96e2ef3934974a0f48e614890666deb27928d12e6a43f0332f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6c52aeb31efdd946038ed769c953c395b8557191ddad5ac173d6b8b56fc0297522bae4d46567c27c49d5c60c608c781ec8a5312844db51ea56c99c9a71c5030
|
|
7
|
+
data.tar.gz: 144acfa16735db88a83fdc9b1f48735f87b249519cae8ee0d4ba69a5af723e552819f1000fa0191a504fabebcf18575901457668f842789022ff57d128ce1b43
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog][1], and this project adheres to [Seman
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.45.0] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Ship protected task-context loading with the Runtime 0.3 API and held artifact verification.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Require Runtime 0.3 and Herdr 0.5.1 so installed contexts cannot resolve missing producer modules.
|
|
18
|
+
|
|
9
19
|
## [0.44.2] - 2026-09-29
|
|
10
20
|
|
|
11
21
|
### Fixed
|
|
@@ -79,6 +79,9 @@ module Ace
|
|
|
79
79
|
# Resource limits
|
|
80
80
|
option :max_size, type: :integer, desc: "Maximum file size in bytes"
|
|
81
81
|
option :timeout, type: :integer, desc: "Command timeout in seconds"
|
|
82
|
+
option :mapping, type: :string, desc: "Original protected mapping for captured task context"
|
|
83
|
+
option :assignment, type: :string, desc: "Original protected assignment ID@SCOPE"
|
|
84
|
+
option :attempt, type: :string, desc: "Original protected attempt for captured task context"
|
|
82
85
|
|
|
83
86
|
# Standard options (inherited from Base but need explicit definition for ace-support-cli)
|
|
84
87
|
option :version, type: :boolean, desc: "Show version information"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "ace/runtime/molecules/protected_task_context_entry"
|
|
5
|
+
require "ace/herdr/molecules/bounded_process"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Bundle
|
|
9
|
+
module Molecules
|
|
10
|
+
# Fixed installed CLI boundary. Bundle does not import Assign, select its
|
|
11
|
+
# roster or resolve task context through project command templates.
|
|
12
|
+
class ProtectedTaskContext
|
|
13
|
+
TOKEN = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,199}\z/
|
|
14
|
+
SCOPE = /\A[0-9]+(?:\.[0-9]+)*\z/
|
|
15
|
+
SHA = /\A[0-9a-f]{64}\z/
|
|
16
|
+
TEXT_LIMIT = 1_048_576
|
|
17
|
+
RESPONSE_LIMIT = 6_307_840
|
|
18
|
+
SELECTION_LIMIT = 65_536
|
|
19
|
+
PRINCIPAL_LIMIT = 1_024
|
|
20
|
+
STDERR_LIMIT = 16_384
|
|
21
|
+
DEADLINE = 30
|
|
22
|
+
SELECTOR_FIELDS = %w[mapping_id assignment_id attempt_id scope].freeze
|
|
23
|
+
ASSOCIATION_FIELDS = (SELECTOR_FIELDS + %w[definition_digest selection_sha256]).freeze
|
|
24
|
+
|
|
25
|
+
def initialize(entry_owner: Ace::Runtime::Molecules::ProtectedTaskContextEntry.new,
|
|
26
|
+
runner: Ace::Herdr::Molecules::BoundedProcess, uid: Process.uid, env: ENV)
|
|
27
|
+
@owner, @runner, @uid, @env = entry_owner, runner, uid, env
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# nil means genuinely ordinary; every installed failure raises.
|
|
31
|
+
def load(uri, options: {})
|
|
32
|
+
selected = selected?(options)
|
|
33
|
+
@owner.with do |discovery|
|
|
34
|
+
unless discovery
|
|
35
|
+
unavailable!("installed entry is absent") if selected
|
|
36
|
+
next nil
|
|
37
|
+
end
|
|
38
|
+
principal = invoke(discovery, ["authority", "task-context-principal"], PRINCIPAL_LIMIT)
|
|
39
|
+
object!(principal, %w[schema uid protected_worker])
|
|
40
|
+
unless principal.fetch("schema") == "ace.assign.task-context-principal/v1" &&
|
|
41
|
+
principal.fetch("uid").is_a?(Integer) && principal.fetch("uid") == @uid && [true, false].include?(principal.fetch("protected_worker"))
|
|
42
|
+
mismatch!("actual caller classification")
|
|
43
|
+
end
|
|
44
|
+
next nil unless principal.fetch("protected_worker") || selected
|
|
45
|
+
task = uri.delete_prefix("task://")
|
|
46
|
+
mismatch!("task URI") unless uri.start_with?("task://") && task.match?(TOKEN)
|
|
47
|
+
selectors, argv = selectors!(options)
|
|
48
|
+
selection = invoke(discovery, ["authority", "task-context-selection", *argv], SELECTION_LIMIT)
|
|
49
|
+
object!(selection, ASSOCIATION_FIELDS + %w[schema task_context_entry])
|
|
50
|
+
association!(selection, selectors, "ace.assign.task-context-selection/v1")
|
|
51
|
+
original = validated_output_pin!(selection.fetch("task_context_entry"))
|
|
52
|
+
manifest_hint!(original)
|
|
53
|
+
result = invoke(original, ["authority", "task-context", *argv, "--task", task], RESPONSE_LIMIT)
|
|
54
|
+
object!(result, ASSOCIATION_FIELDS + %w[schema task_id text])
|
|
55
|
+
association!(result, selectors, "ace.assign.prepared-task-context/v1")
|
|
56
|
+
unless result.slice(*ASSOCIATION_FIELDS) == selection.slice(*ASSOCIATION_FIELDS) && result.fetch("task_id") == task
|
|
57
|
+
mismatch!("original prepared response association")
|
|
58
|
+
end
|
|
59
|
+
text = result.fetch("text")
|
|
60
|
+
mismatch!("captured text bound") unless text.is_a?(String) && text.valid_encoding? && text.bytesize.between?(1, TEXT_LIMIT)
|
|
61
|
+
text.freeze
|
|
62
|
+
end
|
|
63
|
+
rescue Ace::Runtime::Error, IOError, SystemCallError, Timeout::Error, Ace::Herdr::Molecules::BoundedProcess::PostLaunchError => error
|
|
64
|
+
unavailable!(error.message)
|
|
65
|
+
rescue KeyError, TypeError, NoMethodError, ArgumentError, JSON::ParserError, EncodingError
|
|
66
|
+
mismatch!("closed task-context response")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def selected?(options)
|
|
72
|
+
options.values_at(:mapping, :assignment, :attempt).any? { |value| value && !value.to_s.empty? } ||
|
|
73
|
+
%w[ACE_ASSIGN_LAUNCH_MAPPING ACE_ASSIGN_ASSIGNMENT_ID ACE_ASSIGN_ATTEMPT_ID ACE_ASSIGN_TASK_CONTEXT_ENTRY].any? { |key| !@env[key].to_s.empty? }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def selectors!(options)
|
|
77
|
+
mapping = hint!(options[:mapping], "ACE_ASSIGN_LAUNCH_MAPPING")
|
|
78
|
+
attempt = hint!(options[:attempt], "ACE_ASSIGN_ATTEMPT_ID")
|
|
79
|
+
scoped = hint!(options[:assignment], "ACE_ASSIGN_DEFAULT_TARGET", token: false)
|
|
80
|
+
assignment, scope = scoped.split("@", 2)
|
|
81
|
+
mismatch!("explicit scoped original assignment") unless assignment&.match?(TOKEN) && scope&.match?(SCOPE)
|
|
82
|
+
bare_hint = @env["ACE_ASSIGN_ASSIGNMENT_ID"].to_s
|
|
83
|
+
mismatch!("assignment hint differs") unless bare_hint.empty? || bare_hint == assignment
|
|
84
|
+
[{"mapping_id" => mapping, "assignment_id" => assignment, "attempt_id" => attempt, "scope" => scope},
|
|
85
|
+
["--mapping", mapping, "--assignment", scoped, "--attempt", attempt]]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def hint!(explicit, key, token: true)
|
|
89
|
+
hint = @env[key].to_s
|
|
90
|
+
value = explicit || hint
|
|
91
|
+
unless value.is_a?(String) && value.encoding == Encoding::UTF_8 && value.valid_encoding? && !value.empty? &&
|
|
92
|
+
(hint.empty? || hint == value) && (!token || value.match?(TOKEN))
|
|
93
|
+
mismatch!("complete original selectors required")
|
|
94
|
+
end
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def association!(record, selectors, schema)
|
|
99
|
+
unless record.fetch("schema") == schema && record.slice(*SELECTOR_FIELDS) == selectors &&
|
|
100
|
+
%w[definition_digest selection_sha256].all? { |key| record.fetch(key).is_a?(String) && record.fetch(key).match?(SHA) }
|
|
101
|
+
mismatch!("original prepared selectors or digest")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def manifest_hint!(pin)
|
|
106
|
+
hint = @env["ACE_ASSIGN_TASK_CONTEXT_ENTRY"].to_s
|
|
107
|
+
return if hint.empty?
|
|
108
|
+
mismatch!("manifest hint bound") if hint.bytesize > SELECTION_LIMIT
|
|
109
|
+
reference = JSON.parse(hint, create_additions: false, max_nesting: 8, allow_duplicate_key: false, allow_comments: false)
|
|
110
|
+
candidate = validated_output_pin!({"manifest" => reference, "wrapper" => pin.fetch("wrapper")})
|
|
111
|
+
mismatch!("manifest hint differs from original") unless candidate == pin
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validated_output_pin!(pin)
|
|
115
|
+
@owner.validate_pin!(pin)
|
|
116
|
+
rescue Ace::Runtime::Error, KeyError, TypeError, NoMethodError, ArgumentError
|
|
117
|
+
mismatch!("original entry reference fields")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def invoke(pin, arguments, limit)
|
|
121
|
+
@owner.with_entry(pin) do |entry|
|
|
122
|
+
with_selector(entry.pin.fetch("manifest")) do |selector|
|
|
123
|
+
result = @runner.call(["/proc/self/fd/4", "-I", "-S", "-B", "-", *arguments],
|
|
124
|
+
stdin_data: entry.body, timeout_s: DEADLINE, output_limit: limit, stderr_limit: STDERR_LIMIT,
|
|
125
|
+
descriptor_mapping: {4 => entry.interpreter, 5 => selector}, environment: {}, chdir: "/", cleanup_group: true)
|
|
126
|
+
unavailable!("fixed task-context command failed") unless result.status&.success?
|
|
127
|
+
mismatch!("fixed task-context output was truncated") if result.oversized
|
|
128
|
+
bytes = result.stdout
|
|
129
|
+
unless bytes.is_a?(String) && bytes.bytesize.between?(1, limit)
|
|
130
|
+
mismatch!("fixed task-context output bound")
|
|
131
|
+
end
|
|
132
|
+
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
133
|
+
unless text.valid_encoding? && text.end_with?("\n") && text.count("\n") == 1
|
|
134
|
+
mismatch!("fixed task-context output framing")
|
|
135
|
+
end
|
|
136
|
+
record = JSON.parse(text, create_additions: false, max_nesting: 16, allow_duplicate_key: false, allow_comments: false)
|
|
137
|
+
mismatch!("fixed task-context output is not compact JSON") unless JSON.generate(record) + "\n" == text
|
|
138
|
+
record
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# This private temporary transports only an untrusted manifest REF. The
|
|
144
|
+
# accepted code/interpreter come from retained protected handles above.
|
|
145
|
+
def with_selector(reference)
|
|
146
|
+
bytes = JSON.generate(reference) + "\n"
|
|
147
|
+
mismatch!("entry selector bound") if bytes.bytesize > SELECTION_LIMIT
|
|
148
|
+
Dir.mktmpdir("ace-task-context-", "/tmp") do |directory|
|
|
149
|
+
path = File.join(directory, "selector.json")
|
|
150
|
+
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |writer|
|
|
151
|
+
writer.write(bytes); writer.flush
|
|
152
|
+
File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |reader|
|
|
153
|
+
unless reader.stat.file? && reader.stat.uid == Process.uid && (reader.stat.mode & 0o077).zero? &&
|
|
154
|
+
[reader.stat.dev, reader.stat.ino, reader.stat.size] == [writer.stat.dev, writer.stat.ino, bytes.bytesize]
|
|
155
|
+
unavailable!("private selector descriptor differs")
|
|
156
|
+
end
|
|
157
|
+
File.unlink(path)
|
|
158
|
+
yield reader
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def object!(record, fields)
|
|
165
|
+
mismatch!("closed fixed task-context fields") unless record.is_a?(Hash) && record.keys.sort == fields.sort
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def mismatch!(detail)
|
|
169
|
+
raise Ace::Bundle::Error, "prepared_input_mismatch: #{detail}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def unavailable!(detail)
|
|
173
|
+
raise Ace::Bundle::Error, "prepared_input_unavailable: #{detail}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -21,6 +21,7 @@ require_relative "pr_bundle_loader"
|
|
|
21
21
|
require_relative "../models/bundle_data"
|
|
22
22
|
require_relative "../atoms/content_checker"
|
|
23
23
|
require_relative "../atoms/typo_detector"
|
|
24
|
+
require_relative "../molecules/protected_task_context"
|
|
24
25
|
|
|
25
26
|
module Ace
|
|
26
27
|
module Bundle
|
|
@@ -986,19 +987,17 @@ module Ace
|
|
|
986
987
|
# Process files
|
|
987
988
|
if config["files"] && config["files"].any?
|
|
988
989
|
# Resolve any protocol references (e.g., wfi://workflow-name)
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
if !resolved && @review_safe_mode
|
|
990
|
+
source_inputs = resolve_file_inputs(config["files"]) do |file_ref|
|
|
991
|
+
if @review_safe_mode
|
|
992
992
|
data[:errors] << "Unresolved file source: #{file_ref}"
|
|
993
993
|
end
|
|
994
|
-
resolved
|
|
995
994
|
end
|
|
995
|
+
resolved_files = source_inputs.filter_map { |entry| entry[:path] }
|
|
996
996
|
missing_sources = missing_review_sources(resolved_files, base_dir: @options[:base_dir] || project_root,
|
|
997
997
|
exclude: config["exclude"] || [])
|
|
998
998
|
missing_sources.each do |pattern|
|
|
999
999
|
data[:errors] << "Unmatched required file pattern: #{pattern}"
|
|
1000
1000
|
end
|
|
1001
|
-
resolved_files -= missing_sources
|
|
1002
1001
|
|
|
1003
1002
|
aggregator = Ace::Core::Molecules::FileAggregator.new(
|
|
1004
1003
|
max_size: config["max_size"] || @options[:max_size],
|
|
@@ -1008,15 +1007,7 @@ module Ace
|
|
|
1008
1007
|
exclude: config["exclude"] || []
|
|
1009
1008
|
)
|
|
1010
1009
|
|
|
1011
|
-
|
|
1012
|
-
has_globs = resolved_files.any? { |f| f.include?("*") || f.include?("?") || f.include?("[") }
|
|
1013
|
-
|
|
1014
|
-
# Use aggregate for globs, aggregate_files for literal paths
|
|
1015
|
-
result = if has_globs
|
|
1016
|
-
aggregator.aggregate(resolved_files)
|
|
1017
|
-
else
|
|
1018
|
-
aggregator.aggregate_files(resolved_files)
|
|
1019
|
-
end
|
|
1010
|
+
result = aggregate_file_inputs(source_inputs, missing_sources, aggregator)
|
|
1020
1011
|
data[:files] = result[:files]
|
|
1021
1012
|
data[:errors].concat(result[:errors])
|
|
1022
1013
|
end
|
|
@@ -1271,8 +1262,13 @@ module Ace
|
|
|
1271
1262
|
end
|
|
1272
1263
|
|
|
1273
1264
|
def load_protocol(protocol_ref)
|
|
1265
|
+
if (text = protected_task_text(protocol_ref))
|
|
1266
|
+
bundle = Models::BundleData.new(content: text, metadata: {protocol_ref: protocol_ref, protected_prepared: true})
|
|
1267
|
+
bundle.source_files << {path: protocol_ref, content: text}
|
|
1268
|
+
return bundle
|
|
1269
|
+
end
|
|
1274
1270
|
# Resolve protocol using ace-nav
|
|
1275
|
-
resolved_path = resolve_protocol(protocol_ref)
|
|
1271
|
+
resolved_path = resolve_protocol(protocol_ref, protected_checked: true)
|
|
1276
1272
|
|
|
1277
1273
|
if resolved_path && File.exist?(resolved_path)
|
|
1278
1274
|
# Load the resolved file
|
|
@@ -1287,7 +1283,10 @@ module Ace
|
|
|
1287
1283
|
end
|
|
1288
1284
|
end
|
|
1289
1285
|
|
|
1290
|
-
def resolve_protocol(protocol_ref)
|
|
1286
|
+
def resolve_protocol(protocol_ref, protected_checked: false)
|
|
1287
|
+
if !protected_checked && protected_task_text(protocol_ref)
|
|
1288
|
+
raise Ace::Bundle::Error, "Captured protected task context must be consumed as text, never a reopened pathname"
|
|
1289
|
+
end
|
|
1291
1290
|
require "ace/support/nav"
|
|
1292
1291
|
engine = Ace::Support::Nav::Organisms::NavigationEngine.new
|
|
1293
1292
|
if @review_safe_mode && engine.cmd_protocol?(protocol_ref.split("://", 2).first)
|
|
@@ -1317,10 +1316,16 @@ module Ace
|
|
|
1317
1316
|
nil
|
|
1318
1317
|
end
|
|
1319
1318
|
|
|
1320
|
-
def
|
|
1319
|
+
def protected_task_text(reference)
|
|
1320
|
+
return nil unless reference.is_a?(String) && reference.start_with?("task://")
|
|
1321
|
+
@protected_task_context ||= Molecules::ProtectedTaskContext.new
|
|
1322
|
+
@protected_task_context.load(reference, options: @options)
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
def resolve_file_reference(file_ref, protected_checked: false)
|
|
1321
1326
|
# Check if it's a protocol reference (contains ://)
|
|
1322
1327
|
if file_ref.match?(/^[\w-]+:\/\//)
|
|
1323
|
-
resolve_protocol(file_ref)
|
|
1328
|
+
resolve_protocol(file_ref, protected_checked: protected_checked)
|
|
1324
1329
|
elsif file_ref.start_with?("./") && @template_dir
|
|
1325
1330
|
# Resolve ./ paths relative to the template file's directory
|
|
1326
1331
|
File.join(@template_dir, file_ref)
|
|
@@ -1330,6 +1335,45 @@ module Ace
|
|
|
1330
1335
|
end
|
|
1331
1336
|
end
|
|
1332
1337
|
|
|
1338
|
+
def resolve_file_inputs(references)
|
|
1339
|
+
references.filter_map do |reference|
|
|
1340
|
+
if (text = protected_task_text(reference))
|
|
1341
|
+
{record: {path: reference, content: text}}
|
|
1342
|
+
elsif (path = resolve_file_reference(reference, protected_checked: true))
|
|
1343
|
+
{path: path}
|
|
1344
|
+
else
|
|
1345
|
+
yield reference if block_given?
|
|
1346
|
+
nil
|
|
1347
|
+
end
|
|
1348
|
+
end
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def aggregate_file_inputs(inputs, missing, aggregator)
|
|
1352
|
+
paths = inputs.filter_map { |entry| entry[:path] } - missing
|
|
1353
|
+
unless inputs.any? { |entry| entry[:record] }
|
|
1354
|
+
return aggregate_paths(paths, aggregator)
|
|
1355
|
+
end
|
|
1356
|
+
# Retained protocol text enters the existing content record interface,
|
|
1357
|
+
# without a temporary pathname. Preserve mixed source order.
|
|
1358
|
+
inputs.each_with_object({files: [], errors: []}) do |entry, result|
|
|
1359
|
+
if entry[:record]
|
|
1360
|
+
result[:files] << entry.fetch(:record)
|
|
1361
|
+
elsif !missing.include?(entry.fetch(:path))
|
|
1362
|
+
part = aggregate_paths([entry.fetch(:path)], aggregator)
|
|
1363
|
+
result[:files].concat(part.fetch(:files))
|
|
1364
|
+
result[:errors].concat(part.fetch(:errors))
|
|
1365
|
+
end
|
|
1366
|
+
end
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
def aggregate_paths(paths, aggregator)
|
|
1370
|
+
if paths.any? { |path| path.include?("*") || path.include?("?") || path.include?("[") }
|
|
1371
|
+
aggregator.aggregate(paths)
|
|
1372
|
+
else
|
|
1373
|
+
aggregator.aggregate_files(paths)
|
|
1374
|
+
end
|
|
1375
|
+
end
|
|
1376
|
+
|
|
1333
1377
|
# Process content for a specific section
|
|
1334
1378
|
def process_section_content(bundle, section_name, section_data, options, bundle_config = {})
|
|
1335
1379
|
# Process all content types that are present in the section
|
|
@@ -1356,21 +1400,19 @@ module Ace
|
|
|
1356
1400
|
return unless files.any?
|
|
1357
1401
|
|
|
1358
1402
|
# Resolve any protocol references (e.g., wfi://workflow-name)
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
if !resolved && @review_safe_mode
|
|
1403
|
+
source_inputs = resolve_file_inputs(files) do |file_ref|
|
|
1404
|
+
if @review_safe_mode
|
|
1362
1405
|
bundle.metadata[:errors] ||= []
|
|
1363
1406
|
bundle.metadata[:errors] << "Section '#{section_name}': unresolved file source: #{file_ref}"
|
|
1364
1407
|
end
|
|
1365
|
-
resolved
|
|
1366
1408
|
end
|
|
1409
|
+
resolved_files = source_inputs.filter_map { |entry| entry[:path] }
|
|
1367
1410
|
missing_sources = missing_review_sources(resolved_files, base_dir: options[:base_dir] || project_root,
|
|
1368
1411
|
exclude: section_data[:exclude] || section_data["exclude"] || [])
|
|
1369
1412
|
missing_sources.each do |pattern|
|
|
1370
1413
|
bundle.metadata[:errors] ||= []
|
|
1371
1414
|
bundle.metadata[:errors] << "Section '#{section_name}': unmatched required file pattern: #{pattern}"
|
|
1372
1415
|
end
|
|
1373
|
-
resolved_files -= missing_sources
|
|
1374
1416
|
|
|
1375
1417
|
aggregator = Ace::Core::Molecules::FileAggregator.new(
|
|
1376
1418
|
max_size: options[:max_size] || options["max_size"],
|
|
@@ -1380,15 +1422,7 @@ module Ace
|
|
|
1380
1422
|
exclude: section_data[:exclude] || section_data["exclude"] || []
|
|
1381
1423
|
)
|
|
1382
1424
|
|
|
1383
|
-
|
|
1384
|
-
has_globs = resolved_files.any? { |f| f.include?("*") || f.include?("?") || f.include?("[") }
|
|
1385
|
-
|
|
1386
|
-
# Use aggregate for globs, aggregate_files for literal paths to preserve order
|
|
1387
|
-
result = if has_globs
|
|
1388
|
-
aggregator.aggregate(resolved_files)
|
|
1389
|
-
else
|
|
1390
|
-
aggregator.aggregate_files(resolved_files)
|
|
1391
|
-
end
|
|
1425
|
+
result = aggregate_file_inputs(source_inputs, missing_sources, aggregator)
|
|
1392
1426
|
|
|
1393
1427
|
# Store section files in section data
|
|
1394
1428
|
section_data[:_processed_files] = result[:files]
|
|
@@ -1491,6 +1525,13 @@ module Ace
|
|
|
1491
1525
|
files_in_content = bundle.source_files.any? &&
|
|
1492
1526
|
(!bundle_config["embed_document_source"] || @section_processor.has_sections?({"bundle" => bundle_config}))
|
|
1493
1527
|
|
|
1528
|
+
if (text = protected_task_text(base_ref))
|
|
1529
|
+
bundle.content = [text, (bundle.content if files_in_content)].compact.join("\n\n")
|
|
1530
|
+
bundle.metadata.merge!(base_ref: base_ref, base_type: "protected_prepared")
|
|
1531
|
+
bundle.source_files << {path: base_ref, content: text}
|
|
1532
|
+
return
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1494
1535
|
# Check if base_ref looks like a file reference (has protocol, slashes, or is a known path pattern)
|
|
1495
1536
|
# This heuristic helps prioritize file resolution for extension-less files
|
|
1496
1537
|
has_protocol = base_ref.match?(/^[\w-]+:\/\//)
|
|
@@ -1499,7 +1540,7 @@ module Ace
|
|
|
1499
1540
|
(@review_safe_mode && base_ref.match?(/\.(?:md|markdown|txt|json|ya?ml)\z/i))
|
|
1500
1541
|
|
|
1501
1542
|
# Try to resolve as file reference first (handles extension-less files like README, CONTEXT)
|
|
1502
|
-
resolved_path = resolve_file_reference(base_ref)
|
|
1543
|
+
resolved_path = resolve_file_reference(base_ref, protected_checked: true)
|
|
1503
1544
|
|
|
1504
1545
|
# Check if we successfully resolved to an existing file
|
|
1505
1546
|
if resolved_path && File.exist?(resolved_path)
|
data/lib/ace/bundle/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-bundle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.45.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -107,6 +107,34 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '0.28'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: ace-runtime
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.3'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.3'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: ace-herdr
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 0.5.1
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: 0.5.1
|
|
110
138
|
- !ruby/object:Gem::Dependency
|
|
111
139
|
name: ace-compressor
|
|
112
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -172,6 +200,7 @@ files:
|
|
|
172
200
|
- lib/ace/bundle/molecules/bundle_file_writer.rb
|
|
173
201
|
- lib/ace/bundle/molecules/bundle_merger.rb
|
|
174
202
|
- lib/ace/bundle/molecules/preset_manager.rb
|
|
203
|
+
- lib/ace/bundle/molecules/protected_task_context.rb
|
|
175
204
|
- lib/ace/bundle/molecules/section_compressor.rb
|
|
176
205
|
- lib/ace/bundle/molecules/section_formatter.rb
|
|
177
206
|
- lib/ace/bundle/molecules/section_processor.rb
|