ace-herdr 0.4.0 → 0.5.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/herdr/config.yml +0 -4
  3. data/CHANGELOG.md +12 -0
  4. data/docs/usage.md +57 -35
  5. data/lib/ace/herdr/cli/commands/inbox.rb +83 -21
  6. data/lib/ace/herdr/cli/commands/native_source.rb +56 -0
  7. data/lib/ace/herdr/cli.rb +3 -0
  8. data/lib/ace/herdr/errors.rb +23 -0
  9. data/lib/ace/herdr/molecules/bounded_process.rb +261 -52
  10. data/lib/ace/herdr/molecules/delivery_record_store.rb +81 -6
  11. data/lib/ace/herdr/molecules/guarded_native_origin.rb +59 -0
  12. data/lib/ace/herdr/molecules/herdr_executor.rb +6 -3
  13. data/lib/ace/herdr/molecules/inbox_cli_input.rb +36 -0
  14. data/lib/ace/herdr/molecules/inbox_context_client.rb +88 -0
  15. data/lib/ace/herdr/molecules/inbox_context_completion_client.rb +119 -0
  16. data/lib/ace/herdr/molecules/inbox_context_key.rb +63 -0
  17. data/lib/ace/herdr/molecules/inbox_context_lifetime.rb +122 -0
  18. data/lib/ace/herdr/molecules/inbox_context_native_process.rb +92 -0
  19. data/lib/ace/herdr/molecules/inbox_context_service_configuration.rb +183 -0
  20. data/lib/ace/herdr/molecules/inbox_context_store.rb +159 -0
  21. data/lib/ace/herdr/molecules/inbox_context_wire.rb +76 -0
  22. data/lib/ace/herdr/molecules/inbox_direct_effect_binding.rb +92 -0
  23. data/lib/ace/herdr/molecules/inbox_direct_result.rb +65 -0
  24. data/lib/ace/herdr/molecules/native_queue_executor.rb +10 -21
  25. data/lib/ace/herdr/molecules/native_source.rb +189 -0
  26. data/lib/ace/herdr/molecules/protected_inbox_selection.rb +126 -0
  27. data/lib/ace/herdr/molecules/protected_native_control.rb +129 -4
  28. data/lib/ace/herdr/native_source/HERDR-LICENSE +201 -0
  29. data/lib/ace/herdr/native_source/guarded-prompt.patch +3493 -0
  30. data/lib/ace/herdr/native_source/selection.json +24 -0
  31. data/lib/ace/herdr/organisms/inbox.rb +293 -203
  32. data/lib/ace/herdr/organisms/inbox_context_direct_effects.rb +285 -0
  33. data/lib/ace/herdr/organisms/inbox_context_effects.rb +30 -0
  34. data/lib/ace/herdr/organisms/inbox_context_listener.rb +151 -0
  35. data/lib/ace/herdr/organisms/inbox_context_owner.rb +550 -0
  36. data/lib/ace/herdr/organisms/inbox_context_server.rb +70 -0
  37. data/lib/ace/herdr/organisms/inbox_context_service.rb +107 -0
  38. data/lib/ace/herdr/organisms/inbox_guarded_wake.rb +117 -0
  39. data/lib/ace/herdr/organisms/native_source_builder.rb +183 -0
  40. data/lib/ace/herdr/version.rb +1 -1
  41. data/lib/ace/herdr.rb +5 -28
  42. metadata +64 -5
  43. 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Herdr
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
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,14 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-herdr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-05 00:00:00.000000000 Z
10
+ date: 2026-10-08 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
@@ -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 exact-session Codex/Pi queue submission and
168
- signed reconciliation. Implements the ace-hitl provider delivery contract.
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/protected_inbox.rb
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
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "inbox"
4
- require_relative "../molecules/protected_native_control"
5
- require_relative "../molecules/herdr_executor"
6
- require_relative "../molecules/native_queue_executor"
7
- require_relative "../molecules/bounded_process"
8
-
9
- module Ace
10
- module Herdr
11
- module Organisms
12
- # Fixed construction for the canonical authority consumer. Callers of the
13
- # public protocol cannot inject this factory or its collaborators.
14
- module ProtectedInbox
15
- class PaneReader
16
- def initialize(control)
17
- @control = control
18
- end
19
-
20
- def pane_get_bounded(pane)
21
- result = @control.request("pane.get", {"pane_id" => pane})
22
- Molecules::ExecutionResult.new(stdout: JSON.generate({"result" => result}), stderr: "",
23
- success: true, exit_code: 0)
24
- rescue Ace::Runtime::RuntimeUnavailableError
25
- raise ExecutorUnavailableError, "original protected native endpoint is unavailable"
26
- end
27
- end
28
-
29
- class PiIdentity
30
- def initialize(client)
31
- runner = lambda do |argv, stdin_data:, timeout_s:|
32
- unless argv == [client, "--identity"] && stdin_data.empty?
33
- raise ExecutorUnavailableError, "protected inbox permits identity inspection only"
34
- end
35
- result = Molecules::BoundedProcess.call(argv, stdin_data: stdin_data,
36
- timeout_s: timeout_s, environment: {})
37
- raise ExecutorError, "Pi identity output is oversized" if result.oversized
38
- [result.stdout, result.stderr, result.status]
39
- end
40
- @native = Molecules::NativeQueueExecutor.new(pi_client: client, runner: runner)
41
- end
42
-
43
- def pi_identity
44
- @native.pi_identity
45
- end
46
- end
47
-
48
- module_function
49
-
50
- # The Assign resolver has verified context paths/key/client. native is
51
- # the immutable canonical stage, not current process discovery. Endpoint
52
- # liveness is checked lazily only if Inbox actually observes replacement.
53
- def build(context:, mapping:, native:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new)
54
- native_fields = %w[server_identity socket_identity workspace_id]
55
- unless native.is_a?(Hash) && native_fields.all? { |key| native.key?(key) }
56
- raise ValidationError, "canonical native stage is unavailable"
57
- end
58
- fixed = JSON.parse(JSON.generate(mapping))
59
- fixed["native"].merge!(native.slice(*native_fields))
60
- control = Molecules::ProtectedNativeControl.new(mapping: fixed, kernel: kernel)
61
- key = OpenSSL::PKey.read(File.binread(context.fetch("receipt_public_key")))
62
- unless key.is_a?(OpenSSL::PKey::RSA) && !key.private?
63
- raise ValidationError, "protected inbox requires a public RSA key"
64
- end
65
- Inbox.new(executor: PaneReader.new(control), native: PiIdentity.new(context.fetch("pi_queue_client")),
66
- deliveries_dir: context.fetch("deliveries_dir"), receipt_public_key: key)
67
- rescue KeyError, SystemCallError, OpenSSL::PKey::PKeyError
68
- raise ValidationError, "protected inbox fixed context is unavailable"
69
- end
70
- end
71
- end
72
- end
73
- end