ace-herdr 0.3.2 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/herdr/config.yml +0 -4
  3. data/CHANGELOG.md +30 -0
  4. data/docs/usage.md +65 -32
  5. data/lib/ace/herdr/cli/commands/inbox.rb +81 -31
  6. data/lib/ace/herdr/cli/commands/native_source.rb +56 -0
  7. data/lib/ace/herdr/cli/commands/support.rb +2 -6
  8. data/lib/ace/herdr/cli.rb +3 -0
  9. data/lib/ace/herdr/errors.rb +23 -0
  10. data/lib/ace/herdr/molecules/bounded_process.rb +266 -51
  11. data/lib/ace/herdr/molecules/delivery_record_store.rb +81 -5
  12. data/lib/ace/herdr/molecules/guarded_native_origin.rb +59 -0
  13. data/lib/ace/herdr/molecules/herdr_executor.rb +6 -3
  14. data/lib/ace/herdr/molecules/inbox_cli_input.rb +36 -0
  15. data/lib/ace/herdr/molecules/inbox_context_client.rb +88 -0
  16. data/lib/ace/herdr/molecules/inbox_context_completion_client.rb +119 -0
  17. data/lib/ace/herdr/molecules/inbox_context_key.rb +63 -0
  18. data/lib/ace/herdr/molecules/inbox_context_lifetime.rb +122 -0
  19. data/lib/ace/herdr/molecules/inbox_context_native_process.rb +92 -0
  20. data/lib/ace/herdr/molecules/inbox_context_service_configuration.rb +183 -0
  21. data/lib/ace/herdr/molecules/inbox_context_store.rb +159 -0
  22. data/lib/ace/herdr/molecules/inbox_context_wire.rb +76 -0
  23. data/lib/ace/herdr/molecules/inbox_direct_effect_binding.rb +92 -0
  24. data/lib/ace/herdr/molecules/inbox_direct_result.rb +65 -0
  25. data/lib/ace/herdr/molecules/native_queue_executor.rb +10 -21
  26. data/lib/ace/herdr/molecules/native_source.rb +189 -0
  27. data/lib/ace/herdr/molecules/protected_inbox_selection.rb +126 -0
  28. data/lib/ace/herdr/molecules/protected_native_control.rb +259 -0
  29. data/lib/ace/herdr/native_source/HERDR-LICENSE +201 -0
  30. data/lib/ace/herdr/native_source/guarded-prompt.patch +3493 -0
  31. data/lib/ace/herdr/native_source/selection.json +24 -0
  32. data/lib/ace/herdr/organisms/deliverer.rb +2 -3
  33. data/lib/ace/herdr/organisms/inbox.rb +384 -137
  34. data/lib/ace/herdr/organisms/inbox_context_direct_effects.rb +285 -0
  35. data/lib/ace/herdr/organisms/inbox_context_effects.rb +30 -0
  36. data/lib/ace/herdr/organisms/inbox_context_listener.rb +151 -0
  37. data/lib/ace/herdr/organisms/inbox_context_owner.rb +550 -0
  38. data/lib/ace/herdr/organisms/inbox_context_server.rb +70 -0
  39. data/lib/ace/herdr/organisms/inbox_context_service.rb +107 -0
  40. data/lib/ace/herdr/organisms/inbox_guarded_wake.rb +117 -0
  41. data/lib/ace/herdr/organisms/native_source_builder.rb +183 -0
  42. data/lib/ace/herdr/organisms/runtime_adapter.rb +30 -1
  43. data/lib/ace/herdr/version.rb +1 -1
  44. data/lib/ace/herdr.rb +5 -28
  45. metadata +69 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6c7f32e364c4df76447fd6d4356f2117007cf920dc3f2f420bd4dd1e21f8242
4
- data.tar.gz: 783dd62904dd12e952528d80d9956511c052fcf23a53551b390f5817bbe93c8c
3
+ metadata.gz: 03bb080ee7010378787d8e6e05033330cddf0a03405f9bdb321f95b3021395b1
4
+ data.tar.gz: 7b935435b107aeafe1023139a526135131f600711800afbbde4f7cc6de61e5d8
5
5
  SHA512:
6
- metadata.gz: bd865086e653b9f8bed7434073707fb733f0f9f0f08c6d979046476ee955a65a2938e7efbad966942fe2a55b68b80c7bbc6a53076601bdaac4cb0ff94029e099
7
- data.tar.gz: '08c29e0f0cfb5fa1857959c1fd0ef05376ce3dcc895e6ee6a056e2b3be58c00311e5d0b397e07f063bc94151bb3567441c9e1213bf11e6ea16ea570e8e3fa010'
6
+ metadata.gz: ad76243e13f3abc8656f2e6d443dc0d12e2b878849716fde11d4ca55152537f11595ff01e029e08b69c122e2863476e2fb734f733cfd0bfb91115e1758611630
7
+ data.tar.gz: 51735a6becc55d04ab97c0ee83ec735933705674d0fcf6a8c27833a219c9b71e7fa1e132733061f27d557b1914fc9aada318a9b06f09c7c43902ff339b185242
@@ -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,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-10-08
11
+
12
+ ### Changed
13
+
14
+ - 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.
15
+ - Remove dedicated Codex app-server selection, message-read observation, signing keys and consumed/superseded reconciliation APIs.
16
+ - Keep authenticated original-context admission and actual scoped effects without a second current-state projection.
17
+
18
+ ### Fixed
19
+
20
+ - Load bounded process/error primitives directly and preserve bounded cleanup of the original process after execution uncertainty.
21
+
22
+ ## [0.4.0] - 2026-10-05
23
+
24
+ ### Fixed
25
+ - Use the shared typed reverse-reference pair at CLI, Inbox and delivery boundaries, refusing malformed persisted targets and noncanonical serialized files before claim or native observation.
26
+ - Pin managed delivery to the accepted original native target under the event lock and reject secret-bearing payloads even without a nested message.
27
+
28
+ ### Changed
29
+
30
+ - Declare the required direct dependencies and minimum producer versions for this coordinated release: `ace-hitl-contract ~> 0.2`, `ace-runtime ~> 0.2`.
31
+ - Bind protected control to the canonical per-attempt server/socket identity while retaining the fixed native executable view selected by installation verification.
32
+ - Validate and preserve the shared managed delivery envelope at Inbox enqueue, rejecting mismatched scope/digest/correlation and changed replay metadata.
33
+
34
+ ### Added
35
+
36
+ - Add protected Herdr control pinned to the installed server/socket and fixed workspace, creating one fresh bootstrap pane with exact child observation and no default shell.
37
+
38
+ - Expose verified native process/session ownership and allow identical signed inbox proof re-verification after settlement without duplicate transitions.
39
+
10
40
  ## [0.3.2] - 2026-10-04
11
41
 
12
42
  ### Changed
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]`
@@ -48,45 +48,35 @@ Native `agent_blocked` becomes `SendRejectedError`; `agent_prompt_stalled` becom
48
48
 
49
49
  Use a stable event ID for one message and the assignment attempt that owns it. The reverse-address JSON contains `session` and `pane`, as with `deliver`.
50
50
 
51
+ Reverse references use the shared `ace.hitl.ref/v1` pair contract. Herdr token names
52
+ remain valid; the codec also preserves paired native tmux IDs (`$0`/`%0`) when
53
+ carried by managed metadata. Mixed native/token or cross-field IDs refuse.
54
+ Wire and persisted references must be canonical. Syntax acceptance does not add
55
+ tmux execution to this Herdr controller or replace its exact native ownership,
56
+ target and submission checks.
57
+
51
58
  ```bash
52
59
  ace-herdr inbox enqueue --event inb-12345678 --attempt ATTEMPT --ref ref.json --file prompt.txt
53
60
  ace-herdr inbox status --event inb-12345678 --format json
54
61
  ace-herdr inbox deliver --event inb-12345678
55
62
  ```
56
63
 
57
- `enqueue` checks the live pane, durable terminal, agent kind, and native thread identity. 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.
58
-
59
- `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.
60
-
61
- ```bash
62
- ace-herdr inbox reconcile --event inb-12345678 --receipt proof.json
63
- ```
64
-
65
- 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:
66
-
67
- ```json
68
- {
69
- "event_id": "inb-12345678",
70
- "attempt_id": "ATTEMPT",
71
- "claim_generation": 1,
72
- "payload_sha256": "<digest from inbox status>",
73
- "binding": {"session": "<exact binding from inbox status>"},
74
- "outcome": "consumed",
75
- "observer": {"role": "operator", "id": "operator-id"},
76
- "evidence": {"kind": "consumed_acknowledged", "native_reference": "session-log:42", "observation": "message consumed and acknowledged"}
77
- }
78
- ```
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.
79
68
 
80
- 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.
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.
81
74
 
82
- After inspecting the native outcome and writing `proof.json`, the trusted operator signs the exact bytes:
83
-
84
- ```bash
85
- openssl dgst -sha256 -sign operator-private.pem -out proof.json.sig proof.json
86
- ace-herdr inbox reconcile --event inb-12345678 --receipt proof.json
87
- ```
88
-
89
- 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.
90
80
 
91
81
  ## tmux-intent ↔ herdr-command parity
92
82
 
@@ -364,3 +354,46 @@ Commands raise a CLI error (non-zero exit) carrying herdr's machine code where o
364
354
  - Invalid send shapes: usage error **before any transport call** (nothing is sent)
365
355
 
366
356
  Exit `0` means success (including an explicit empty `list` result or a satisfied wait).
357
+
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.
398
+
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,29 +17,43 @@ 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"
35
- inbox = Organisms::Inbox.new(executor: executor,
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
54
+ inbox = Organisms::Inbox.from_config(config: config, executor: executor,
36
55
  native: @native || Molecules::NativeQueueExecutor.new,
37
- deliveries_dir: deliveries_dir,
38
- receipt_public_key: %w[enqueue reconcile].include?(operation) ? receipt_public_key : nil)
56
+ root: Dir.pwd)
39
57
  result = case operation
40
58
  when "enqueue"
41
59
  %i[attempt ref file].each { |key| cli_error("--#{key} is required") if options[key].to_s.empty? }
@@ -45,24 +63,8 @@ module Ace
45
63
  inbox.status(event: options[:event])
46
64
  when "deliver"
47
65
  inbox.deliver(event: options[:event])
48
- when "reconcile"
49
- path = options[:receipt]
50
- begin
51
- bytes = path.to_s.empty? ? nil : File.binread(path)
52
- receipt = bytes && JSON.parse(bytes)
53
- signature = bytes && File.binread("#{path}.sig")
54
- rescue SystemCallError, JSON::ParserError => e
55
- # Receipt input is caller-owned: unreadable or malformed
56
- # proof files surface as the documented refusal, never as
57
- # command failures.
58
- puts JSON.generate(inbox.status(event: options[:event]).merge(
59
- "reconciliation_refusal" => "invalid receipt: #{e.message}"))
60
- return
61
- end
62
- inbox.reconcile(event: options[:event], receipt: receipt,
63
- signed_bytes: bytes, signature: signature)
64
66
  else
65
- cli_error("operation must be enqueue, status, deliver, or reconcile")
67
+ cli_error("operation must be enqueue, status, or deliver")
66
68
  end
67
69
  puts JSON.generate(result)
68
70
  end
@@ -70,17 +72,65 @@ module Ace
70
72
  raise Ace::Support::Cli::Error, e.message
71
73
  end
72
74
 
75
+
73
76
  private
74
77
 
75
- def receipt_public_key
76
- path = config["inbox_receipt_public_key"]
77
- return nil unless path.is_a?(String) && path.start_with?("/")
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
78
126
 
79
- key = OpenSSL::PKey.read(File.read(path))
80
- key if key.is_a?(OpenSSL::PKey::RSA) && !key.private?
81
- rescue SystemCallError, OpenSSL::PKey::PKeyError
82
- nil
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))})
83
132
  end
133
+
84
134
  end
85
135
  end
86
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
@@ -30,12 +30,8 @@ module Ace
30
30
  # fail closed (spec 8wm.t.vrz §3)
31
31
  def resolve_ref(session, pane)
32
32
  Ace::Hitl::Providers::Ref.new(
33
- session: Ace::Hitl::Providers::Ref.validate!(
34
- session || ENV["HERDR_SESSION"], "HERDR_SESSION"
35
- ),
36
- pane: Ace::Hitl::Providers::Ref.validate!(
37
- pane || ENV["HERDR_PANE"], "HERDR_PANE"
38
- )
33
+ session: session || ENV["HERDR_SESSION"], pane: pane || ENV["HERDR_PANE"],
34
+ session_source: "HERDR_SESSION", pane_source: "HERDR_PANE"
39
35
  )
40
36
  end
41
37
 
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