ace-hitl 0.11.1 → 0.12.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 +21 -0
- data/README.md +21 -27
- data/docs/usage.md +219 -63
- data/lib/ace/hitl/cli/commands/ask.rb +5 -27
- data/lib/ace/hitl/cli/commands/pending.rb +2 -1
- data/lib/ace/hitl/cli/commands/proposal.rb +73 -0
- data/lib/ace/hitl/cli/commands/serve.rb +1 -4
- data/lib/ace/hitl/cli/commands/update.rb +2 -0
- data/lib/ace/hitl/cli/commands/wait.rb +23 -14
- data/lib/ace/hitl/cli.rb +4 -1
- data/lib/ace/hitl/lifecycle/binding.rb +9 -11
- data/lib/ace/hitl/lifecycle/client.rb +56 -3
- data/lib/ace/hitl/lifecycle/duty.rb +2 -3
- data/lib/ace/hitl/lifecycle/effects.rb +4 -2
- data/lib/ace/hitl/lifecycle/kinds.rb +4 -10
- data/lib/ace/hitl/lifecycle/peer.rb +5 -4
- data/lib/ace/hitl/lifecycle/policy.rb +21 -7
- data/lib/ace/hitl/lifecycle/proposals.rb +279 -0
- data/lib/ace/hitl/lifecycle/protocol.rb +4 -3
- data/lib/ace/hitl/lifecycle/service.rb +35 -5
- data/lib/ace/hitl/lifecycle/store.rb +167 -51
- data/lib/ace/hitl/live_client.rb +144 -0
- data/lib/ace/hitl/organisms/hitl_manager.rb +10 -51
- data/lib/ace/hitl/proposals/evaluator.rb +29 -0
- data/lib/ace/hitl/proposals/policy.rb +125 -0
- data/lib/ace/hitl/providers/lab/assignment_binding.rb +21 -3
- data/lib/ace/hitl/providers/lab.rb +28 -24
- data/lib/ace/hitl/version.rb +1 -1
- data/lib/ace/hitl.rb +1 -1
- metadata +25 -9
- data/lib/ace/hitl/molecules/lab_projection_observer.rb +0 -97
- data/lib/ace/hitl/providers/lab/composite_binding.rb +0 -47
- data/lib/ace/hitl/providers/lab/daemon_binding.rb +0 -210
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f29e133df951a7ba9c8781bc983cea5b72efe298b6544bae9c7a60626cadcdd2
|
|
4
|
+
data.tar.gz: e3078f625d062203d1dd94b946b3fa4503daf49c6029201682dc659c9cc8463e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: edf72377db41e27f64075763543fcb4886357b9647400b634c7e06cf4ccb9285aeb87506e1ffde60f2b594e7e337f826a7beecfef3e8e32d0e28521da984679d
|
|
7
|
+
data.tar.gz: a71bc89e5841a1fa25b2256f7989d1ee2fb4cc830e61ae27935bb84e4f38deed4a9bd51cb5370de61633cecf911309b11219af5798798551d9dd7c7331d17a30
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add an opt-in installed proposal scenario with an isolated local gem closure, real Unix and tmux owner binding, controlled sixteen-hour restart, technical refusal, and one verified service effect/receipt.
|
|
15
|
+
- Resolve immutable second-commander proposals through confirmed-delivery sixteen-hour policy and canonical Assign authorization.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Declare the required direct dependencies and minimum producer versions for this coordinated release: `ace-assign ~> 0.64`, `ace-herdr ~> 0.4`, `ace-hitl-contract ~> 0.2`.
|
|
20
|
+
|
|
21
|
+
- Replace daemon/Work bindings with the managed assignment envelope and kernel-attributed exact native reverse owner. Add explicit in-process delivery/watch, authenticated pane-less wait, visible pending recovery, and existing signed Inbox reconciliation; keep native transport and business effect receipts separate.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Recover revision operations by explicit source revision and stable operation identity without resetting delivery; require current project authority for proposal reads, refuse generic proposal creation, and preserve ordinary question character limits.
|
|
26
|
+
|
|
27
|
+
- Recover stable-ID proposal creation from canonical prepared requests, retain exact reply deduplication across retry ordering, and queue proposer reconciliation wakes for the existing transport actor.
|
|
28
|
+
- Bound pending IPC pages by encoded frame size while retaining all native recovery claims and project authorization; Ruby callers traverse keyset pages.
|
|
29
|
+
- Preserve the accepted native incarnation through answer consumption and require explicit signed-supersession retry before another submission.
|
|
30
|
+
|
|
10
31
|
## [0.11.1] - 2026-10-04
|
|
11
32
|
|
|
12
33
|
### Fixed
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ 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 authenticated boundary (`--assignment/--attempt/--project` managed binding,
|
|
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, 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
|
|
@@ -37,29 +37,23 @@ Use `ace-overseer status` for a global worktree dashboard.
|
|
|
37
37
|
|
|
38
38
|
## Provider adapters
|
|
39
39
|
|
|
40
|
-
`ace-hitl ask
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`UnsupportedOperationError` until the ace-herdr push-delivery
|
|
58
|
-
integration lands. `ace-hitl wait` stays the pane-less script path and
|
|
59
|
-
does not go through a provider.
|
|
60
|
-
- The generic lifecycle is provider-agnostic; all lab coupling lives in
|
|
61
|
-
the provider=lab seams (the `Providers::Lab::DaemonBinding` labd
|
|
62
|
-
binding client and the store factory), enforced by guard tests.
|
|
40
|
+
`ace-hitl ask --question ... --assignment ... --attempt ...` creates a scoped
|
|
41
|
+
request through authenticated IPC. The managed coordinator verifies the exact
|
|
42
|
+
active native owner and reverse address; caller environment variables cannot
|
|
43
|
+
supply authority. No Lab daemon or Work binding is used.
|
|
44
|
+
|
|
45
|
+
`Ace::Hitl::LiveClient` provides explicit in-process `deliver`, `watch`, `wait`,
|
|
46
|
+
`status`, `pending`, and signed `reconcile`. `watch` returns a thread owned by
|
|
47
|
+
its calling agent. Native delivery commits an incarnation-bound event through
|
|
48
|
+
Herdr Inbox and registers it through Assign. Queue acceptance and wake do not
|
|
49
|
+
prove consumption. Only an event-bound signed native observation verified by
|
|
50
|
+
Herdr and journaled by Assign completes delivery. Business callbacks have
|
|
51
|
+
separate authorization and receipts and are never rerun by native retries.
|
|
52
|
+
|
|
53
|
+
Pane-less `wait --request ID` consumes an existing authorized request over IPC;
|
|
54
|
+
it neither creates a native target nor proves a business effect. OTP answers
|
|
55
|
+
use that protected path with their authorized operation; no OTP bytes or OTP
|
|
56
|
+
hash enter the shared envelope, folder or native inbox.
|
|
63
57
|
|
|
64
58
|
## Examples
|
|
65
59
|
|
|
@@ -67,8 +61,8 @@ Use `ace-overseer status` for a global worktree dashboard.
|
|
|
67
61
|
ace-hitl list
|
|
68
62
|
ace-hitl list --scope all
|
|
69
63
|
ace-hitl create "Which auth strategy?" --kind decision --question "JWT or sessions?"
|
|
70
|
-
ace-hitl ask "Proceed with deploy?" --
|
|
71
|
-
ace-hitl ask "Proceed with deploy?" --provider lab --
|
|
64
|
+
ace-hitl ask --question "Proceed with deploy?" --assignment assign685 --attempt attempt685 --effect-arg /bin/false --effect-cwd /tmp
|
|
65
|
+
ace-hitl ask --question "Proceed with deploy?" --provider lab --assignment assign685 --attempt attempt685
|
|
72
66
|
ace-hitl show abc123 --content
|
|
73
67
|
ace-hitl show abc123 --scope current
|
|
74
68
|
ace-hitl update abc123 --answer "Use JWT with server-side refresh tokens."
|
|
@@ -78,7 +72,7 @@ ace-hitl update abc123 --answer "Use JWT with server-side refresh tokens." --res
|
|
|
78
72
|
|
|
79
73
|
## Testing
|
|
80
74
|
|
|
81
|
-
This package
|
|
75
|
+
This package has deterministic fast and scoped integration tests.
|
|
82
76
|
|
|
83
77
|
- Deterministic test coverage lives under `test/fast/`.
|
|
84
78
|
- This migration does not introduce `test/feat/` or `test/e2e/` for this package.
|
data/docs/usage.md
CHANGED
|
@@ -19,7 +19,7 @@ Runtime store default: `.ace-local/hitl/` (legacy `.ace-hitl/` is no longer used
|
|
|
19
19
|
|
|
20
20
|
## Testing
|
|
21
21
|
|
|
22
|
-
`ace-hitl`
|
|
22
|
+
`ace-hitl` includes isolated fast tests and deterministic scoped integration tests.
|
|
23
23
|
|
|
24
24
|
- Deterministic coverage lives under `test/fast/`.
|
|
25
25
|
- This package does not introduce `test/feat/` or `test/e2e/` in this migration.
|
|
@@ -102,47 +102,65 @@ ace-hitl update abc123 --move-to next
|
|
|
102
102
|
ace-hitl update abc123 --answer "close the assignment" --resume
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
## Ask
|
|
106
|
-
|
|
107
|
-
`ace-hitl ask` dispatches through the provider adapter registry
|
|
108
|
-
(`--provider`, default: `ACE_HITL_PROVIDER` env, then `lab`). ONE
|
|
109
|
-
operation: it creates the local HITL event, forwards the question through
|
|
110
|
-
the provider transport bound to the event via `--ace-hitl-id`, and prints
|
|
111
|
-
both ids. Effect declarations are validated client-side (exact bounds)
|
|
112
|
-
and passed through verbatim into the native relay request store.
|
|
105
|
+
## Ask and scoped live client
|
|
113
106
|
|
|
114
107
|
```bash
|
|
115
|
-
ace-hitl ask "Proceed with deploy?" \
|
|
116
|
-
--
|
|
117
|
-
--effect-arg /usr/bin/notify-send "{answer}"
|
|
118
|
-
|
|
108
|
+
ace-hitl ask --question "Proceed with deploy?" \
|
|
109
|
+
--assignment assign685 --attempt attempt685 --project ace \
|
|
110
|
+
--effect-arg /usr/bin/notify-send --effect-arg "{answer}" --effect-cwd /tmp
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Assignment and attempt are required compact managed IDs. The coordinator verifies
|
|
114
|
+
an active owner and its exact native reverse binding, using the kernel peer PID
|
|
115
|
+
and Runtime's existing process ancestry/birth authority. Missing peer PID or
|
|
116
|
+
native evidence refuses an exact-owner claim. Darwin uses LOCAL_PEERPID; Linux
|
|
117
|
+
uses SO_PEERCRED. UID equality alone never selects another attempt. There is no
|
|
118
|
+
Work binding, Lab daemon socket or environment-derived reverse target.
|
|
119
|
+
|
|
120
|
+
An agent explicitly hosts its watcher in its own process:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
client = Ace::Hitl::LiveClient.new(root: checkout_root)
|
|
124
|
+
watcher = client.watch(request: request_id) { |delivery| handle_queue_result(delivery) }
|
|
125
|
+
watcher.value
|
|
126
|
+
client.status(request: request_id)
|
|
119
127
|
```
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
129
|
+
`deliver` consumes an authorized ordinary answer, enqueues one incarnation-bound
|
|
130
|
+
Herdr event, registers its digest/key with Assign, and attempts exact native
|
|
131
|
+
submission. The accepted owner's terminal, agent and immutable native session ID
|
|
132
|
+
are captured before consumption and checked under the Inbox event lock. A thread
|
|
133
|
+
restart in the same pane refuses delivery rather than changing that original
|
|
134
|
+
identity. Repeated calls reuse the same event and never resubmit an uncertain
|
|
135
|
+
intent. A stopped watcher leaves the request/native intent visible in
|
|
136
|
+
`pending --project ace`; it never chooses a new pane or launches a replacement
|
|
137
|
+
watcher. The configured Hermes transport publishes created requests for its
|
|
138
|
+
explicitly registered project channels and owns their Telegram polling.
|
|
139
|
+
|
|
140
|
+
Queue acceptance (`delivered`) and wake are transport facts. Business effects
|
|
141
|
+
run once through the scoped service under the requester's declaration; their
|
|
142
|
+
separate receipt reference cannot be inferred from native delivery. Actual
|
|
143
|
+
consumption requires the existing trusted supervisor/observer signing context:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
client.reconcile(request: request_id, receipt_path: signed_receipt_path)
|
|
147
|
+
# Explicit retry only after verified supersession/non-consumption:
|
|
148
|
+
client.reconcile(request: request_id, receipt_path: signed_receipt_path, retry_delivery: true)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Herdr verifies the signature, exact event/attempt/digest/generation/native binding
|
|
152
|
+
and accepted registration under the event lock. Assign journals the verified
|
|
153
|
+
observation. Missing authority or signer, wrong key, changed target and stale
|
|
154
|
+
proof stay refused/unknown. Signed supersession leaves ordinary `deliver` and
|
|
155
|
+
`watch` calls queued; only `reconcile(..., retry_delivery: true)` submits again.
|
|
156
|
+
No elapsed-time rule establishes success or retries.
|
|
157
|
+
Keep the original trusted verification/signing context for unresolved events or
|
|
158
|
+
defer key rotation; a replacement fingerprint cannot rebind an existing event.
|
|
159
|
+
|
|
160
|
+
The shared versioned envelope is semantically owned by HITL and packaged in
|
|
161
|
+
`ace-hitl-contract` to preserve the acyclic HITL → Assign → Herdr → contract
|
|
162
|
+
graph. Its nested Hermes message and reverse reference are distinct schemas.
|
|
163
|
+
OTP answers and their hashes are excluded from the envelope and native delivery.
|
|
146
164
|
|
|
147
165
|
## Scoped Store Boundary (spec 8wq.t.34i)
|
|
148
166
|
|
|
@@ -204,7 +222,7 @@ root is `ACE_HITL_OVERSEER_CHANNEL_ROOT` (default
|
|
|
204
222
|
Managed binding (the default authority):
|
|
205
223
|
|
|
206
224
|
```bash
|
|
207
|
-
ace-hitl ask "Choose the next scope" \
|
|
225
|
+
ace-hitl ask --question "Choose the next scope" \
|
|
208
226
|
--assignment 8x3abc --attempt a1b2c3 --project ace
|
|
209
227
|
```
|
|
210
228
|
|
|
@@ -213,11 +231,8 @@ identity, verified through the ace-assign coordinator under the
|
|
|
213
231
|
assignment exclusion: a stale, ended, replaced, or uncertain attempt
|
|
214
232
|
cannot acquire authority, and the exclusion is HELD across every
|
|
215
233
|
consume/deliver transition so an attempt cannot end between the
|
|
216
|
-
liveness check and the commit.
|
|
217
|
-
|
|
218
|
-
consumers; the two authorities are mutually exclusive and
|
|
219
|
-
`--attempt` is always required (environment variables are never
|
|
220
|
-
attempt identity).
|
|
234
|
+
liveness check and the commit. Assignment and attempt identity are always explicit;
|
|
235
|
+
there is no legacy Work-only contract.
|
|
221
236
|
|
|
222
237
|
Transport side:
|
|
223
238
|
|
|
@@ -285,25 +300,24 @@ ace-hitl wait abc123 --poll-every 600 --timeout 14400
|
|
|
285
300
|
ace-hitl wait abc123 --scope current
|
|
286
301
|
```
|
|
287
302
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
their pane.
|
|
303
|
+
Managed event waits resolve `lab_request_id` and consume through authenticated
|
|
304
|
+
IPC. They never read folder projections or persist returned answers into the
|
|
305
|
+
local event. Explicit pane-less scripts can wait without any local event:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
ace-hitl wait --request hitl001 --timeout 30
|
|
309
|
+
ace-hitl wait --request otp001 --operation gem-push
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The default managed wait is indefinite; a positive timeout bounds only this
|
|
313
|
+
call. It never expires the request. OTP consumption requires the authorized
|
|
314
|
+
operation and returns its bytes only over protected IPC. Local event polling
|
|
315
|
+
remains available for ordinary unbound local events. Managed events cannot use
|
|
316
|
+
`update --resume` to launch unscoped session/shell delivery.
|
|
317
|
+
|
|
318
|
+
Installed acceptance still requires real registered Telegram ingress, native
|
|
319
|
+
owner observation and trusted signer/OS-user evidence in Lab. Controlled local
|
|
320
|
+
transport and synthetic native observations do not satisfy those gates.
|
|
307
321
|
|
|
308
322
|
## Lifecycle Event Names
|
|
309
323
|
|
|
@@ -317,3 +331,145 @@ Canonical namespace for HITL lifecycle signaling:
|
|
|
317
331
|
- `hitl.event.resume_skipped_waiter_active`
|
|
318
332
|
- `hitl.event.resume_failed`
|
|
319
333
|
- `hitl.event.archived`
|
|
334
|
+
|
|
335
|
+
## Pending recovery history across bounded IPC pages
|
|
336
|
+
|
|
337
|
+
`ace-hitl pending --project ace` continues to show unresolved native delivery
|
|
338
|
+
claims even after their answers have been consumed. A large retained history
|
|
339
|
+
must not prevent newly created questions from reaching Telegram.
|
|
340
|
+
|
|
341
|
+
The lifecycle wire `pending` operation returns `{items, next}`. `next` is an
|
|
342
|
+
exclusive request-ID cursor; send it as `after` with the same project for the
|
|
343
|
+
next page. Each response, including framing, fits the IPC byte limit. Every
|
|
344
|
+
page rechecks transport/project authorization. `Lifecycle::Client#pending`
|
|
345
|
+
collects the pages for existing Ruby/CLI callers; `pending_page(project:, after:)`
|
|
346
|
+
exposes a single bounded page. `read(id)` remains available for exact recovery.
|
|
347
|
+
No consumed claim is deleted to make the list fit. A record too large for one
|
|
348
|
+
page produces a classified error, never a successful truncated list.
|
|
349
|
+
|
|
350
|
+
This is a live keyset scan, not a frozen snapshot: a newly inserted ID before
|
|
351
|
+
the current cursor appears on the next scan. Repeated polling therefore remains
|
|
352
|
+
required; a cursor is not proof of delivery or native consumption.
|
|
353
|
+
|
|
354
|
+
## Immutable second-commander proposals
|
|
355
|
+
|
|
356
|
+
`ace-hitl proposal create proposal-0123456789abcdef01234567 --assignment ID --attempt ID --project ID --file proposal.json`
|
|
357
|
+
returns persisted immutable proposal/revision/request IDs and current awaiting-delivery state immediately, even with unavailable transport. delivered_at and deadline are absent until acknowledged submission; show exposes them afterward. The sole Hermes
|
|
358
|
+
polling actor publishes the full precise proposal and records confirmed submission before
|
|
359
|
+
HITL persists delivered_at and a deadline exactly sixteen hours later. Failed or uncertain
|
|
360
|
+
submission cannot arm the window. A Telegram Reply `approve [rationale]`, `veto [rationale]`
|
|
361
|
+
or `clarify [rationale]` applies only to the correlated immutable revision; other replies
|
|
362
|
+
stop automatic approval and require revision. Missing rationale remains absent.
|
|
363
|
+
|
|
364
|
+
The JSON proposal requires operation, target (`resource` and optional `artifact_digest`),
|
|
365
|
+
candidate_head, input_digest, context, options, recommendation and prerequisites; rationale
|
|
366
|
+
is optional. Contents are bounded and non-secret. Input digest uses the canonical service
|
|
367
|
+
input digest. Candidate head is separate from base head and the evidence journal commit.
|
|
368
|
+
Every precisely presented operation is eligible for sixteen-hour silence, including
|
|
369
|
+
publishing, deployment and access/privilege expansion. Fixed service scope, current head,
|
|
370
|
+
independently executed review/tests, bound running attempt and operation-specific OTP gates
|
|
371
|
+
still apply at execution. Proposal authorization never supplies credentials or runs a callback.
|
|
372
|
+
|
|
373
|
+
`ace-hitl proposal show ID --format json` shows decision, deadline, actual Assign claim/outcome
|
|
374
|
+
and a bounded history page; continue with `--history-after HISTORY_NEXT`.
|
|
375
|
+
`ace-hitl proposal history --project ID --query TEXT` retrieves relevant prior decisions;
|
|
376
|
+
continue with `--after NEXT`. Current project grants and exact requester identity gate history and show; losing project access refuses reads and excludes history, including lifecycle proposal request reads.
|
|
377
|
+
`ace-hitl proposal revise ID --expected-revision N --operation-id revision-abcdef0123456789abcdef01 --file changed.json` supersedes the prior decision and creates a
|
|
378
|
+
new request with a fresh full window after acknowledgement. An unresolved claimed effect
|
|
379
|
+
must be reconciled before revision; known successful or proven no-effect settlement can be
|
|
380
|
+
followed by a new revision. Persist the proposal ID, expected source revision, stable revision operation ID and exact file before invocation. The operation ID is `revision-` followed by 24 lowercase hex digits and is unique across proposals. Exact retry returns that committed revision, including after acknowledged delivery, approval or a later revision, without creating another request or resetting the window. Changed proposal/source/content/caller bindings and stale-source new operations are refused. Supersession and new prepared revision commit atomically against effect claims. Generic lifecycle `create` rejects proposal kind; use this canonical proposal interface.
|
|
381
|
+
|
|
382
|
+
Set ACE_HITL_SOCKET and ACE_HITL_PROJECT for the living overseer. It calls
|
|
383
|
+
`ace-hitl proposal resolve-due --project PROJECT` on start/status/watch ticks.
|
|
384
|
+
The authenticated proposer queues an idempotent reconciliation wake in the
|
|
385
|
+
canonical Assign proposal; the command returns `queued-for-transport`, never an
|
|
386
|
+
approval claim. The existing installed Hermes `serve` loop polls Telegram under
|
|
387
|
+
its own configured transport UID and reconciles queued deadlines after polling.
|
|
388
|
+
No proposer subprocess opens transport configuration or impersonates transport.
|
|
389
|
+
Hermes holds its ingress lock across checkpoint and HITL decision transition;
|
|
390
|
+
unknown health/backlog defers resolution. Failed ticks remain visible while
|
|
391
|
+
watch/status continues. Production `--now` is rejected.
|
|
392
|
+
|
|
393
|
+
Creation requires an explicit stable ID (`proposal-` followed by 24 lowercase
|
|
394
|
+
hex digits). Persist that ID before invocation and retry the exact same ID,
|
|
395
|
+
assignment, attempt, caller and document after failure or a lost reply. Changed
|
|
396
|
+
binding/content is refused. Assign commits the immutable prepared lifecycle
|
|
397
|
+
request first; exact retry or the transport pending scan materializes it after
|
|
398
|
+
restart. No pending lifecycle orphan exists before canonical commit. Concurrent
|
|
399
|
+
materialization creates once and preserves current delivered/answered state.
|
|
400
|
+
|
|
401
|
+
Unresolved earlier same-request ingress blocks later approval delivery. Exact
|
|
402
|
+
reply sequence/content/time deduplication uses canonical decision history;
|
|
403
|
+
unseen lower sequence is applied, and changed duplicate content is refused.
|
|
404
|
+
History grows in the existing canonical event chain, while public responses use
|
|
405
|
+
bounded pages. Hermes metadata never stores message bodies.
|
|
406
|
+
|
|
407
|
+
The immutable authorization reference is the revision ID, passed to `ace-lab service request`.
|
|
408
|
+
Assign atomically checks the canonical proposal under its sole journal claim lock/CAS;
|
|
409
|
+
a forged prefix/YAML string or changed operation/target/head/input/caller cannot authorize.
|
|
410
|
+
A late veto before claim denies execution. After claim it records stop_requested without
|
|
411
|
+
rewriting performed effects; the service rechecks it before safely stoppable invocation.
|
|
412
|
+
An uncertain claim remains uncertain on restart and is never automatically dispatched again.
|
|
413
|
+
Canonical sanitized decision history shares the qjl evidence ref; no parallel executor or
|
|
414
|
+
execution ledger exists in HITL. This source workflow is not installed/native/Telegram or
|
|
415
|
+
multi-UID acceptance proof.
|
|
416
|
+
|
|
417
|
+
### Proposal admission configuration
|
|
418
|
+
|
|
419
|
+
The protected HITL service reads proposer admission from the same trusted grants
|
|
420
|
+
policy as transport and project authorization. For example:
|
|
421
|
+
|
|
422
|
+
```yaml
|
|
423
|
+
hitl:
|
|
424
|
+
service_uid: 1200
|
|
425
|
+
transport_uids: [1201]
|
|
426
|
+
proposal_uids: [1202]
|
|
427
|
+
authorization:
|
|
428
|
+
principals:
|
|
429
|
+
"1201":
|
|
430
|
+
projects: [ace]
|
|
431
|
+
"1202":
|
|
432
|
+
projects: [ace]
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`proposal_uids` admits authenticated kernel peers to create and revise proposals
|
|
436
|
+
only for their configured projects. Transport admission alone cannot create a
|
|
437
|
+
proposal; proposer admission cannot acknowledge delivery, fabricate ingress, or
|
|
438
|
+
resolve silence. Missing role or project admission refuses the operation,
|
|
439
|
+
including direct library calls. This is a source configuration contract;
|
|
440
|
+
installed deployment adoption remains part of gad.8 acceptance.
|
|
441
|
+
|
|
442
|
+
### Controlled installed proposal verification
|
|
443
|
+
|
|
444
|
+
From an ACE checkout with Ruby, tmux and the complete dependency archives cached locally:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
bin/ace-test ace-hitl edge --filter installed_proposal_test --config-path "$PWD/ace-hitl/test/support/installed_proposal/runner.yml"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The explicit fixture configuration activates the installed scenario without changing
|
|
451
|
+
test deadlines. Default package runs skip it. The run builds the current runtime
|
|
452
|
+
gem closure and installs it into an empty GEM_HOME, using local archives only.
|
|
453
|
+
It prints `Installed SC3 artifacts: /tmp/ace-installed-sc3/run-...`; read the
|
|
454
|
+
reported test receipt and that directory's `result.json`, `artifacts.json`,
|
|
455
|
+
`build-provenance.json`, `processes.jsonl` and `canonical-evidence.json`. A manifest/result pointer is
|
|
456
|
+
retained under checkout `.ace-local/installed-sc3/`.
|
|
457
|
+
|
|
458
|
+
The expected terminal result is one passing test with no failures/errors. Its
|
|
459
|
+
consumer asserts confirmed submission + sixteen hours, actual service/actor
|
|
460
|
+
process restarts, one effective authorization and one executor invocation with
|
|
461
|
+
a canonical verified receipt. Failed submission does not arm a deadline; lost
|
|
462
|
+
poll coverage stays blocked, including after fresh polling. Changed technical
|
|
463
|
+
scope remains refused after approval. No inbox record is manually inserted.
|
|
464
|
+
|
|
465
|
+
UTC and Telegram are test-only injected adapters. Assignment binding, kernel peer
|
|
466
|
+
authentication, policy, claim, execution and receipt verification remain real.
|
|
467
|
+
The fixture uses one host and the current UID, with an isolated tmux process and
|
|
468
|
+
the existing local service mode. It does not prove live Telegram, protected Herdr
|
|
469
|
+
launch, installed root-owned grants, or multiUID privilege separation. There is
|
|
470
|
+
no production `--now` or transport-override option.
|
|
471
|
+
|
|
472
|
+
Missing local dependency archives fail visibly without network fallback. A failed
|
|
473
|
+
consumer retains diagnostic artifacts; inspect `result.json` and `service.log`
|
|
474
|
+
before retrying. Each invocation retains a new run directory and requires a new
|
|
475
|
+
empty GEM_HOME.
|
|
@@ -14,7 +14,7 @@ module Ace
|
|
|
14
14
|
|
|
15
15
|
desc "Ask a human via HITL and forward the request through a provider adapter"
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
option :question, type: :string, required: true, desc: "Question text for the human"
|
|
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)"
|
|
@@ -26,7 +26,6 @@ module Ace
|
|
|
26
26
|
option :"otp-result-ref", type: :string, desc: "OTP challenge: OTP-required publisher result reference"
|
|
27
27
|
option :"otp-input-digest", type: :string, desc: "OTP challenge: sha256 input digest of the authorized input"
|
|
28
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
29
|
option :attempt, type: :string, desc: "Attempt id of the exact active attempt"
|
|
31
30
|
option :project, type: :string, desc: "Lab project label (default: ace)"
|
|
32
31
|
option :harness, type: :string, desc: "Lab harness label (default: lab-admin)"
|
|
@@ -45,17 +44,14 @@ module Ace
|
|
|
45
44
|
effect = build_effect(options)
|
|
46
45
|
validate_effect!(effect)
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
assignment = require_binding!(options)
|
|
49
48
|
attempt = require_attempt!(options)
|
|
50
49
|
otp = build_otp_challenge!(options)
|
|
51
50
|
provider = resolve_provider(options[:provider])
|
|
52
|
-
ref = capture_ref
|
|
53
51
|
|
|
54
52
|
result = provider.ask(
|
|
55
53
|
question: question,
|
|
56
54
|
title: options[:title],
|
|
57
|
-
ref: ref,
|
|
58
|
-
work: work,
|
|
59
55
|
assignment: assignment,
|
|
60
56
|
attempt: attempt,
|
|
61
57
|
kind: options[:kind] || "text",
|
|
@@ -67,6 +63,7 @@ module Ace
|
|
|
67
63
|
)
|
|
68
64
|
|
|
69
65
|
puts "HITL event: #{result.event_id}"
|
|
66
|
+
ref = result.ref
|
|
70
67
|
puts "Provider: #{Providers::Lab::PROVIDER_NAME} (ref #{ref.session}/#{ref.pane}, #{Providers::Ref::SCHEMA})"
|
|
71
68
|
puts "Lab request: #{result.request_id}"
|
|
72
69
|
rescue Providers::ProviderUnavailableError => e
|
|
@@ -90,22 +87,10 @@ module Ace
|
|
|
90
87
|
raise_cli_error(e.message)
|
|
91
88
|
end
|
|
92
89
|
|
|
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
90
|
def require_binding!(options)
|
|
98
|
-
work = options[:work]
|
|
99
91
|
assignment = options[:assignment]
|
|
100
|
-
if
|
|
101
|
-
|
|
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]
|
|
92
|
+
raise_cli_error("--assignment required (the managed assignment id)") if assignment.nil? || assignment.strip.empty?
|
|
93
|
+
assignment
|
|
109
94
|
end
|
|
110
95
|
|
|
111
96
|
# The OTP challenge evidence is non-secret, structurally
|
|
@@ -144,13 +129,6 @@ module Ace
|
|
|
144
129
|
raise_cli_error(e.message)
|
|
145
130
|
end
|
|
146
131
|
|
|
147
|
-
# Fail closed BEFORE any state is created: the reverse address is
|
|
148
|
-
# required so the answer can be delivered back to this pane.
|
|
149
|
-
def capture_ref
|
|
150
|
-
Providers::Ref.from_env
|
|
151
|
-
rescue Providers::InvalidRefError => e
|
|
152
|
-
raise_cli_error(e.message)
|
|
153
|
-
end
|
|
154
132
|
end
|
|
155
133
|
end
|
|
156
134
|
end
|
|
@@ -16,9 +16,10 @@ module Ace
|
|
|
16
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
|
+
option :project, type: :string, desc: "Show requests for one authorized project"
|
|
19
20
|
|
|
20
21
|
def call(**options)
|
|
21
|
-
emit(lifecycle_client.pending)
|
|
22
|
+
emit(LiveClient.new(boundary: lifecycle_client).pending(project: options[:project]))
|
|
22
23
|
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
23
24
|
raise_lifecycle_error(e.message)
|
|
24
25
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require_relative "lifecycle_command"
|
|
5
|
+
require_relative "../../proposals/evaluator"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Hitl
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class Proposal < Ace::Support::Cli::Command
|
|
12
|
+
include Ace::Support::Cli::Base
|
|
13
|
+
include LifecycleCommand
|
|
14
|
+
argument :operation, required: true, desc: "create, show, revise, history or resolve-due"
|
|
15
|
+
argument :id, required: false, desc: "Exact proposal ID"
|
|
16
|
+
option :assignment, type: :string
|
|
17
|
+
option :attempt, type: :string
|
|
18
|
+
option :project, type: :string
|
|
19
|
+
option :file, type: :string
|
|
20
|
+
option :"expected-revision", type: :integer, desc: "Exact source revision for revise"
|
|
21
|
+
option :"operation-id", type: :string, desc: "Caller-persisted revision- plus 24 lowercase hex digits"
|
|
22
|
+
option :format, type: :string, default: "json"
|
|
23
|
+
option :now, type: :string, desc: "Unavailable in production; inject a fixture clock in tests"
|
|
24
|
+
option :"history-after", type: :integer, desc: "Read the next bounded history page from history_next"
|
|
25
|
+
option :query, type: :string, desc: "Relevant prior decisions containing this text"
|
|
26
|
+
option :after, type: :string, desc: "History-list cursor from next"
|
|
27
|
+
|
|
28
|
+
def call(operation:, id: nil, **options)
|
|
29
|
+
raise_lifecycle_error("only --format json is supported") unless options[:format] == "json"
|
|
30
|
+
raise_lifecycle_error("--now is restricted to injected test fixtures; production uses trusted UTC clock") if options[:now]
|
|
31
|
+
result = case operation
|
|
32
|
+
when "create"
|
|
33
|
+
%i[assignment attempt project].each { |key| raise_lifecycle_error("--#{key} required") if options[key].to_s.empty? }
|
|
34
|
+
raise_lifecycle_error("stable proposal ID required") if id.to_s.empty?
|
|
35
|
+
lifecycle_client.proposal_create(id: id, assignment: options[:assignment], attempt: options[:attempt],
|
|
36
|
+
project: options[:project], document: load_document(options[:file]))
|
|
37
|
+
when "show"
|
|
38
|
+
raise_lifecycle_error("proposal ID required") if id.to_s.empty?
|
|
39
|
+
lifecycle_client.proposal_show(id, history_after: options[:"history-after"] || 0)
|
|
40
|
+
when "revise"
|
|
41
|
+
raise_lifecycle_error("proposal ID required") if id.to_s.empty?
|
|
42
|
+
raise_lifecycle_error("--expected-revision required") unless options[:"expected-revision"]
|
|
43
|
+
raise_lifecycle_error("--operation-id required") if options[:"operation-id"].to_s.empty?
|
|
44
|
+
lifecycle_client.proposal_revise(id, expected_revision: options[:"expected-revision"],
|
|
45
|
+
operation_id: options[:"operation-id"], document: load_document(options[:file]))
|
|
46
|
+
when "resolve-due"
|
|
47
|
+
raise_lifecycle_error("--project required") if options[:project].to_s.empty?
|
|
48
|
+
Proposals::Evaluator.new(boundary: lifecycle_client, project: options[:project]).call
|
|
49
|
+
when "history"
|
|
50
|
+
raise_lifecycle_error("--project required") if options[:project].to_s.empty?
|
|
51
|
+
lifecycle_client.proposal_history(project: options[:project], query: options[:query] || "", after: options[:after])
|
|
52
|
+
else raise_lifecycle_error("choose create, show, revise, history or resolve-due")
|
|
53
|
+
end
|
|
54
|
+
emit(result)
|
|
55
|
+
rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
|
|
56
|
+
raise_lifecycle_error(e.message)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def load_document(path)
|
|
62
|
+
raise_lifecycle_error("--file required") if path.to_s.empty?
|
|
63
|
+
content = File.open(path, "rb") { |file| file.read(4097) }
|
|
64
|
+
raise_lifecycle_error("proposal file exceeds 4096 bytes") if content.bytesize > 4096
|
|
65
|
+
JSON.parse(content)
|
|
66
|
+
rescue JSON::ParserError, SystemCallError
|
|
67
|
+
raise_lifecycle_error("proposal file must be readable JSON")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -35,10 +35,7 @@ module Ace
|
|
|
35
35
|
"fail every client endpoint verification"
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
|
-
binding_policy = Providers::Lab
|
|
39
|
-
assignment: Providers::Lab.assignment_binding(repo_root: options[:repo_root]),
|
|
40
|
-
work: Providers::Lab::DaemonBinding.new
|
|
41
|
-
)
|
|
38
|
+
binding_policy = Providers::Lab.assignment_binding(repo_root: options[:repo_root])
|
|
42
39
|
service = Lifecycle::Service.new(
|
|
43
40
|
root: store_root(options),
|
|
44
41
|
binding: binding_policy,
|