ace-hitl 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +10 -3
- data/docs/usage.md +85 -16
- data/lib/ace/hitl/cli/commands/ask.rb +51 -8
- data/lib/ace/hitl/cli/commands/cancel.rb +2 -2
- data/lib/ace/hitl/cli/commands/consume.rb +6 -3
- data/lib/ace/hitl/cli/commands/deliver.rb +3 -2
- data/lib/ace/hitl/cli/commands/duty.rb +3 -3
- data/lib/ace/hitl/cli/commands/lifecycle_command.rb +8 -6
- data/lib/ace/hitl/cli/commands/pending.rb +3 -3
- data/lib/ace/hitl/cli/commands/serve.rb +71 -0
- data/lib/ace/hitl/cli/commands/states.rb +3 -3
- data/lib/ace/hitl/cli.rb +5 -3
- data/lib/ace/hitl/lifecycle/binding.rb +12 -1
- data/lib/ace/hitl/lifecycle/client.rb +161 -0
- data/lib/ace/hitl/lifecycle/duty.rb +6 -6
- data/lib/ace/hitl/lifecycle/effects.rb +35 -10
- data/lib/ace/hitl/lifecycle/errors.rb +12 -1
- data/lib/ace/hitl/lifecycle/kinds.rb +12 -0
- data/lib/ace/hitl/lifecycle/otp_vault.rb +131 -0
- data/lib/ace/hitl/lifecycle/peer.rb +61 -0
- data/lib/ace/hitl/lifecycle/policy.rb +172 -0
- data/lib/ace/hitl/lifecycle/protocol.rb +109 -0
- data/lib/ace/hitl/lifecycle/service.rb +289 -0
- data/lib/ace/hitl/lifecycle/store.rb +475 -129
- data/lib/ace/hitl/lifecycle.rb +13 -4
- data/lib/ace/hitl/providers/lab/assignment_binding.rb +82 -0
- data/lib/ace/hitl/providers/lab/composite_binding.rb +47 -0
- data/lib/ace/hitl/providers/lab/daemon_binding.rb +9 -5
- data/lib/ace/hitl/providers/lab.rb +62 -21
- data/lib/ace/hitl/version.rb +1 -1
- metadata +25 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 579408795de68b05baeeb590eac46687706134301423cb6114874c377d8183ed
|
|
4
|
+
data.tar.gz: ba4f300554670fb40acd58c470183d70342626b170889bda9944468aa54df6e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f421f0a4df5ea2eb782766c259336f1298844b3e8ebcefad34885c446865538f02d4d7da3bedcbc84c254464e8a589ff4d3007de8fbe14d0158ad23edf6118d3
|
|
7
|
+
data.tar.gz: 5102b14720d671f3252eae1b37b942620128f33df90983877e25354958b147bda0545b86ff946aa35f973210abdea603a342547acf11621b4baf2829c0372a65
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.0] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Scoped privilege boundary for multi-user HITL state (spec 8wq.t.34i)**: the lifecycle store is now the PRIVATE state of one authenticated boundary service. `ace-hitl serve` runs the boundary over a peer-credential-authenticated UNIX socket (`Ace::Hitl::Lifecycle::Service`): every connection is identified from kernel peer credentials (`getpeereid` — payload fields and environment variables are never authority), clients authenticate the endpoint back (socket must be owned by the trusted service uid from the grants document, not world-writable, connected peer must BE the service uid), and the store root is service-owned (`0711` traverse-only root, `0700` private directories, `0755` public projections with `0440` non-secret records — no chmod-to-world workaround anywhere). Bounded newline-JSON protocol (`Lifecycle::Protocol`) with classified errors: `PermissionError`, `BindingError`, `StateError`, `AnswerError`, and the new `TransportError` for boundary unavailability — transport failures are visible and recoverable, never silent.
|
|
14
|
+
- **Managed assignment binding**: requests bind to the exact ACTIVE MANAGED attempt of the calling actor through the ace-assign coordinator (`Providers::Lab::AssignmentBinding`). `ace-assign` gained `AttemptCoordinator#with_verified_attempt`, which holds the assignment `LifecycleExclusion` (shared side) across the caller's whole locked transition while `finish`/`reconcile` hold the exclusive side — stale, ended, replaced, or unproven (uncertain) attempts cannot acquire authority, and terminal commits can never interleave with a liveness check. CLI: `ace-hitl ask --assignment ID --attempt ID --project ID`.
|
|
15
|
+
- **Transport authorization from trusted grants** (`Lifecycle::GrantsPolicy` + `TrustedFile`): transport uids and the service identity come only from the deployment-owned grants document (same file and traversal trust rules as ace-lab's `GrantResolver`: root-owned, no group/world-writable components, symlink-verified). ace-lab ships the matching `Molecules::HitlAuthorizer` for the lab-side delivery path.
|
|
16
|
+
- **Idempotent terminal receipts**: every consume/cancel commits a durable receipt under the stable per-request lock (lock files live in `locks/` and survive id reuse); retries report the committed outcome instead of failing, conflicting transitions stay classified errors.
|
|
17
|
+
- **OTP exact-operation challenges**: an `otp` request exists only with its non-secret publisher evidence (`{operation, result_ref, input_digest, expires_at}`, bounded to 24h); consumption requires the challenge's authorized operation; duplicate consumes replay the receipt WITHOUT the secret bytes. OTP effects are forbidden (the authorized operation consumes the OTP transiently). The service holds OTP bytes in a bounded, TTL-bound memory vault (`OtpVault::MemoryVault`) — secret bytes persist nowhere, transfer exactly once, and a restart deliberately loses the challenge (prompt again).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **BREAKING (pre-1.0, ADR-024)**: `ace-hitl ask/deliver/consume/cancel/pending/states/duty` run through the boundary client — the CLI no longer touches shared store files directly, and broker operations no longer require root (the configured transport identity from the grants document does). The admin binding bypass is removed: EVERY request validates against its live attempt. The `LAB_ATTEMPT_ID` environment default is gone (environment is never attempt authority); `--attempt` is required, and binding requires `--assignment` (managed) or `--work` (legacy path, kept until vs2 switches consumers). Answers and store files are service-owned `0600` — requesters receive bytes over the authenticated boundary, not through file ownership. The multi-UID acceptance fixture (`test/edge/lifecycle/`) proves requester isolation, protected ownership, forged-identity rejection, and one-terminal-transition races under real distinct OS accounts (requires root; CI records the run as advisory evidence).
|
|
21
|
+
|
|
22
|
+
|
|
10
23
|
## [0.10.0] - 2026-09-27
|
|
11
24
|
|
|
12
25
|
### Added
|
data/README.md
CHANGED
|
@@ -10,15 +10,22 @@ Canonical workflow and skill for agents:
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
12
12
|
- `ace-hitl create` creates a HITL event
|
|
13
|
-
- `ace-hitl ask` asks a human via HITL and forwards the request through a provider adapter (`--provider`, default `lab`); ONE operation: local event + relay request through the
|
|
13
|
+
- `ace-hitl ask` asks a human via HITL and forwards the request through a provider adapter (`--provider`, default `lab`); ONE operation: local event + relay request through the authenticated boundary (`--assignment/--attempt/--project` managed binding, `--work` legacy, effect callback flags)
|
|
14
14
|
- `ace-hitl list` lists HITL events with filters (`--scope current|all`, all statuses by default)
|
|
15
15
|
- `ace-hitl show` renders event details, path, or raw content (`--scope current|all`)
|
|
16
16
|
- `ace-hitl update` updates frontmatter, answer content, and folder location
|
|
17
17
|
- `ace-hitl wait` polls a specific HITL event until answered (`--poll-every`, `--timeout`)
|
|
18
|
-
- `ace-hitl deliver` answers a pending relay request from stdin (
|
|
18
|
+
- `ace-hitl deliver` answers a pending relay request from stdin (configured transport operation through the boundary); executes the declared effect callback (non-OTP kinds)
|
|
19
19
|
- `ace-hitl consume` consumes one own relay request's answer (indefinite by default; `--timeout` bounds only the local wait)
|
|
20
20
|
- `ace-hitl cancel` cancels with an audited reason (the only way to abandon a request)
|
|
21
|
-
- `ace-hitl pending` / `ace-hitl states` / `ace-hitl duty`
|
|
21
|
+
- `ace-hitl pending` / `ace-hitl states` / `ace-hitl duty` transport projections (pending, public lifecycle records, pending + escalated)
|
|
22
|
+
- `ace-hitl serve` runs the authenticated store boundary service (peer-credential UNIX socket; trusted grants authorize the transport)
|
|
23
|
+
|
|
24
|
+
Multi-user HITL state (spec 8wq.t.34i) is the PRIVATE state of the
|
|
25
|
+
`ace-hitl serve` boundary: identity comes from kernel peer
|
|
26
|
+
credentials, transport authority from the trusted grants document,
|
|
27
|
+
and OTP secrets are held in service memory only — never in files,
|
|
28
|
+
logs, or projections.
|
|
22
29
|
- `ace-hitl overseer-send` / `ace-hitl overseer-pending` / `ace-hitl overseer-ack` the Overseer reverse-address response channel
|
|
23
30
|
|
|
24
31
|
`ace-hitl` is a blocker-resolution tool, not a global dashboard:
|
data/docs/usage.md
CHANGED
|
@@ -144,17 +144,82 @@ ace-hitl ask "Proceed with deploy?" \
|
|
|
144
144
|
is declared by the adapter interface; provider `lab` reports it as
|
|
145
145
|
unsupported until the ace-herdr push-delivery integration lands.
|
|
146
146
|
|
|
147
|
+
## Scoped Store Boundary (spec 8wq.t.34i)
|
|
148
|
+
|
|
149
|
+
The relay request store is the PRIVATE state of one authenticated
|
|
150
|
+
boundary service. `ace-hitl serve` owns the store and speaks a bounded
|
|
151
|
+
JSON protocol over a UNIX socket; every client operation
|
|
152
|
+
(`ask`/`deliver`/`consume`/`cancel`/`pending`/`states`/`duty`/`read`)
|
|
153
|
+
runs through the authenticated boundary client — the CLI never touches
|
|
154
|
+
shared store files directly.
|
|
155
|
+
|
|
156
|
+
Identity is a kernel fact: the service resolves each connection's peer
|
|
157
|
+
uid from the OS (`getpeereid`), and payload fields or environment
|
|
158
|
+
variables can never name a requester. Clients authenticate the
|
|
159
|
+
endpoint back: the socket must be owned by the trusted service uid,
|
|
160
|
+
must not be world-writable, and the connected peer must BE the service
|
|
161
|
+
uid. Authorization facts (service uid, transport uids, per-project
|
|
162
|
+
Captain visibility) come only from the trusted deployment grants
|
|
163
|
+
document — the same file and traversal trust rules as ace-lab
|
|
164
|
+
(default `ACE_HITL_GRANTS_PATH`, `/etc/lab/ace-lab/authorization.yml`).
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Deployment (root runs the service under its own account):
|
|
168
|
+
ace-hitl serve # ACE_HITL_SOCKET, ACE_HITL_STORE_ROOT,
|
|
169
|
+
# ACE_HITL_GRANTS_PATH override paths
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Store layout (all service-owned): `0711` traverse-only root;
|
|
173
|
+
`requests/ answers/ secrets/ effects/ locks/ terminals/` are `0700`;
|
|
174
|
+
`public/` is `0755` with `0440` non-secret projections. There is no
|
|
175
|
+
chmod-to-world-writable mode: requesters receive answer bytes over the
|
|
176
|
+
authenticated connection, never through file ownership.
|
|
177
|
+
|
|
178
|
+
Roles:
|
|
179
|
+
|
|
180
|
+
- requester: create, consume and cancel its OWN requests (request
|
|
181
|
+
facts come back from `ask`/`consume`; the boundary `read` protocol
|
|
182
|
+
operation is available to library clients);
|
|
183
|
+
- configured transport (grants `hitl.transport_uids` + principals):
|
|
184
|
+
`deliver`, `pending`, `states`, `duty`;
|
|
185
|
+
- unknown identity is an error, never permission.
|
|
186
|
+
|
|
187
|
+
Failures are classified end to end: `PermissionError`,
|
|
188
|
+
`BindingError`, `StateError`, `AnswerError`, and `TransportError`
|
|
189
|
+
(unavailable/untrusted boundary, deadline, malformed frame) — transport
|
|
190
|
+
failures are visible and recoverable, and duplicate consume/cancel are
|
|
191
|
+
idempotent (the committed receipt replays; a conflicting transition is
|
|
192
|
+
an error).
|
|
193
|
+
|
|
147
194
|
## Relay Lifecycle (Generic HITL Request Store)
|
|
148
195
|
|
|
149
196
|
The generic relay request lifecycle is native to the gem
|
|
150
|
-
(`Ace::Hitl::Lifecycle`; migration spec 8wm.t.y21
|
|
151
|
-
|
|
152
|
-
shares one per-request lock
|
|
153
|
-
(default `/run/lab/hitl`); the Overseer channel
|
|
154
|
-
`ACE_HITL_OVERSEER_CHANNEL_ROOT` (default
|
|
155
|
-
Machine output is one JSON line.
|
|
197
|
+
(`Ace::Hitl::Lifecycle`; migration spec 8wm.t.y21, scoped by
|
|
198
|
+
8wq.t.34i). Requests never expire on a timer, every terminal
|
|
199
|
+
transition shares one stable per-request lock, and the store root is
|
|
200
|
+
`ACE_HITL_STORE_ROOT` (default `/run/lab/hitl`); the Overseer channel
|
|
201
|
+
root is `ACE_HITL_OVERSEER_CHANNEL_ROOT` (default
|
|
202
|
+
`/lab/state/overseer-channel`). Machine output is one JSON line.
|
|
203
|
+
|
|
204
|
+
Managed binding (the default authority):
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
ace-hitl ask "Choose the next scope" \
|
|
208
|
+
--assignment 8x3abc --attempt a1b2c3 --project ace
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The request binds to the exact ACTIVE MANAGED attempt of the calling
|
|
212
|
+
identity, verified through the ace-assign coordinator under the
|
|
213
|
+
assignment exclusion: a stale, ended, replaced, or uncertain attempt
|
|
214
|
+
cannot acquire authority, and the exclusion is HELD across every
|
|
215
|
+
consume/deliver transition so an attempt cannot end between the
|
|
216
|
+
liveness check and the commit. `--work W... --attempt A-...` remains
|
|
217
|
+
the legacy binding until the provider=lab integration (vs2) switches
|
|
218
|
+
consumers; the two authorities are mutually exclusive and
|
|
219
|
+
`--attempt` is always required (environment variables are never
|
|
220
|
+
attempt identity).
|
|
156
221
|
|
|
157
|
-
|
|
222
|
+
Transport side:
|
|
158
223
|
|
|
159
224
|
```bash
|
|
160
225
|
ace-hitl pending # answerable requests
|
|
@@ -163,15 +228,16 @@ ace-hitl duty # pending + escalated projection
|
|
|
163
228
|
ace-hitl deliver hitl-0a1b2c3d4e5f6708 <<< "approved"
|
|
164
229
|
```
|
|
165
230
|
|
|
166
|
-
`deliver` reads the answer from stdin
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
and
|
|
231
|
+
`deliver` reads the answer from stdin and relays it unchanged. OTP
|
|
232
|
+
bytes go to the service's memory vault; plain answers to the
|
|
233
|
+
service-owned `0600` answers file. Liveness is re-verified under the
|
|
234
|
+
lock, and the declared effect callback (non-OTP kinds only) executes:
|
|
170
235
|
exec-style argv (never a shell), `{answer}` substituted once per
|
|
171
236
|
element, optional fullmatch regex gate, bounded timeout, attempts
|
|
172
|
-
logged redacted in the
|
|
173
|
-
|
|
174
|
-
|
|
237
|
+
logged redacted in the effects log, and one deduped escalation with
|
|
238
|
+
`effect_state: callback-escalated` in the public projection on failure
|
|
239
|
+
(`callback-ok` on success). Duplicate deliveries are idempotent: the
|
|
240
|
+
committed answer is reported without re-running any effect.
|
|
175
241
|
|
|
176
242
|
Requester side:
|
|
177
243
|
|
|
@@ -182,9 +248,12 @@ ace-hitl cancel hitl-0a1b2c3d4e5f6708 --reason "operator stopped the work"
|
|
|
182
248
|
```
|
|
183
249
|
|
|
184
250
|
A consume timeout bounds ONLY the local wait — the request stays
|
|
185
|
-
pending and answerable.
|
|
251
|
+
pending and answerable. OTP consumption requires the challenge's
|
|
252
|
+
authorized operation (`--operation <name>`); the secret transfers
|
|
253
|
+
exactly once and a consumed retry replays the receipt WITHOUT the
|
|
254
|
+
bytes. Cancel is the ONLY way to abandon a request, requester-only;
|
|
186
255
|
it records `cancelled_by` and the `reason` in the public projection,
|
|
187
|
-
and
|
|
256
|
+
and late answers/consumes fail closed against the committed receipt.
|
|
188
257
|
|
|
189
258
|
Overseer reverse address (bounded, type-tagged responses):
|
|
190
259
|
|
|
@@ -18,8 +18,16 @@ module Ace
|
|
|
18
18
|
|
|
19
19
|
option :title, type: :string, desc: "Local HITL event title (defaults to the question)"
|
|
20
20
|
option :provider, type: :string, desc: "HITL provider adapter (default: ACE_HITL_PROVIDER or lab)"
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# Managed binding (spec 8wq.t.34i): the request binds to the
|
|
22
|
+
# exact active managed attempt of the calling identity.
|
|
23
|
+
option :assignment, type: :string, desc: "Managed assignment id (compact id)"
|
|
24
|
+
option :kind, type: :string, desc: "Request kind (default: text; otp requires the challenge evidence)"
|
|
25
|
+
option :"otp-operation", type: :string, desc: "OTP challenge: the ONE authorized operation name"
|
|
26
|
+
option :"otp-result-ref", type: :string, desc: "OTP challenge: OTP-required publisher result reference"
|
|
27
|
+
option :"otp-input-digest", type: :string, desc: "OTP challenge: sha256 input digest of the authorized input"
|
|
28
|
+
option :"otp-expires-at", type: :string, desc: "OTP challenge: expiry as unix seconds (<= 24h ahead)"
|
|
29
|
+
option :work, type: :string, desc: "Lab Work id (W...) - legacy binding until vs2 switches consumers"
|
|
30
|
+
option :attempt, type: :string, desc: "Attempt id of the exact active attempt"
|
|
23
31
|
option :project, type: :string, desc: "Lab project label (default: ace)"
|
|
24
32
|
option :harness, type: :string, desc: "Lab harness label (default: lab-admin)"
|
|
25
33
|
option :plan, type: :string, desc: "Lab plan label (default: ace-hitl ask)"
|
|
@@ -37,8 +45,9 @@ module Ace
|
|
|
37
45
|
effect = build_effect(options)
|
|
38
46
|
validate_effect!(effect)
|
|
39
47
|
|
|
40
|
-
work =
|
|
48
|
+
work, assignment = require_binding!(options)
|
|
41
49
|
attempt = require_attempt!(options)
|
|
50
|
+
otp = build_otp_challenge!(options)
|
|
42
51
|
provider = resolve_provider(options[:provider])
|
|
43
52
|
ref = capture_ref
|
|
44
53
|
|
|
@@ -47,7 +56,10 @@ module Ace
|
|
|
47
56
|
title: options[:title],
|
|
48
57
|
ref: ref,
|
|
49
58
|
work: work,
|
|
59
|
+
assignment: assignment,
|
|
50
60
|
attempt: attempt,
|
|
61
|
+
kind: options[:kind] || "text",
|
|
62
|
+
otp: otp,
|
|
51
63
|
project: options[:project] || Providers::Lab::DEFAULT_PROJECT,
|
|
52
64
|
harness: options[:harness] || Providers::Lab::DEFAULT_HARNESS,
|
|
53
65
|
plan: options[:plan] || Providers::Lab::DEFAULT_PLAN,
|
|
@@ -78,17 +90,48 @@ module Ace
|
|
|
78
90
|
raise_cli_error(e.message)
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
# The binding is authority: exactly one of the managed
|
|
94
|
+
# assignment binding or the legacy Work binding. An
|
|
95
|
+
# environment variable never supplies attempt identity
|
|
96
|
+
# (spec 8wq.t.34i).
|
|
97
|
+
def require_binding!(options)
|
|
82
98
|
work = options[:work]
|
|
83
|
-
|
|
99
|
+
assignment = options[:assignment]
|
|
100
|
+
if work && assignment
|
|
101
|
+
raise_cli_error("--assignment and --work are mutually exclusive binding authorities")
|
|
102
|
+
end
|
|
103
|
+
if work.nil? && assignment.nil?
|
|
104
|
+
raise_cli_error("--assignment required (managed binding), or --work for the legacy Work binding")
|
|
105
|
+
end
|
|
106
|
+
return [work, nil] unless work.nil?
|
|
107
|
+
|
|
108
|
+
[nil, assignment]
|
|
109
|
+
end
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
# The OTP challenge evidence is non-secret, structurally
|
|
112
|
+
# validated store-side; the CLI only assembles it (spec
|
|
113
|
+
# 8wq.t.34i: no publisher result, no OTP request).
|
|
114
|
+
def build_otp_challenge!(options)
|
|
115
|
+
kind = options[:kind] || "text"
|
|
116
|
+
return nil unless kind == "otp"
|
|
117
|
+
|
|
118
|
+
operation = options[:"otp-operation"]
|
|
119
|
+
result_ref = options[:"otp-result-ref"]
|
|
120
|
+
input_digest = options[:"otp-input-digest"]
|
|
121
|
+
expires_at = options[:"otp-expires-at"]
|
|
122
|
+
missing = %w[--otp-operation --otp-result-ref --otp-input-digest --otp-expires-at]
|
|
123
|
+
.zip([operation, result_ref, input_digest, expires_at])
|
|
124
|
+
.select { |_flag, value| value.nil? || value.to_s.strip.empty? }
|
|
125
|
+
.map(&:first)
|
|
126
|
+
raise_cli_error("OTP requests require #{missing.join(", ")} (the OTP-required publisher evidence)") unless missing.empty?
|
|
127
|
+
|
|
128
|
+
{operation: operation, result_ref: result_ref, input_digest: input_digest, expires_at: expires_at}
|
|
86
129
|
end
|
|
87
130
|
|
|
88
131
|
def require_attempt!(options)
|
|
89
|
-
attempt = options[:attempt]
|
|
132
|
+
attempt = options[:attempt]
|
|
90
133
|
unless attempt && !attempt.strip.empty?
|
|
91
|
-
raise_cli_error("--attempt required (
|
|
134
|
+
raise_cli_error("--attempt required (the exact active attempt id)")
|
|
92
135
|
end
|
|
93
136
|
|
|
94
137
|
attempt
|
|
@@ -23,8 +23,8 @@ module Ace
|
|
|
23
23
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
24
24
|
|
|
25
25
|
def call(id:, **options)
|
|
26
|
-
emit(
|
|
27
|
-
rescue Lifecycle::Error => e
|
|
26
|
+
emit(lifecycle_client.cancel(id, reason: options[:reason] || ""))
|
|
27
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
28
28
|
raise_lifecycle_error(e.message)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -9,7 +9,9 @@ module Ace
|
|
|
9
9
|
module Commands
|
|
10
10
|
# Requester-side answer consumption. A positive timeout bounds
|
|
11
11
|
# ONLY the local wait and never cancels the request (W651);
|
|
12
|
-
# without it the wait is indefinite (spec 8wm.t.y21 §3).
|
|
12
|
+
# without it the wait is indefinite (spec 8wm.t.y21 §3). OTP
|
|
13
|
+
# answers require the challenge's authorized operation
|
|
14
|
+
# (spec 8wq.t.34i).
|
|
13
15
|
class Consume < Ace::Support::Cli::Command
|
|
14
16
|
include Ace::Support::Cli::Base
|
|
15
17
|
include LifecycleCommand
|
|
@@ -19,12 +21,13 @@ module Ace
|
|
|
19
21
|
argument :id, required: true, desc: "HITL relay request id"
|
|
20
22
|
|
|
21
23
|
option :timeout, type: :integer, desc: "Local wait bound in seconds; 0 (default) waits indefinitely"
|
|
24
|
+
option :operation, type: :string, desc: "Authorized operation (required for OTP challenges)"
|
|
22
25
|
|
|
23
26
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
24
27
|
|
|
25
28
|
def call(id:, **options)
|
|
26
|
-
emit(
|
|
27
|
-
rescue Lifecycle::Error => e
|
|
29
|
+
emit(lifecycle_client.consume(id, timeout: options[:timeout] || 0, operation: options[:operation]))
|
|
30
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
28
31
|
raise_lifecycle_error(e.message)
|
|
29
32
|
end
|
|
30
33
|
end
|
|
@@ -21,8 +21,9 @@ module Ace
|
|
|
21
21
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
22
22
|
|
|
23
23
|
def call(id:, **options)
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
answer = $stdin.read(Lifecycle::Store::MAX_ANSWER_BYTES + 1).to_s
|
|
25
|
+
emit(lifecycle_client.deliver(id, answer))
|
|
26
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
26
27
|
raise_lifecycle_error(e.message)
|
|
27
28
|
end
|
|
28
29
|
end
|
|
@@ -13,13 +13,13 @@ module Ace
|
|
|
13
13
|
include Ace::Support::Cli::Base
|
|
14
14
|
include LifecycleCommand
|
|
15
15
|
|
|
16
|
-
desc "Project pending and escalated HITL requests (
|
|
16
|
+
desc "Project pending and escalated HITL requests (transport operation)"
|
|
17
17
|
|
|
18
18
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
19
19
|
|
|
20
20
|
def call(**options)
|
|
21
|
-
emit(Lifecycle::Duty.project(
|
|
22
|
-
rescue Lifecycle::Error => e
|
|
21
|
+
emit(Lifecycle::Duty.project(pending: lifecycle_client.pending, states: lifecycle_client.states))
|
|
22
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
23
23
|
raise_lifecycle_error(e.message)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -6,15 +6,17 @@ module Ace
|
|
|
6
6
|
module Hitl
|
|
7
7
|
module CLI
|
|
8
8
|
module Commands
|
|
9
|
-
# Shared plumbing for the
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# Shared plumbing for the lifecycle commands (spec 8wm.t.y21 §8,
|
|
10
|
+
# scoped by 8wq.t.34i): every lifecycle operation reaches the
|
|
11
|
+
# store through the AUTHENTICATED BOUNDARY CLIENT — direct
|
|
12
|
+
# shared-store access is not a CLI option — and machine outputs
|
|
13
|
+
# are one JSON line, byte-compatible with the migrated CLI
|
|
14
|
+
# contract.
|
|
13
15
|
module LifecycleCommand
|
|
14
16
|
STDIN_READER = ->(limit) { $stdin.read(limit) }.freeze
|
|
15
17
|
|
|
16
|
-
def
|
|
17
|
-
Providers::Lab.
|
|
18
|
+
def lifecycle_client
|
|
19
|
+
Providers::Lab.boundary_client
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def overseer
|
|
@@ -13,13 +13,13 @@ module Ace
|
|
|
13
13
|
include Ace::Support::Cli::Base
|
|
14
14
|
include LifecycleCommand
|
|
15
15
|
|
|
16
|
-
desc "List answerable HITL relay requests (
|
|
16
|
+
desc "List answerable HITL relay requests (transport operation)"
|
|
17
17
|
|
|
18
18
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
19
19
|
|
|
20
20
|
def call(**options)
|
|
21
|
-
emit(
|
|
22
|
-
rescue Lifecycle::Error => e
|
|
21
|
+
emit(lifecycle_client.pending)
|
|
22
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
23
23
|
raise_lifecycle_error(e.message)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require_relative "../../lifecycle"
|
|
5
|
+
require_relative "../../providers/providers"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Hitl
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
# The installed scoped store boundary (spec 8wq.t.34i): serves
|
|
12
|
+
# the private HITL store over a peer-authenticated UNIX socket.
|
|
13
|
+
# Authorization facts come only from the trusted grants
|
|
14
|
+
# document; the store root and socket paths come from the
|
|
15
|
+
# deployment (options/environment), never from caller payloads.
|
|
16
|
+
class Serve < Ace::Support::Cli::Command
|
|
17
|
+
include Ace::Support::Cli::Base
|
|
18
|
+
|
|
19
|
+
desc "Run the authenticated HITL store boundary service (peer-credential access)"
|
|
20
|
+
|
|
21
|
+
option :socket, type: :string, desc: "Boundary socket path (default: ACE_HITL_SOCKET or /run/lab/hitl.sock)"
|
|
22
|
+
option :store_root, type: :string, desc: "Private store root (default: ACE_HITL_STORE_ROOT or /run/lab/hitl)"
|
|
23
|
+
option :grants_path, type: :string, desc: "Trusted grants document (default: ACE_HITL_GRANTS_PATH or /etc/lab/ace-lab/authorization.yml)"
|
|
24
|
+
option :repo_root, type: :string, desc: "Candidate repository for the managed attempt authority"
|
|
25
|
+
|
|
26
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
27
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
28
|
+
|
|
29
|
+
def call(**options)
|
|
30
|
+
policy = Providers::Lab.grants_policy(grants_path: grants_path(options))
|
|
31
|
+
unless policy.service_uid
|
|
32
|
+
raise_cli_error(
|
|
33
|
+
"the HITL service requires trusted grants with hitl.service_uid " \
|
|
34
|
+
"(#{grants_path(options)}); serving without a trusted service identity would " \
|
|
35
|
+
"fail every client endpoint verification"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
binding_policy = Providers::Lab::CompositeBinding.new(
|
|
39
|
+
assignment: Providers::Lab.assignment_binding(repo_root: options[:repo_root]),
|
|
40
|
+
work: Providers::Lab::DaemonBinding.new
|
|
41
|
+
)
|
|
42
|
+
service = Lifecycle::Service.new(
|
|
43
|
+
root: store_root(options),
|
|
44
|
+
binding: binding_policy,
|
|
45
|
+
policy: policy,
|
|
46
|
+
socket_path: socket_path(options),
|
|
47
|
+
logger: options[:verbose] ? ->(message) { warn message } : nil
|
|
48
|
+
)
|
|
49
|
+
service.run
|
|
50
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
51
|
+
raise_cli_error(e.message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def socket_path(options)
|
|
57
|
+
options[:socket] || ENV[Providers::Lab::SOCKET_PATH_ENV] || Providers::Lab::DEFAULT_SOCKET_PATH
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def store_root(options)
|
|
61
|
+
options[:store_root] || ENV[Providers::Lab::STORE_ROOT_ENV] || Providers::Lab::DEFAULT_STORE_ROOT
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def grants_path(options)
|
|
65
|
+
options[:grants_path] || ENV[Providers::Lab::GRANTS_PATH_ENV] || Providers::Lab::DEFAULT_GRANTS_PATH
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -12,13 +12,13 @@ module Ace
|
|
|
12
12
|
include Ace::Support::Cli::Base
|
|
13
13
|
include LifecycleCommand
|
|
14
14
|
|
|
15
|
-
desc "List public HITL lifecycle projections (
|
|
15
|
+
desc "List public HITL lifecycle projections (transport operation)"
|
|
16
16
|
|
|
17
17
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
18
18
|
|
|
19
19
|
def call(**options)
|
|
20
|
-
emit(
|
|
21
|
-
rescue Lifecycle::Error => e
|
|
20
|
+
emit(lifecycle_client.states)
|
|
21
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
22
22
|
raise_lifecycle_error(e.message)
|
|
23
23
|
end
|
|
24
24
|
end
|
data/lib/ace/hitl/cli.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "cli/commands/cancel"
|
|
|
14
14
|
require_relative "cli/commands/pending"
|
|
15
15
|
require_relative "cli/commands/states"
|
|
16
16
|
require_relative "cli/commands/duty"
|
|
17
|
+
require_relative "cli/commands/serve"
|
|
17
18
|
require_relative "cli/commands/overseer_send"
|
|
18
19
|
require_relative "cli/commands/overseer_pending"
|
|
19
20
|
require_relative "cli/commands/overseer_ack"
|
|
@@ -35,9 +36,9 @@ module Ace
|
|
|
35
36
|
["deliver", "Deliver an answer (stdin) to a pending HITL relay request"],
|
|
36
37
|
["consume", "Wait for and consume the answer of one own HITL relay request"],
|
|
37
38
|
["cancel", "Cancel one HITL relay request with an audited reason"],
|
|
38
|
-
["pending", "List answerable HITL relay requests (
|
|
39
|
-
["states", "List public HITL lifecycle projections (
|
|
40
|
-
["duty", "Project pending and escalated HITL requests (
|
|
39
|
+
["pending", "List answerable HITL relay requests (transport)"],
|
|
40
|
+
["states", "List public HITL lifecycle projections (transport)"],
|
|
41
|
+
["duty", "Project pending and escalated HITL requests (transport)"],
|
|
41
42
|
["overseer-send", "Queue a bounded, type-tagged Overseer response (stdin)"],
|
|
42
43
|
["overseer-pending", "List queued Overseer responses (host-broker)"],
|
|
43
44
|
["overseer-ack", "Acknowledge one relayed Overseer response"]
|
|
@@ -64,6 +65,7 @@ module Ace
|
|
|
64
65
|
register "pending", CLI::Commands::Pending
|
|
65
66
|
register "states", CLI::Commands::States
|
|
66
67
|
register "duty", CLI::Commands::Duty
|
|
68
|
+
register "serve", CLI::Commands::Serve
|
|
67
69
|
register "overseer-send", CLI::Commands::OverseerSend
|
|
68
70
|
register "overseer-pending", CLI::Commands::OverseerPending
|
|
69
71
|
register "overseer-ack", CLI::Commands::OverseerAck
|
|
@@ -12,7 +12,7 @@ module Ace
|
|
|
12
12
|
class Binding
|
|
13
13
|
# Validate a request creation against the requester's exact live
|
|
14
14
|
# attempt. Raises BindingError on any violation or unavailability.
|
|
15
|
-
def validate_request(work
|
|
15
|
+
def validate_request(work: nil, assignment: nil, attempt:, project:, requester:)
|
|
16
16
|
raise NotImplementedError
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -21,6 +21,17 @@ module Ace
|
|
|
21
21
|
def require_active(work:, attempt:)
|
|
22
22
|
raise NotImplementedError
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Hold verified live authority across ONE whole transition: the
|
|
26
|
+
# check and the caller's locked commit happen inside the same
|
|
27
|
+
# exclusion, so a concurrent attempt termination cannot land
|
|
28
|
+
# between them (spec 8wq.t.34i). The yielded value is opaque.
|
|
29
|
+
# Implementations without a holdable exclusion verify and yield
|
|
30
|
+
# (the legacy gap); the assignment authority holds its lock.
|
|
31
|
+
def with_active(work: nil, assignment: nil, attempt:, project: nil, requester: nil)
|
|
32
|
+
require_active(work: work, attempt: attempt)
|
|
33
|
+
yield
|
|
34
|
+
end
|
|
24
35
|
end
|
|
25
36
|
end
|
|
26
37
|
end
|