ace-herdr 0.4.0 → 0.5.1
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/herdr/config.yml +0 -4
- data/CHANGELOG.md +18 -0
- data/docs/usage.md +57 -35
- data/lib/ace/herdr/cli/commands/inbox.rb +83 -21
- data/lib/ace/herdr/cli/commands/native_source.rb +56 -0
- data/lib/ace/herdr/cli.rb +3 -0
- data/lib/ace/herdr/errors.rb +23 -0
- data/lib/ace/herdr/molecules/bounded_process.rb +261 -52
- data/lib/ace/herdr/molecules/delivery_record_store.rb +81 -6
- data/lib/ace/herdr/molecules/guarded_native_origin.rb +59 -0
- data/lib/ace/herdr/molecules/herdr_executor.rb +6 -3
- data/lib/ace/herdr/molecules/inbox_cli_input.rb +36 -0
- data/lib/ace/herdr/molecules/inbox_context_client.rb +88 -0
- data/lib/ace/herdr/molecules/inbox_context_completion_client.rb +119 -0
- data/lib/ace/herdr/molecules/inbox_context_key.rb +63 -0
- data/lib/ace/herdr/molecules/inbox_context_lifetime.rb +122 -0
- data/lib/ace/herdr/molecules/inbox_context_native_process.rb +92 -0
- data/lib/ace/herdr/molecules/inbox_context_service_configuration.rb +183 -0
- data/lib/ace/herdr/molecules/inbox_context_store.rb +159 -0
- data/lib/ace/herdr/molecules/inbox_context_wire.rb +76 -0
- data/lib/ace/herdr/molecules/inbox_direct_effect_binding.rb +92 -0
- data/lib/ace/herdr/molecules/inbox_direct_result.rb +65 -0
- data/lib/ace/herdr/molecules/native_queue_executor.rb +10 -21
- data/lib/ace/herdr/molecules/native_source.rb +189 -0
- data/lib/ace/herdr/molecules/protected_inbox_selection.rb +126 -0
- data/lib/ace/herdr/molecules/protected_native_control.rb +129 -4
- data/lib/ace/herdr/native_source/HERDR-LICENSE +201 -0
- data/lib/ace/herdr/native_source/guarded-prompt.patch +3493 -0
- data/lib/ace/herdr/native_source/selection.json +24 -0
- data/lib/ace/herdr/organisms/inbox.rb +293 -203
- data/lib/ace/herdr/organisms/inbox_context_direct_effects.rb +285 -0
- data/lib/ace/herdr/organisms/inbox_context_effects.rb +30 -0
- data/lib/ace/herdr/organisms/inbox_context_listener.rb +151 -0
- data/lib/ace/herdr/organisms/inbox_context_owner.rb +550 -0
- data/lib/ace/herdr/organisms/inbox_context_server.rb +70 -0
- data/lib/ace/herdr/organisms/inbox_context_service.rb +107 -0
- data/lib/ace/herdr/organisms/inbox_guarded_wake.rb +117 -0
- data/lib/ace/herdr/organisms/native_source_builder.rb +183 -0
- data/lib/ace/herdr/version.rb +1 -1
- data/lib/ace/herdr.rb +5 -28
- metadata +68 -9
- data/lib/ace/herdr/organisms/protected_inbox.rb +0 -73
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../molecules/inbox_context_native_process"
|
|
4
|
+
require_relative "../molecules/inbox_context_completion_client"
|
|
5
|
+
require_relative "../molecules/native_queue_executor"
|
|
6
|
+
require_relative "../molecules/herdr_executor"
|
|
7
|
+
require_relative "inbox_context_server"
|
|
8
|
+
require_relative "inbox_context_listener"
|
|
9
|
+
|
|
10
|
+
module Ace
|
|
11
|
+
module Herdr
|
|
12
|
+
module Organisms
|
|
13
|
+
# The generated fixed entry calls these distinct methods only after its
|
|
14
|
+
# SAME installation owner selects normal start or retained initialization.
|
|
15
|
+
# There is no caller-supplied Inbox, epoch or native executable fallback.
|
|
16
|
+
class InboxContextService
|
|
17
|
+
ASSOCIATION = %i[cgroups configuration installation kernel manager].freeze
|
|
18
|
+
attr_reader :epoch, :configuration
|
|
19
|
+
|
|
20
|
+
def initialize(configuration:, epoch:, association:, listener:, store:)
|
|
21
|
+
@configuration, @epoch, @association, @listener, @store = configuration, epoch, association, listener, store
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def serve
|
|
25
|
+
observed = Molecules::InboxContextLifetime.new(**@association).capture_epoch!
|
|
26
|
+
raise ValidationError, "context service epoch changed before ingress" unless observed == @epoch
|
|
27
|
+
@listener.serve
|
|
28
|
+
ensure
|
|
29
|
+
close
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def close
|
|
33
|
+
raise ValidationError, "context service handlers remain unresolved" unless @listener.quiescent?
|
|
34
|
+
@store.close
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.start!(stage:, installation:, bootstrap:)
|
|
38
|
+
run!(stage: stage, installation: installation, bootstrap: bootstrap, initialize_context: false)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.provision!(stage:, installation:, bootstrap:)
|
|
42
|
+
run!(stage: stage, installation: installation, bootstrap: bootstrap, initialize_context: true)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.run!(stage:, installation:, bootstrap:, initialize_context:)
|
|
46
|
+
configuration = Molecules::InboxContextServiceConfiguration.load(stage: stage)
|
|
47
|
+
constructed = nil
|
|
48
|
+
owner_method = initialize_context ? :with_initial_inbox_context : :with_inbox_context_service
|
|
49
|
+
prepared = bootstrap.public_send(owner_method, configuration: configuration, installation: installation) do |association|
|
|
50
|
+
unless association.is_a?(Hash) && association.keys.sort == ASSOCIATION && association.frozen? &&
|
|
51
|
+
association.fetch(:configuration).is_a?(Molecules::InboxContextServiceConfiguration) &&
|
|
52
|
+
association.fetch(:configuration).reference == configuration.reference && association.fetch(:configuration).data == configuration.data
|
|
53
|
+
raise ValidationError, "context service owner association differs"
|
|
54
|
+
end
|
|
55
|
+
selected = association.fetch(:configuration)
|
|
56
|
+
clients = selected.data.fetch("native_clients")
|
|
57
|
+
association.fetch(:installation).verify_inbox_native_projection!(
|
|
58
|
+
references: clients.values_at("codex", "pi", "herdr") + clients.fetch("dependencies"),
|
|
59
|
+
resources: clients.fetch("resources"), manager: association.fetch(:manager))
|
|
60
|
+
lifetime = Molecules::InboxContextLifetime.new(**association)
|
|
61
|
+
epoch = lifetime.capture_epoch!
|
|
62
|
+
data = selected.data
|
|
63
|
+
keys = Molecules::InboxContextKey.new(context_id: data.fetch("inbox_context_id"),
|
|
64
|
+
public_key_path: data.fetch("key").fetch("public_key_path"), config_path: data.fetch("key").fetch("configuration_path"))
|
|
65
|
+
key = keys.selected
|
|
66
|
+
process = Molecules::InboxContextNativeProcess.new(configuration: selected)
|
|
67
|
+
process.verify!
|
|
68
|
+
inbox = Inbox.new(deliveries_dir: data.fetch("deliveries_dir"),
|
|
69
|
+
executor: Molecules::HerdrExecutor.new(binary: clients.fetch("herdr").fetch("path"), process: process),
|
|
70
|
+
native: Molecules::NativeQueueExecutor.new(pi_client: clients.fetch("pi").fetch("path"), process: process))
|
|
71
|
+
store = Molecules::InboxContextStore.new(root: data.fetch("state_root"), uid: data.fetch("owner_credentials").fetch("uid"))
|
|
72
|
+
owner = InboxContextOwner.new(context_id: data.fetch("inbox_context_id"), deliveries_dir: data.fetch("deliveries_dir"),
|
|
73
|
+
grants: data.fetch("grants"), store: store, keys: keys, epoch: epoch, kernel: association.fetch(:kernel), inbox: inbox,
|
|
74
|
+
completion: Molecules::InboxContextCompletionClient.new(authority: data.fetch("authority"),
|
|
75
|
+
project_id: data.fetch("project_id"), mapping_id: data.fetch("native_mapping_id"), kernel: association.fetch(:kernel)))
|
|
76
|
+
if initialize_context
|
|
77
|
+
begin
|
|
78
|
+
owner.provision!
|
|
79
|
+
rescue Molecules::InboxContextStore::AlreadyProvisioned
|
|
80
|
+
owner.observe_initialized_epoch!
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
owner.activate_epoch!
|
|
84
|
+
end
|
|
85
|
+
server = InboxContextServer.new(owner: owner, context_id: data.fetch("inbox_context_id"), kernel: association.fetch(:kernel))
|
|
86
|
+
listener = InboxContextListener.new(configuration: selected, server: server, kernel: association.fetch(:kernel))
|
|
87
|
+
constructed = new(configuration: selected, epoch: epoch, association: association, listener: listener, store: store)
|
|
88
|
+
rescue Exception
|
|
89
|
+
store&.close
|
|
90
|
+
raise
|
|
91
|
+
end
|
|
92
|
+
# The initial owner completes its bounded existing Installer phase
|
|
93
|
+
# before returning this prepared instance. It cannot retain operator
|
|
94
|
+
# exclusion through the listener's unbounded serving lifetime.
|
|
95
|
+
unless prepared.is_a?(self) && prepared.configuration.reference == configuration.reference
|
|
96
|
+
raise ValidationError, "context service startup acknowledgement differs"
|
|
97
|
+
end
|
|
98
|
+
prepared.serve
|
|
99
|
+
ensure
|
|
100
|
+
constructed&.close if constructed.is_a?(self)
|
|
101
|
+
end
|
|
102
|
+
private_class_method :run!
|
|
103
|
+
private_class_method :new
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../molecules/inbox_direct_effect_binding"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Herdr
|
|
6
|
+
module Organisms
|
|
7
|
+
# DeliveryRecord owns notification intent; native IO is outside its lock.
|
|
8
|
+
module InboxGuardedWake
|
|
9
|
+
def guarded_wake_pending?(event:, binding:, claim:)
|
|
10
|
+
with_event(event, create_lock: false) do |record|
|
|
11
|
+
next false unless record&.state == "delivered"
|
|
12
|
+
guarded_queue_record!(record, binding, claim)
|
|
13
|
+
%w[pending not_issued].include?(record.inbox.dig("wake", "status"))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def prepare_guarded_wake(event:, binding:, claim:, operation_id:)
|
|
18
|
+
with_event(event, create_lock: false) do |record|
|
|
19
|
+
next nil unless record&.state == "delivered"
|
|
20
|
+
guarded_queue_record!(record, binding, claim)
|
|
21
|
+
wake = record.inbox.fetch("wake")
|
|
22
|
+
next nil unless %w[pending not_issued].include?(wake.fetch("status"))
|
|
23
|
+
unless wake.values_at("operation_id", "input_sha256") == [operation_id, binding.fetch("input_sha256")]
|
|
24
|
+
raise ValidationError, "guarded wake invocation differs"
|
|
25
|
+
end
|
|
26
|
+
issuing = wake.reject { |key, _| key == "code" }.merge("status" => "issuing")
|
|
27
|
+
save(record.advance_inbox(state: "delivered", inbox: record.inbox.merge("wake" => issuing),
|
|
28
|
+
detail: {"action" => "wake-issuing"}, timestamp: Time.now.utc.iso8601))
|
|
29
|
+
issuing
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def finish_guarded_wake(event:, binding:, claim:, issuing:, result:)
|
|
34
|
+
with_event(event, create_lock: false) do |record|
|
|
35
|
+
guarded_queue_record!(record, binding, claim)
|
|
36
|
+
unless record.state == "delivered" && record.inbox.fetch("wake") == issuing && issuing.fetch("status") == "issuing"
|
|
37
|
+
raise ValidationError, "guarded wake completion changed"
|
|
38
|
+
end
|
|
39
|
+
status = case result.fetch("outcome")
|
|
40
|
+
when "submitted" then "sent"
|
|
41
|
+
when "not_issued" then "not_issued"
|
|
42
|
+
else "uncertain"
|
|
43
|
+
end
|
|
44
|
+
wake = issuing.merge("status" => status)
|
|
45
|
+
wake["code"] = result.fetch("code") if status == "not_issued"
|
|
46
|
+
record = record.advance_inbox(state: "delivered", inbox: record.inbox.merge("wake" => wake),
|
|
47
|
+
detail: {"action" => "wake-#{status}"}, timestamp: Time.now.utc.iso8601)
|
|
48
|
+
save(record)
|
|
49
|
+
public_record(record)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def verify_guarded_wake_admission!(binding:, claim:, operation_id:)
|
|
54
|
+
with_event(binding.fetch("event_id"), create_lock: false) do |record|
|
|
55
|
+
guarded_queue_record!(record, binding, claim)
|
|
56
|
+
unless claim.slice("claim_generation", "claim_owner") == record.inbox.slice("claim_generation", "claim_owner") &&
|
|
57
|
+
record.inbox.fetch("wake").values_at("operation_id", "input_sha256") == [operation_id, binding.fetch("input_sha256")]
|
|
58
|
+
raise ValidationError, "guarded wake admission association differs"
|
|
59
|
+
end
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def queue_issuer!(record)
|
|
67
|
+
issuer = record.inbox.fetch("queue_issuer")
|
|
68
|
+
generation = record.inbox.fetch("claim_generation")
|
|
69
|
+
unless issuer.is_a?(Hash) && issuer.keys.sort == %w[input_sha256 key_generation operation_id] &&
|
|
70
|
+
issuer["operation_id"].is_a?(String) && issuer["operation_id"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
71
|
+
issuer["key_generation"].is_a?(Integer) && issuer["key_generation"].positive? && generation.positive?
|
|
72
|
+
raise ValidationError, "guarded queue issuer differs"
|
|
73
|
+
end
|
|
74
|
+
original = record.inbox.fetch("original_context")
|
|
75
|
+
expected = Molecules::InboxDirectEffectBinding.build(purpose: "deliver", event_id: record.event_id,
|
|
76
|
+
attempt_id: record.inbox.fetch("attempt_id"), key_generation: issuer.fetch("key_generation"),
|
|
77
|
+
selection: {"expected_claim_generation" => generation - 1}, original: original)
|
|
78
|
+
owner = Digest::SHA256.hexdigest(JSON.generate([original.fetch("inbox_context_id"), issuer.fetch("operation_id")]))
|
|
79
|
+
retained_owner = record.inbox["claim_owner"]
|
|
80
|
+
unless issuer.fetch("input_sha256") == expected.fetch("input_sha256") &&
|
|
81
|
+
retained_owner == owner &&
|
|
82
|
+
record.history.count { |entry| entry.slice("action", "claim_generation", "claim_owner") ==
|
|
83
|
+
{"action" => "claim", "claim_generation" => generation, "claim_owner" => owner} } == 1
|
|
84
|
+
raise ValidationError, "guarded queue claim attribution differs"
|
|
85
|
+
end
|
|
86
|
+
issuer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def guarded_queue_record!(record, binding, claim)
|
|
90
|
+
issuer = queue_issuer!(record)
|
|
91
|
+
receipt = record.inbox.fetch("receipt")
|
|
92
|
+
unless binding.fetch("purpose") == "deliver" && binding.fetch("event_id") == record.event_id &&
|
|
93
|
+
record.inbox.fetch("original_context") == binding.slice(*Molecules::InboxDirectEffectBinding::ORIGINAL_FIELDS) &&
|
|
94
|
+
record.inbox.fetch("attempt_id") == binding.fetch("attempt_id") &&
|
|
95
|
+
claim.slice("claim_generation", "claim_owner") == record.inbox.slice("claim_generation", "claim_owner") &&
|
|
96
|
+
receipt.is_a?(Hash) && receipt.slice("event_id", "attempt_id", "claim_generation", "payload_sha256", "binding") ==
|
|
97
|
+
{"event_id" => record.event_id, "attempt_id" => record.inbox.fetch("attempt_id"), "claim_generation" => record.inbox.fetch("claim_generation"),
|
|
98
|
+
"payload_sha256" => record.answer_digest, "binding" => record.inbox.fetch("binding")}
|
|
99
|
+
raise ValidationError, "guarded wake queue receipt differs"
|
|
100
|
+
end
|
|
101
|
+
wake = record.inbox.fetch("wake")
|
|
102
|
+
fields = %w[binding_digest claim_generation input_sha256 operation_id status]
|
|
103
|
+
fields << "code" if wake.is_a?(Hash) && wake["status"] == "not_issued"
|
|
104
|
+
unless wake.is_a?(Hash) && wake.keys.sort == fields.sort &&
|
|
105
|
+
%w[pending issuing sent not_issued uncertain none].include?(wake["status"]) &&
|
|
106
|
+
wake["claim_generation"] == record.inbox.fetch("claim_generation") && wake["binding_digest"] == record.inbox.fetch("original_binding_digest") &&
|
|
107
|
+
wake["operation_id"].is_a?(String) && wake["operation_id"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
108
|
+
wake["input_sha256"].is_a?(String) && wake["input_sha256"].match?(/\A[0-9a-f]{64}\z/) &&
|
|
109
|
+
(wake["status"] != "not_issued" || %w[guard_mismatch origin_unavailable origin_exited agent_blocked agent_not_ready target_missing submission_busy].include?(wake["code"]))
|
|
110
|
+
raise ValidationError, "guarded wake retention differs"
|
|
111
|
+
end
|
|
112
|
+
issuer
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require_relative "../molecules/native_source"
|
|
6
|
+
require_relative "../molecules/bounded_process"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module Organisms
|
|
11
|
+
# Explicit source build, outside the installed protected deployment.
|
|
12
|
+
# No server contact, product execution, global tool selection or publication.
|
|
13
|
+
class NativeSourceBuilder
|
|
14
|
+
def initialize(selection: Molecules::NativeSource.new, runner: nil)
|
|
15
|
+
@selection = selection
|
|
16
|
+
@runner = runner || lambda do |argv, environment:, chdir:, timeout_s:|
|
|
17
|
+
Molecules::BoundedProcess.call(argv, environment: environment, chdir: chdir,
|
|
18
|
+
timeout_s: timeout_s, output_limit: 1_048_576)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build(source:, output:, target:)
|
|
23
|
+
raise Molecules::NativeSource::Error, "native source builds must run as a nonroot build owner" if Process.euid.zero?
|
|
24
|
+
selected = @selection.selection
|
|
25
|
+
unless selected.fetch("targets").include?(target)
|
|
26
|
+
raise Molecules::NativeSource::Error, "unsupported native source build target"
|
|
27
|
+
end
|
|
28
|
+
validate_native_host!(target)
|
|
29
|
+
source = File.expand_path(source)
|
|
30
|
+
output = File.expand_path(output)
|
|
31
|
+
raise Molecules::NativeSource::Error, "native output must be new" if File.exist?(output) || File.symlink?(output)
|
|
32
|
+
raise Molecules::NativeSource::Error, "native source directory is unavailable" unless File.directory?(source) && !File.symlink?(source)
|
|
33
|
+
@environment = build_environment
|
|
34
|
+
@git = executable("git")
|
|
35
|
+
unless git(source, "status", "--porcelain", "--untracked-files=all").empty?
|
|
36
|
+
raise Molecules::NativeSource::Error, "native source checkout must be clean"
|
|
37
|
+
end
|
|
38
|
+
git(source, "cat-file", "-e", "#{selected.fetch("baseline_commit")}^{commit}")
|
|
39
|
+
tools = selected_tools
|
|
40
|
+
tool_digests = tool_digests(tools)
|
|
41
|
+
# Exclusive persistent output claim: failures never overwrite an older
|
|
42
|
+
# build or return an incomplete artifact as successful.
|
|
43
|
+
Dir.mkdir(output, 0o700)
|
|
44
|
+
staging = File.join(output, ".source-build")
|
|
45
|
+
Dir.mkdir(staging, 0o700)
|
|
46
|
+
checkout = File.join(staging, "source")
|
|
47
|
+
run([@git, "-c", "core.hooksPath=/dev/null", "clone", "--no-local", "--no-hardlinks", "--no-checkout", "--", source, checkout])
|
|
48
|
+
git(checkout, "checkout", "--detach", selected.fetch("baseline_commit"))
|
|
49
|
+
git(checkout, "am", "--committer-date-is-author-date", @selection.patch_path)
|
|
50
|
+
unless git(checkout, "rev-parse", "HEAD").strip == selected.fetch("source_commit") &&
|
|
51
|
+
git(checkout, "rev-parse", "HEAD^{tree}").strip == selected.fetch("source_tree")
|
|
52
|
+
raise Molecules::NativeSource::Error, "native patched source revision mismatch"
|
|
53
|
+
end
|
|
54
|
+
verify_source_files(checkout)
|
|
55
|
+
reject_ancestor_cargo_configuration(checkout)
|
|
56
|
+
cargo_home = File.join(staging, "cargo-home")
|
|
57
|
+
Dir.mkdir(cargo_home, 0o700)
|
|
58
|
+
compile_environment = @environment.merge("CARGO_HOME" => cargo_home,
|
|
59
|
+
"CARGO_TARGET_DIR" => File.join(staging, "target"), "RUSTC" => tools.fetch("rustc"),
|
|
60
|
+
"RUSTUP_TOOLCHAIN" => selected.fetch("rust_version"), "ZIG" => tools.fetch("zig"),
|
|
61
|
+
"CARGO_TARGET_#{target.upcase.tr("-", "_")}_LINKER" => tools.fetch("linker"))
|
|
62
|
+
run([tools.fetch("cargo"), "build", "--release", "--locked", "--target", target],
|
|
63
|
+
chdir: checkout, environment: compile_environment, timeout_s: 1800)
|
|
64
|
+
verify_source_files(checkout)
|
|
65
|
+
unless tool_digests(tools) == tool_digests
|
|
66
|
+
raise Molecules::NativeSource::Error, "native build tools changed during compilation"
|
|
67
|
+
end
|
|
68
|
+
artifact = @selection.read_regular(File.join(staging, "target", target, "release", "herdr"),
|
|
69
|
+
Molecules::NativeSource::MAX_ARTIFACT_BYTES)
|
|
70
|
+
receipt = @selection.receipt(build: {"target" => target, "profile" => "release",
|
|
71
|
+
"rust_version" => selected.fetch("rust_version"), "zig_version" => selected.fetch("zig_version"),
|
|
72
|
+
"tools_sha256" => tool_digests},
|
|
73
|
+
artifact: @selection.artifact_record(bytes: artifact, target: target))
|
|
74
|
+
write_new(File.join(output, "herdr"), artifact, 0o755)
|
|
75
|
+
FileUtils.remove_entry(staging)
|
|
76
|
+
write_new(File.join(output, "provenance.json"), JSON.pretty_generate(receipt) + "\n", 0o644)
|
|
77
|
+
@selection.verify_artifact(directory: output)
|
|
78
|
+
rescue SystemCallError, IOError, Timeout::Error, Molecules::BoundedProcess::PostLaunchError => error
|
|
79
|
+
raise Molecules::NativeSource::Error, "native source build failed: #{error.class}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def validate_native_host!(target)
|
|
85
|
+
architecture = {"x86_64-unknown-linux-musl" => /\A(?:x86_64|amd64)\z/,
|
|
86
|
+
"aarch64-unknown-linux-musl" => /\A(?:aarch64|arm64)\z/}.fetch(target)
|
|
87
|
+
unless RbConfig::CONFIG.fetch("host_os").include?("linux") &&
|
|
88
|
+
architecture.match?(RbConfig::CONFIG.fetch("host_cpu"))
|
|
89
|
+
raise Molecules::NativeSource::Error, "native source builds require a matching Linux host; cross builds are unsupported"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_environment
|
|
94
|
+
selected = @selection.selection
|
|
95
|
+
committer = selected.fetch("committer")
|
|
96
|
+
environment = {"PATH" => ENV.fetch("PATH", "/usr/bin:/bin"), "HOME" => Dir.home,
|
|
97
|
+
"GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_GLOBAL" => "/dev/null", "GIT_TERMINAL_PROMPT" => "0",
|
|
98
|
+
"GIT_COMMITTER_NAME" => committer.fetch("name"), "GIT_COMMITTER_EMAIL" => committer.fetch("email"),
|
|
99
|
+
"GIT_COMMITTER_DATE" => committer.fetch("date")}
|
|
100
|
+
environment["RUSTUP_HOME"] = ENV["RUSTUP_HOME"] if ENV["RUSTUP_HOME"] && !ENV["RUSTUP_HOME"].empty?
|
|
101
|
+
environment
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def executable(name)
|
|
105
|
+
path = @environment.fetch("PATH").split(File::PATH_SEPARATOR).map { |directory| File.join(directory, name) }
|
|
106
|
+
.find { |candidate| File.file?(candidate) && File.executable?(candidate) }
|
|
107
|
+
raise Molecules::NativeSource::Error, "required native build tool is missing: #{name}" unless path
|
|
108
|
+
File.realpath(path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def tool_digests(tools)
|
|
112
|
+
tools.transform_values do |path|
|
|
113
|
+
@selection.digest(@selection.read_regular(path, Molecules::NativeSource::MAX_ARTIFACT_BYTES))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def reject_ancestor_cargo_configuration(checkout)
|
|
118
|
+
current = File.dirname(checkout)
|
|
119
|
+
loop do
|
|
120
|
+
if %w[config config.toml].any? { |file| File.exist?(File.join(current, ".cargo", file)) || File.symlink?(File.join(current, ".cargo", file)) }
|
|
121
|
+
raise Molecules::NativeSource::Error, "native build has undeclared ancestor Cargo configuration"
|
|
122
|
+
end
|
|
123
|
+
parent = File.dirname(current)
|
|
124
|
+
break if parent == current
|
|
125
|
+
current = parent
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def selected_tools
|
|
130
|
+
rustup = executable("rustup")
|
|
131
|
+
version = @selection.selection.fetch("rust_version")
|
|
132
|
+
tools = %w[cargo rustc].to_h do |name|
|
|
133
|
+
path = run([rustup, "which", "--toolchain", version, name]).strip
|
|
134
|
+
unless path.start_with?("/") && File.file?(path) && File.executable?(path)
|
|
135
|
+
raise Molecules::NativeSource::Error, "selected Rust tool is unavailable"
|
|
136
|
+
end
|
|
137
|
+
[name, File.realpath(path)]
|
|
138
|
+
end
|
|
139
|
+
tools["zig"] = executable("zig")
|
|
140
|
+
tools["linker"] = executable("musl-gcc")
|
|
141
|
+
unless run([tools.fetch("rustc"), "--version"]).start_with?("rustc #{version} ") &&
|
|
142
|
+
run([tools.fetch("cargo"), "--version"]).start_with?("cargo #{version} ") &&
|
|
143
|
+
run([tools.fetch("zig"), "version"]).strip == @selection.selection.fetch("zig_version")
|
|
144
|
+
raise Molecules::NativeSource::Error, "selected native build tool versions mismatch"
|
|
145
|
+
end
|
|
146
|
+
tools
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def git(checkout, *arguments)
|
|
150
|
+
run([@git, "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", "-C", checkout, *arguments])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def verify_source_files(checkout)
|
|
154
|
+
%w[Cargo.lock rust-toolchain.toml].zip(%w[cargo_lock_sha256 rust_toolchain_sha256]).each do |file, key|
|
|
155
|
+
unless @selection.digest(@selection.read_regular(File.join(checkout, file), 1_048_576)) == @selection.selection.fetch(key)
|
|
156
|
+
raise Molecules::NativeSource::Error, "native source lock/toolchain bytes mismatch"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
unless git(checkout, "status", "--porcelain", "--untracked-files=all").empty?
|
|
160
|
+
raise Molecules::NativeSource::Error, "native source changed during build"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def run(argv, chdir: nil, environment: @environment, timeout_s: 60)
|
|
165
|
+
result = @runner.call(argv, environment: environment, chdir: chdir, timeout_s: timeout_s)
|
|
166
|
+
unless result.status.success? && !result.oversized
|
|
167
|
+
raise Molecules::NativeSource::Error, "native build command failed or exceeded output bound"
|
|
168
|
+
end
|
|
169
|
+
result.stdout
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def write_new(path, bytes, mode)
|
|
173
|
+
File.open(path, File::WRONLY | File::CREAT | File::EXCL, mode) do |file|
|
|
174
|
+
file.write(bytes)
|
|
175
|
+
file.flush
|
|
176
|
+
file.fsync
|
|
177
|
+
file.chmod(mode)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/ace/herdr/version.rb
CHANGED
data/lib/ace/herdr.rb
CHANGED
|
@@ -3,34 +3,6 @@
|
|
|
3
3
|
require_relative "herdr/version"
|
|
4
4
|
require "ace/support/config"
|
|
5
5
|
|
|
6
|
-
module Ace
|
|
7
|
-
module Herdr
|
|
8
|
-
class Error < StandardError; end
|
|
9
|
-
|
|
10
|
-
# Raised when the caller's herdr workspace/session/pane cannot be resolved
|
|
11
|
-
class TargetResolutionError < Error; end
|
|
12
|
-
|
|
13
|
-
# Raised when arguments or refs fail validation
|
|
14
|
-
class ValidationError < Error; end
|
|
15
|
-
|
|
16
|
-
# Raised when a bounded wait exceeds its timeout
|
|
17
|
-
class WaitTimeoutError < Error; end
|
|
18
|
-
|
|
19
|
-
# Raised when a preset tab was created natively but a later
|
|
20
|
-
# materialization step (splits, commands, agents) failed; carries the
|
|
21
|
-
# created tab id so the owner can roll back exactly that tab.
|
|
22
|
-
class TabMaterializationError < Error
|
|
23
|
-
attr_reader :tab_id
|
|
24
|
-
|
|
25
|
-
def initialize(tab_id:, message:)
|
|
26
|
-
@tab_id = tab_id
|
|
27
|
-
super(message)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Executor error hierarchy (subclasses Ace::Herdr::Error)
|
|
34
6
|
require_relative "herdr/errors"
|
|
35
7
|
|
|
36
8
|
# Load all ace-herdr components
|
|
@@ -39,6 +11,8 @@ require_relative "herdr/atoms/json_find"
|
|
|
39
11
|
require_relative "herdr/models/delivery_record"
|
|
40
12
|
require_relative "herdr/models/dispatch_outcome"
|
|
41
13
|
require_relative "herdr/molecules/bounded_process"
|
|
14
|
+
require_relative "herdr/molecules/native_source"
|
|
15
|
+
require_relative "herdr/organisms/native_source_builder"
|
|
42
16
|
require_relative "herdr/molecules/herdr_executor"
|
|
43
17
|
require_relative "herdr/molecules/native_queue_executor"
|
|
44
18
|
require_relative "herdr/molecules/pane_tidy_probe"
|
|
@@ -47,6 +21,9 @@ require_relative "herdr/molecules/preset_loader"
|
|
|
47
21
|
require_relative "herdr/molecules/preset_resolver"
|
|
48
22
|
require_relative "herdr/organisms/deliverer"
|
|
49
23
|
require_relative "herdr/organisms/inbox"
|
|
24
|
+
require_relative "herdr/organisms/inbox_context_owner"
|
|
25
|
+
require_relative "herdr/organisms/inbox_context_server"
|
|
26
|
+
require_relative "herdr/molecules/inbox_context_client"
|
|
50
27
|
require_relative "herdr/organisms/dispatcher"
|
|
51
28
|
require_relative "herdr/organisms/control_surface"
|
|
52
29
|
require_relative "herdr/organisms/runtime_adapter"
|
metadata
CHANGED
|
@@ -1,42 +1,76 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-herdr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: websocket-driver
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.8.2
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.8.2
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fiddle
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.1'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.1'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
12
46
|
- !ruby/object:Gem::Dependency
|
|
13
47
|
name: ace-hitl-contract
|
|
14
48
|
requirement: !ruby/object:Gem::Requirement
|
|
15
49
|
requirements:
|
|
16
50
|
- - "~>"
|
|
17
51
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
52
|
+
version: 0.2.1
|
|
19
53
|
type: :runtime
|
|
20
54
|
prerelease: false
|
|
21
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
56
|
requirements:
|
|
23
57
|
- - "~>"
|
|
24
58
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
59
|
+
version: 0.2.1
|
|
26
60
|
- !ruby/object:Gem::Dependency
|
|
27
61
|
name: ace-runtime
|
|
28
62
|
requirement: !ruby/object:Gem::Requirement
|
|
29
63
|
requirements:
|
|
30
64
|
- - "~>"
|
|
31
65
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.
|
|
66
|
+
version: '0.3'
|
|
33
67
|
type: :runtime
|
|
34
68
|
prerelease: false
|
|
35
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
70
|
requirements:
|
|
37
71
|
- - "~>"
|
|
38
72
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.
|
|
73
|
+
version: '0.3'
|
|
40
74
|
- !ruby/object:Gem::Dependency
|
|
41
75
|
name: ace-support-cli
|
|
42
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -164,8 +198,8 @@ dependencies:
|
|
|
164
198
|
- !ruby/object:Gem::Version
|
|
165
199
|
version: '0.22'
|
|
166
200
|
description: Herdr agent orchestration with push delivery, agent bootstrap, dispatch,
|
|
167
|
-
and a durable attempt-linked inbox for
|
|
168
|
-
|
|
201
|
+
and a durable attempt-linked inbox for submission to the original Codex/Pi terminal,
|
|
202
|
+
with explicit uncertain outcomes. Implements the ace-hitl provider delivery contract.
|
|
169
203
|
email:
|
|
170
204
|
- mc@cs3b.com
|
|
171
205
|
executables:
|
|
@@ -193,6 +227,7 @@ files:
|
|
|
193
227
|
- lib/ace/herdr/cli/commands/inbox.rb
|
|
194
228
|
- lib/ace/herdr/cli/commands/list.rb
|
|
195
229
|
- lib/ace/herdr/cli/commands/list_presets.rb
|
|
230
|
+
- lib/ace/herdr/cli/commands/native_source.rb
|
|
196
231
|
- lib/ace/herdr/cli/commands/send.rb
|
|
197
232
|
- lib/ace/herdr/cli/commands/support.rb
|
|
198
233
|
- lib/ace/herdr/cli/commands/tab.rb
|
|
@@ -204,17 +239,41 @@ files:
|
|
|
204
239
|
- lib/ace/herdr/models/dispatch_outcome.rb
|
|
205
240
|
- lib/ace/herdr/molecules/bounded_process.rb
|
|
206
241
|
- lib/ace/herdr/molecules/delivery_record_store.rb
|
|
242
|
+
- lib/ace/herdr/molecules/guarded_native_origin.rb
|
|
207
243
|
- lib/ace/herdr/molecules/herdr_executor.rb
|
|
244
|
+
- lib/ace/herdr/molecules/inbox_cli_input.rb
|
|
245
|
+
- lib/ace/herdr/molecules/inbox_context_client.rb
|
|
246
|
+
- lib/ace/herdr/molecules/inbox_context_completion_client.rb
|
|
247
|
+
- lib/ace/herdr/molecules/inbox_context_key.rb
|
|
248
|
+
- lib/ace/herdr/molecules/inbox_context_lifetime.rb
|
|
249
|
+
- lib/ace/herdr/molecules/inbox_context_native_process.rb
|
|
250
|
+
- lib/ace/herdr/molecules/inbox_context_service_configuration.rb
|
|
251
|
+
- lib/ace/herdr/molecules/inbox_context_store.rb
|
|
252
|
+
- lib/ace/herdr/molecules/inbox_context_wire.rb
|
|
253
|
+
- lib/ace/herdr/molecules/inbox_direct_effect_binding.rb
|
|
254
|
+
- lib/ace/herdr/molecules/inbox_direct_result.rb
|
|
208
255
|
- lib/ace/herdr/molecules/native_queue_executor.rb
|
|
256
|
+
- lib/ace/herdr/molecules/native_source.rb
|
|
209
257
|
- lib/ace/herdr/molecules/pane_tidy_probe.rb
|
|
210
258
|
- lib/ace/herdr/molecules/preset_loader.rb
|
|
211
259
|
- lib/ace/herdr/molecules/preset_resolver.rb
|
|
260
|
+
- lib/ace/herdr/molecules/protected_inbox_selection.rb
|
|
212
261
|
- lib/ace/herdr/molecules/protected_native_control.rb
|
|
262
|
+
- lib/ace/herdr/native_source/HERDR-LICENSE
|
|
263
|
+
- lib/ace/herdr/native_source/guarded-prompt.patch
|
|
264
|
+
- lib/ace/herdr/native_source/selection.json
|
|
213
265
|
- lib/ace/herdr/organisms/control_surface.rb
|
|
214
266
|
- lib/ace/herdr/organisms/deliverer.rb
|
|
215
267
|
- lib/ace/herdr/organisms/dispatcher.rb
|
|
216
268
|
- lib/ace/herdr/organisms/inbox.rb
|
|
217
|
-
- lib/ace/herdr/organisms/
|
|
269
|
+
- lib/ace/herdr/organisms/inbox_context_direct_effects.rb
|
|
270
|
+
- lib/ace/herdr/organisms/inbox_context_effects.rb
|
|
271
|
+
- lib/ace/herdr/organisms/inbox_context_listener.rb
|
|
272
|
+
- lib/ace/herdr/organisms/inbox_context_owner.rb
|
|
273
|
+
- lib/ace/herdr/organisms/inbox_context_server.rb
|
|
274
|
+
- lib/ace/herdr/organisms/inbox_context_service.rb
|
|
275
|
+
- lib/ace/herdr/organisms/inbox_guarded_wake.rb
|
|
276
|
+
- lib/ace/herdr/organisms/native_source_builder.rb
|
|
218
277
|
- lib/ace/herdr/organisms/runtime_adapter.rb
|
|
219
278
|
- lib/ace/herdr/organisms/tidy.rb
|
|
220
279
|
- lib/ace/herdr/version.rb
|