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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/herdr/config.yml +0 -4
  3. data/CHANGELOG.md +18 -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 +68 -9
  43. data/lib/ace/herdr/organisms/protected_inbox.rb +0 -73
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e0747847d9a56e36a452fd156063284154be0c5eb6464b9acca559fba504750
4
- data.tar.gz: 2dd3be6a92a0217c2ec0d194a83714c0b6cbc22856c4df0ba7bb99f4f0a02810
3
+ metadata.gz: 00422d9f6820e48b0ea45698e4663f7ecaeb9b5b01ddf1626ab86aed50adcbfd
4
+ data.tar.gz: 2632e4056271d3c022056a70fa5f3b95e2f69ae08544bf70f2577bc11fbac4f7
5
5
  SHA512:
6
- metadata.gz: dc0422a8f818acfb8ad055543ef95938bd41bc988622e527dcffccbf0e757456f4b9aef47ab141eda71eb97fd1d55311d836b2f0f6fc2f04d6ac2f50f56a5c80
7
- data.tar.gz: 98c840541c5d15bc6241f1a7bf0535567b34386fe441fdb14c97be8d1a98413a9777151807d4f142504d725871905b77144d89a2622b64f32e85e384b74daaa1
6
+ metadata.gz: c6626094540195dadf06116add8bd27708934b809347c12cb0bc355cb6c03d5ff32e30e3e63870e158316cf9c80de7e2bdd5cad5f731cea4fef11303c1c9bd8a
7
+ data.tar.gz: 7b4e02b683be089a602d10c7fafcb09abc134a9f5b60e5a58a8d0ef42b35bb0ec18555700e0c316ec5de12aae7e5c01a4f0ffede12e26597f1a881fcbb5d50af
@@ -19,10 +19,6 @@ timeouts:
19
19
  # Delivery record root (ADR-029 short-name layout), relative to project root
20
20
  deliveries_dir: .ace-local/herdr/deliveries
21
21
 
22
- # Absolute path to the supervisor-managed receipt verification key. Configure
23
- # before enqueue; each event pins this key's fingerprint for reconciliation.
24
- inbox_receipt_public_key:
25
-
26
22
  # Tidy (spec 8wq.t.1w0): archive delivered records strictly older than this
27
23
  # many days (age = record updated_at); failed/retryable/pending are never
28
24
  # touched
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.1] - 2026-10-10
11
+
12
+ ### Fixed
13
+
14
+ - Require the published producer and consumer patch versions containing the protected context and managed-attempt APIs; exclude incompatible prior dependency graphs.
15
+
16
+ ## [0.5.0] - 2026-10-08
17
+
18
+ ### Changed
19
+
20
+ - Submit Codex answers through the existing Herdr prompt command to the original live terminal. A successful send means submitted, not read; retain unknown sends without automatic resend.
21
+ - Remove dedicated Codex app-server selection, message-read observation, signing keys and consumed/superseded reconciliation APIs.
22
+ - Keep authenticated original-context admission and actual scoped effects without a second current-state projection.
23
+
24
+ ### Fixed
25
+
26
+ - Load bounded process/error primitives directly and preserve bounded cleanup of the original process after execution uncertainty.
27
+
10
28
  ## [0.4.0] - 2026-10-05
11
29
 
12
30
  ### Fixed
data/docs/usage.md CHANGED
@@ -14,7 +14,7 @@ ace-docs:
14
14
  ## Command Surface
15
15
 
16
16
  - `ace-herdr deliver [OPTIONS]`
17
- - `ace-herdr inbox enqueue|status|deliver|reconcile [OPTIONS]`
17
+ - `ace-herdr inbox enqueue|status|deliver [OPTIONS]`
18
18
  - `ace-herdr dispatch [OPTIONS]`
19
19
  - `ace-herdr list [--panes|--tabs|--workspaces] [--workspace ID] [--quiet]`
20
20
  - `ace-herdr send [--cmd TEXT] [--msg TEXT...] [--key NAME...] --pane ID [--quiet]`
@@ -53,7 +53,7 @@ remain valid; the codec also preserves paired native tmux IDs (`$0`/`%0`) when
53
53
  carried by managed metadata. Mixed native/token or cross-field IDs refuse.
54
54
  Wire and persisted references must be canonical. Syntax acceptance does not add
55
55
  tmux execution to this Herdr controller or replace its exact native ownership,
56
- target and signed receipt checks.
56
+ target and submission checks.
57
57
 
58
58
  ```bash
59
59
  ace-herdr inbox enqueue --event inb-12345678 --attempt ATTEMPT --ref ref.json --file prompt.txt
@@ -61,39 +61,22 @@ ace-herdr inbox status --event inb-12345678 --format json
61
61
  ace-herdr inbox deliver --event inb-12345678
62
62
  ```
63
63
 
64
- `enqueue` checks the live pane, durable terminal, agent kind, and native thread identity. Managed API callers must supply `expected_target`, projected from the accepted runtime owner with `Inbox#target_from_owner`; the live observation must equal that original session/pane/terminal/agent/thread identity under the event lock. The immutable `origin_target` survives retries and signed replacement of the current target. Managed payloads are checked for secret shapes before persistence, including envelopes without a nested message. Repeating the same event, attempt, target, and payload returns its existing record; a changed value is an error. `deliver` claims the record once. Idle and busy Codex or Pi agents receive an exact-session native queue submission. After an accepted idle submission, Herdr sends a generic wake prompt without the inbox payload. A pane replacement can receive at most that generic wake, never the payload. The JSON result includes `state`, `claim_generation`, `binding`, `last_error`, and any submission receipt.
64
+ `enqueue` checks the live pane, terminal and original agent-session identity.
65
+ Managed callers supply `expected_target` from the accepted runtime owner. Reusing
66
+ an event with identical attempt, target and payload returns its original record;
67
+ changed input refuses. `deliver` records one submission intent before sending.
65
68
 
66
- `queued` can be retried after a proven pre-submission rejection. `delivered` means native submission was accepted; it does not mean the agent consumed the message. A crash after claim, changed target identity, or a native result without a verified acceptance receipt is `uncertain`; another `deliver` call does not resend it. A `completed` record has a positively verified consumption receipt. A superseded uncertain record can return to `queued` only after positive nonconsumption proof.
69
+ Codex delivery uses the existing Herdr terminal prompt; Pi uses its maintained
70
+ queue boundary. `delivered` means the submission was accepted, not that the
71
+ agent read it or completed the task. A known pre-submission refusal stays queued.
72
+ A lost response, process interruption after intent, or changed target is uncertain;
73
+ a later `deliver` does not automatically resend or select a replacement terminal.
67
74
 
68
- ```bash
69
- ace-herdr inbox reconcile --event inb-12345678 --receipt proof.json
70
- ```
71
-
72
- The receipt is a file supplied from an operator or supervisor observation of the native outcome. Codex and Pi expose submission but no queryable consumption or eviction proof, so `reconcile` never polls the native queue or infers an outcome from age. Before enqueue, the supervisor sets `inbox_receipt_public_key` in `.ace/herdr/config.yml` to the absolute path of its trusted RSA public key. Each event pins that key's fingerprint; a later process cannot substitute a different key. Protect this configuration from delivery requesters. The receipt file must have a detached SHA-256 RSA signature at `FILE.sig`. The private key stays with the operator or supervisor. Its JSON must include the recorded event, attempt, claim generation, payload digest, and complete binding, plus an outcome (`consumed` or `superseded`), an identified observer, and a native observation reference:
73
-
74
- ```json
75
- {
76
- "event_id": "inb-12345678",
77
- "attempt_id": "ATTEMPT",
78
- "claim_generation": 1,
79
- "payload_sha256": "<digest from inbox status>",
80
- "binding": {"session": "<exact binding from inbox status>"},
81
- "outcome": "consumed",
82
- "observer": {"role": "operator", "id": "operator-id"},
83
- "evidence": {"kind": "consumed_acknowledged", "native_reference": "session-log:42", "observation": "message consumed and acknowledged"}
84
- }
85
- ```
86
-
87
- Copy the **entire** `binding` object from `inbox status`; the shortened object above only illustrates the field. `consumed` requires `evidence.kind: consumed_acknowledged`. `superseded` requires `queue_evicted`, `queue_expired`, or `thread_replaced`, with an observation that the old queue entry cannot be consumed. A matching receipt changes `uncertain` to `completed` for consumption, or to `queued` for proven supersession. The original target binding stays pinned. To authorize a replacement native session, include `replacement_target` in the signed receipt with the complete target identity from a fresh Herdr pane observation; reconciliation verifies its session, pane, terminal, agent, and thread against that live replacement address before saving it. Without that field, a new claim can only use the original target. A missing or mismatched receipt returns JSON with unchanged `state: uncertain` and `reconciliation_refusal`; it does not resend. Keep the observation record with the receipt. Signature validation authenticates the configured key; the operator or supervisor remains responsible for checking the cited native outcome before signing.
88
-
89
- After inspecting the native outcome and writing `proof.json`, the trusted operator signs the exact bytes:
90
-
91
- ```bash
92
- openssl dgst -sha256 -sign operator-private.pem -out proof.json.sig proof.json
93
- ace-herdr inbox reconcile --event inb-12345678 --receipt proof.json
94
- ```
95
-
96
- The command verifies the detached signature against the configured public key before any transition. Missing, malformed, or self-signed receipts leave the event uncertain and return a machine-readable refusal. Keep the signed receipt and source observation for audit.
75
+ Inspect current progress directly through `ace-herdr capture` or the native pane.
76
+ Inbox status is transport history, not a cached projection of current agent state.
77
+ No agent-read proof, signing key, reconciliation command or dedicated Codex
78
+ app-server is required. Actual task results and scoped service effects retain
79
+ their independent authorization and verification.
97
80
 
98
81
  ## tmux-intent ↔ herdr-command parity
99
82
 
@@ -372,6 +355,45 @@ Commands raise a CLI error (non-zero exit) carrying herdr's machine code where o
372
355
 
373
356
  Exit `0` means success (including an explicit empty `list` result or a satisfied wait).
374
357
 
375
- An identical signed reconciliation receipt may be verified again after a consumed/superseded settlement, without another transition. This lets an assignment consumer recover a crash before journaling its verified observation reference. Re-verification preserves the original pinned key, complete binding and generation checks; a later claim's generation rejects the old proof. It never resubmits the payload. Runtime recovery additionally exposes the read-only `process_binding(pane:, caller_pid:)` adapter operation, which requires OS ancestry under the native shell, a matching foreground owner and immutable agent-session identity.
358
+ Runtime recovery exposes the read-only `process_binding(pane:, caller_pid:)`
359
+ adapter operation, which checks OS ancestry, foreground ownership and the
360
+ original agent-session identity. Message submission is never task completion.
361
+
362
+ ## Packaged guarded native source
363
+
364
+ `ace-herdr native-source selection` describes the source selection and patch shipped by this gem. These assets live under `lib/ace/herdr/native_source`; installed consumers do not need task directories. This source retains native version 0.9.3 and protocol 22; its exact source commit and patch digest identify the guarded producer. No upstream release or publication is implied.
365
+
366
+ An explicit nonroot build owner can run:
367
+
368
+ ```sh
369
+ ace-herdr native-source build --source /absolute/clean/local/herdr-checkout --output /absolute/new/artifact-directory --target x86_64-unknown-linux-musl
370
+ ace-herdr native-source verify --artifact /absolute/artifact-directory
371
+ ```
372
+
373
+ The build requires a matching native Linux host (x86_64 or aarch64), the selected Rust and Zig versions, and a native `musl-gcc` toolchain already provisioned by the build owner. Cross builds are unsupported. The output must be new. The builder reproduces the exact baseline plus shipped patch in an isolated checkout, checks the commit/tree/lock/toolchain bytes, pins resolved Rust/Zig/linker paths, invokes the locked release build, and records observed executable/tool digests. It never runs the resulting Herdr binary, changes global tool defaults, contacts a running Herdr server, or publishes an artifact. Locked dependency fetching by Cargo may require network access. Failed builds retain their output claim; a partial directory does not verify.
374
+
375
+ Verification checks bounded strict provenance against this gem's source selection and the observed ELF bytes/architecture. It provides byte provenance, not deployment authorization. The trusted Lab deployment owner separately selects the gem, source selection, artifact and receipt digests before installing the fixed Lab binary. Protected runtime replacement must also use the canonical maintenance admission/release owner. That operational installer join remains required source work; these commands alone do not activate the producer or complete installed acceptance.
376
+
377
+ The selected source also provides `terminal.inhibit_input` for an exact original terminal/origin. Its successful `terminal_input_drained` reply proves permanent input inhibition and zero queued input, including terminal replies; it does not prove child exit or canonical scope cleanup. Lost acknowledgement may be retried only against the same original actor. Source fixtures validate this contract without running a native binary; actual Lab installation and N2 consumption remain required.
378
+
379
+ The inbox context source owner supplies durable operation admission and exclusive key rotation through a fixed authenticated exchange. A lost begin reply recovers the same original process's grant; unknown effects, owner restart and incomplete installation retain blocking state. Replacement and rollback each require an attestation from the actual configured signer plus protected public/config readback. This core does not yet activate a context CLI/server, migrate Assign/private-store consumers, or authorize orphan reclamation. Existing protected mutations must all be migrated before this becomes an accepted end-to-end rotation path; installation and private-key pair handling remain the trusted provisioner's responsibility.
380
+
381
+ ### Trusted bounded descriptor handoff
382
+
383
+ Internal source installers using `BoundedProcess.call` may supply
384
+ `descriptor_mapping: {4 => authenticated_readonly_file}` together with bounded
385
+ `stdin_data` carrying exact selected source bytes. The mapping has at most16
386
+ entries; child descriptor keys are strict Integers3..63 and handles must be live
387
+ read-only regular files. Stdio overwrite, writable/closed/nonregular handles and
388
+ ambient descriptor/path fallback refuse before spawn. Standard Open3 spawn maps
389
+ only explicit descriptors; `close_others: true` excludes all other descriptors.
390
+ The supplying verification owner keeps the handle scoped and authenticates the
391
+ selected inode; the runner does not confer authority or authenticate arbitrary
392
+ files. Actual installed privilege-transition/PAM preservation is a separate
393
+ installed acceptance check, not established by controlled file/child tests.
394
+
395
+ ### Bounded subprocess cleanup
396
+
397
+ `BoundedProcess.call` retains capped stdout/stderr and uses an execution deadline plus at most one additional second of cleanup. `stderr_limit:` defaults to `output_limit:`. Unconfirmed cleanup raises `PostLaunchError` and transfers only the still-owned original child to an eventual reaper; no success or absence proof follows. Generic successful calls do not newly signal process groups.
376
398
 
377
- Recovery consumers with a separately accepted journal registration pass `expected_registration:` to `Inbox#reconcile`. The exact `event_id`, `attempt_id`, `payload_sha256` and `receipt_key_sha256` map is checked inside the event lock before signature verification or settlement. A prior `status` call alone does not establish this precondition. A mismatch raises `ValidationError` without settling the event.
399
+ The fixed protected handler selects `cleanup_group: true`: stream EOF alone never triggers termination. Nonreaping observation must establish the original leader exited before its group is signalled and the leader reaped. Loss of original child ownership refuses further signalling. Controlled source checks cover ownership/IO boundaries; installed native/descendant effectiveness belongs to gad.2.
@@ -4,6 +4,10 @@ require "json"
4
4
  require "openssl"
5
5
  require "ace/support/cli"
6
6
  require_relative "support"
7
+ require_relative "../../molecules/protected_inbox_selection"
8
+ require_relative "../../molecules/inbox_context_client"
9
+ require_relative "../../molecules/inbox_cli_input"
10
+ require_relative "../../molecules/inbox_direct_effect_binding"
7
11
 
8
12
  module Ace
9
13
  module Herdr
@@ -13,25 +17,40 @@ module Ace
13
17
  include Ace::Support::Cli::Base
14
18
  include Runtime
15
19
 
16
- desc "Enqueue, inspect, deliver, or reconcile a durable agent inbox event"
17
- argument :operation, desc: "enqueue, status, deliver, or reconcile"
20
+ desc "Enqueue, inspect, or deliver a durable agent inbox event"
21
+ argument :operation, desc: "enqueue, status, or deliver"
22
+ option :project, type: :string, desc: "Installed project ID"
23
+ option :mapping, type: :string, desc: "Installed mapping ID"
24
+ option :inbox_context, type: :string, desc: "Installed inbox context ID"
25
+ option :claim_generation, type: :integer, desc: "Original expected claim generation"
26
+ option :assignment, type: :string, desc: "Original assignment ID"
18
27
  option :event, type: :string, desc: "Stable event ID"
19
28
  option :attempt, type: :string, desc: "Assignment attempt ID"
20
29
  option :ref, type: :string, desc: "Herdr reverse-address JSON file"
21
30
  option :file, type: :string, desc: "Payload file"
22
- option :receipt, type: :string, desc: "Native proof JSON file"
23
31
  option :format, type: :string, desc: "Output format (json)"
24
32
 
25
- def initialize(executor: nil, native: nil)
33
+ def initialize(executor: nil, native: nil, selection: Molecules::ProtectedInboxSelection.new,
34
+ context_client_factory: Molecules::InboxContextClient.method(:selected),
35
+ kernel: Ace::Runtime::Molecules::ProtectedLinux.new)
26
36
  @executor = executor
27
37
  @native = native
38
+ @selection, @context_client_factory, @kernel = selection, context_client_factory, kernel
28
39
  end
29
40
 
30
41
  def call(operation: nil, **options)
31
42
  inbox = nil
32
43
  translate_errors do
44
+ cli_error("unsupported inbox operation") unless %w[enqueue status deliver].include?(operation)
33
45
  cli_error("--event is required") if options[:event].to_s.empty?
34
46
  cli_error("only --format json is supported") if options[:format] && options[:format] != "json"
47
+ protected = false
48
+ @selection.with(options) do |selected|
49
+ next unless selected
50
+ puts JSON.generate(protected_call(operation, options, selected))
51
+ protected = true
52
+ end
53
+ next if protected
35
54
  inbox = Organisms::Inbox.from_config(config: config, executor: executor,
36
55
  native: @native || Molecules::NativeQueueExecutor.new,
37
56
  root: Dir.pwd)
@@ -44,24 +63,8 @@ module Ace
44
63
  inbox.status(event: options[:event])
45
64
  when "deliver"
46
65
  inbox.deliver(event: options[:event])
47
- when "reconcile"
48
- path = options[:receipt]
49
- begin
50
- bytes = path.to_s.empty? ? nil : File.binread(path)
51
- receipt = bytes && JSON.parse(bytes)
52
- signature = bytes && File.binread("#{path}.sig")
53
- rescue SystemCallError, JSON::ParserError => e
54
- # Receipt input is caller-owned: unreadable or malformed
55
- # proof files surface as the documented refusal, never as
56
- # command failures.
57
- puts JSON.generate(inbox.status(event: options[:event]).merge(
58
- "reconciliation_refusal" => "invalid receipt: #{e.message}"))
59
- return
60
- end
61
- inbox.reconcile(event: options[:event], receipt: receipt,
62
- signed_bytes: bytes, signature: signature)
63
66
  else
64
- cli_error("operation must be enqueue, status, deliver, or reconcile")
67
+ cli_error("operation must be enqueue, status, or deliver")
65
68
  end
66
69
  puts JSON.generate(result)
67
70
  end
@@ -69,6 +72,65 @@ module Ace
69
72
  raise Ace::Support::Cli::Error, e.message
70
73
  end
71
74
 
75
+
76
+ private
77
+
78
+ def protected_call(operation, options, selected)
79
+ %i[event attempt assignment].each { |key| cli_error("--#{key} is required") if options[key].to_s.empty? }
80
+ unless %w[enqueue status deliver].include?(operation)
81
+ cli_error("protected operation requires its maintained direct handler")
82
+ end
83
+ allowed = %i[project mapping inbox_context event attempt assignment format]
84
+ allowed += %i[ref file] if operation == "enqueue"
85
+ allowed += %i[claim_generation] if %w[deliver].include?(operation)
86
+ if options.any? { |key, value| !value.nil? && !allowed.include?(key) }
87
+ cli_error("protected inbox operation options differ")
88
+ end
89
+ context = selected.fetch("context")
90
+ client = @context_client_factory.call(context_id: selected.fetch("inbox_context_id"),
91
+ socket_path: context.fetch("control_socket_path"), owner_credentials: context.fetch("owner_credentials"))
92
+ if operation == "status"
93
+ return client.request("status_context", {"event_id" => options.fetch(:event), "attempt_id" => options.fetch(:attempt), "original" => direct_original(selected, options)}).fetch("record")
94
+ end
95
+ if operation == "deliver"
96
+ unless options[:claim_generation].is_a?(Integer) && options[:claim_generation] >= 0
97
+ cli_error("--claim-generation must be an explicit nonnegative integer")
98
+ end
99
+ admission = direct_admission(client, selected, options, "deliver")
100
+ result = client.request("deliver_context", {"operation_id" => admission.fetch("operation_id"),
101
+ "key_generation" => admission.fetch("key_generation"), "event_id" => options.fetch(:event),
102
+ "attempt_id" => options.fetch(:attempt), "original" => direct_original(selected, options), "expected_claim_generation" => options.fetch(:claim_generation)})
103
+ unless %w[idle unknown].include?(result["admission_state"])
104
+ cli_error("direct delivery admission observation differs")
105
+ end
106
+ client.request("end_context_operation", {"operation_id" => admission.fetch("operation_id")}) if result.fetch("admission_state") == "idle"
107
+ return result.fetch("record").merge("admission_state" => result.fetch("admission_state"))
108
+ end
109
+ %i[ref file].each { |key| cli_error("--#{key} is required") if options[key].to_s.empty? }
110
+ reverse = Molecules::InboxCliInput.reverse(options.fetch(:ref))
111
+ payload = Molecules::InboxCliInput.read(options.fetch(:file), limit: 65_536).dup.force_encoding(Encoding::UTF_8)
112
+ cli_error("payload encoding differs") unless payload.valid_encoding? && !payload.include?("\0")
113
+ admission = direct_admission(client, selected, options, "enqueue")
114
+ result = client.request("enqueue_context", {"operation_id" => admission.fetch("operation_id"),
115
+ "key_generation" => admission.fetch("key_generation"), "event_id" => options.fetch(:event),
116
+ "attempt_id" => options.fetch(:attempt), "original" => direct_original(selected, options), "reverse" => reverse, "payload_bytes" => payload.bytesize,
117
+ "payload_sha256" => Digest::SHA256.hexdigest(payload)}, payload: payload)
118
+ client.request("end_context_operation", {"operation_id" => admission.fetch("operation_id")})
119
+ result.fetch("record").merge("admission_state" => result.fetch("admission_state"))
120
+ end
121
+
122
+ def direct_original(selected, options)
123
+ value = selected.slice("project_id", "mapping_id", "inbox_context_id").merge("assignment_id" => options.fetch(:assignment))
124
+ Molecules::InboxDirectEffectBinding.original!(value)
125
+ end
126
+
127
+ def direct_admission(client, selected, options, purpose)
128
+ peer = @kernel.capture(Process.pid)
129
+ client.request("begin_context_operation", {"context_id" => selected.fetch("inbox_context_id"),
130
+ "purpose" => purpose, "event_id" => options.fetch(:event), "process_binding" => peer,
131
+ "original" => direct_original(selected, options).merge("attempt_id" => options.fetch(:attempt))})
132
+ end
133
+
72
134
  end
73
135
  end
74
136
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ace/support/cli"
5
+ require_relative "support"
6
+ require_relative "../../molecules/native_source"
7
+ require_relative "../../organisms/native_source_builder"
8
+
9
+ module Ace
10
+ module Herdr
11
+ module CLI
12
+ module Commands
13
+ class NativeSource < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+ include Runtime
16
+
17
+ desc "Describe, build, or verify the packaged guarded native source without running Herdr"
18
+ argument :operation, desc: "selection, build, or verify"
19
+ option :source, type: :string, desc: "Clean local Git checkout containing the pinned baseline"
20
+ option :output, type: :string, desc: "New build artifact directory"
21
+ option :target, type: :string, desc: "Selected Linux target"
22
+ option :artifact, type: :string, desc: "Build artifact directory to verify"
23
+
24
+ def initialize(selection: nil, builder: nil)
25
+ @selection = selection
26
+ @builder = builder
27
+ end
28
+
29
+ def call(operation: nil, **options)
30
+ selection = @selection || Molecules::NativeSource.new
31
+ supplied = options.select { |key, value| %i[source output target artifact].include?(key) && !value.nil? }.keys
32
+ result = case operation
33
+ when "selection"
34
+ cli_error("selection accepts no build/artifact options") unless supplied.empty?
35
+ selection.description
36
+ when "verify"
37
+ cli_error("verify requires only --artifact") unless supplied == [:artifact] && !options[:artifact].empty?
38
+ selection.verify_artifact(directory: options.fetch(:artifact))
39
+ when "build"
40
+ unless supplied.sort == %i[output source target] && supplied.all? { |key| !options[key].empty? }
41
+ cli_error("build requires --source, --output and --target")
42
+ end
43
+ builder = @builder || Organisms::NativeSourceBuilder.new(selection: selection)
44
+ builder.build(source: options.fetch(:source), output: options.fetch(:output), target: options.fetch(:target))
45
+ else
46
+ cli_error("native-source operation must be selection, build, or verify")
47
+ end
48
+ puts JSON.generate(result)
49
+ rescue Molecules::NativeSource::Error => error
50
+ raise Ace::Support::Cli::Error, error.message
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/ace/herdr/cli.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "cli/commands/capture"
15
15
  require_relative "cli/commands/workspace"
16
16
  require_relative "cli/commands/tab"
17
17
  require_relative "cli/commands/list_presets"
18
+ require_relative "cli/commands/native_source"
18
19
 
19
20
  module Ace
20
21
  module Herdr
@@ -29,6 +30,7 @@ module Ace
29
30
  ["deliver", "Push an answer to an agent pane (ace-hitl delivery contract)"],
30
31
  ["inbox", "Durable agent message queue and reconciliation"],
31
32
  ["dispatch", "Start an agent in one command: tab + agent + prompt"],
33
+ ["native-source", "Describe, build, or verify the packaged guarded native source"],
32
34
  ["list", "List live panes, tabs, or workspaces as one JSON line"],
33
35
  ["send", "Send a command, raw text, or named keys to a pane"],
34
36
  ["capture", "Print recent pane output as raw text"],
@@ -69,6 +71,7 @@ module Ace
69
71
  register "deliver", CLI::Commands::Deliver.new
70
72
  register "inbox", CLI::Commands::Inbox.new
71
73
  register "dispatch", CLI::Commands::Dispatch.new
74
+ register "native-source", CLI::Commands::NativeSource.new
72
75
  register "list", CLI::Commands::List.new
73
76
  register "send", CLI::Commands::Send.new
74
77
  register "capture", CLI::Commands::Capture.new
@@ -2,6 +2,29 @@
2
2
 
3
3
  module Ace
4
4
  module Herdr
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the caller's herdr workspace/session/pane cannot be resolved
8
+ class TargetResolutionError < Error; end
9
+
10
+ # Raised when arguments or refs fail validation
11
+ class ValidationError < Error; end
12
+
13
+ # Raised when a bounded wait exceeds its timeout
14
+ class WaitTimeoutError < Error; end
15
+
16
+ # Raised when a preset tab was created natively but a later
17
+ # materialization step (splits, commands, agents) failed; carries the
18
+ # created tab id so the owner can roll back exactly that tab.
19
+ class TabMaterializationError < Error
20
+ attr_reader :tab_id
21
+
22
+ def initialize(tab_id:, message:)
23
+ @tab_id = tab_id
24
+ super(message)
25
+ end
26
+ end
27
+
5
28
  # Base for herdr CLI executor failures; retry classification drives the
6
29
  # ace-hitl DeliverResult state mapping (retryable vs terminal).
7
30
  class ExecutorError < Error