ace-hitl 0.15.0 → 0.16.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/.ace-defaults/hitl/config.yml +0 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-hitl.yml +0 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-hitl.yml +0 -0
- data/CHANGELOG.md +10 -0
- data/Rakefile +0 -0
- data/docs/demo/ace-hitl-scope-and-answer.tape.yml +0 -0
- data/docs/demo/fixtures/demo-context.md +0 -0
- data/docs/usage.md +12 -2
- data/handbook/README.md +0 -0
- data/handbook/skills/as-hitl/SKILL.md +0 -0
- data/handbook/workflow-instructions/hitl.wf.md +0 -0
- data/lib/ace/hitl/atoms/hitl_effect_validator.rb +0 -0
- data/lib/ace/hitl/atoms/hitl_file_pattern.rb +0 -0
- data/lib/ace/hitl/atoms/hitl_id_formatter.rb +0 -0
- data/lib/ace/hitl/cli/commands/create.rb +0 -0
- data/lib/ace/hitl/cli/commands/list.rb +0 -0
- data/lib/ace/hitl/cli/commands/serve.rb +1 -0
- data/lib/ace/hitl/cli/commands/show.rb +0 -0
- data/lib/ace/hitl/lifecycle/policy.rb +34 -3
- data/lib/ace/hitl/lifecycle/proposals.rb +12 -0
- data/lib/ace/hitl/lifecycle/service.rb +5 -3
- data/lib/ace/hitl/lifecycle/tls_access_policy.rb +12 -1
- data/lib/ace/hitl/lifecycle/tls_authority.rb +14 -5
- data/lib/ace/hitl/lifecycle/tls_service.rb +4 -3
- data/lib/ace/hitl/models/hitl_event.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_answer_editor.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_config_loader.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_creator.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_display_formatter.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_loader.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_resolver.rb +0 -0
- data/lib/ace/hitl/molecules/hitl_scanner.rb +0 -0
- data/lib/ace/hitl/molecules/resume_dispatcher.rb +0 -0
- data/lib/ace/hitl/molecules/worktree_scope_resolver.rb +0 -0
- data/lib/ace/hitl/proposals/policy.rb +1 -0
- data/lib/ace/hitl/providers/lab/assignment_binding.rb +20 -1
- data/lib/ace/hitl/version.rb +1 -1
- metadata +20 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d2bae48cdaf02e617f52c254eb78f5a8dae316be06a8787d4282041c01eacc3
|
|
4
|
+
data.tar.gz: 81410404b3ad0e07900497a9f62594aba56aaf987ed2d816d208bcab112b096c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5ad8fad6083826ca4f1fb6220d7bf87bd302482bbbe946cc93778bf83279d3cf728898e9f2c9bb286ebac9e98a278b5129ab3914d1ee7b354c2befd07400129
|
|
7
|
+
data.tar.gz: 1b0bae700322c0fae15a9f6ab81a7efd5faad863db9f729115909c95e66087be26b2217ad037e5e21ea3a7e7dc7bd418afa3dd1b01654f8f5fcda36007fd5b3d
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Require explicit predelegation for sixteen-hour proposal decisions and bind ordinary project proposals to the selected canonical owner. Refresh project and owner grants at transitions and replay; compose existing Unix/TLS listeners under the authority lifetime.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Read TLS authority configuration through bounded protected descriptors, rejecting symlinks, unsafe ancestors, special files and concurrent replacement before startup. Declare the direct Runtime dependency used by this reader.
|
|
19
|
+
|
|
10
20
|
## [0.15.0] - 2026-10-14
|
|
11
21
|
|
|
12
22
|
### Added
|
data/Rakefile
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/docs/usage.md
CHANGED
|
@@ -358,8 +358,11 @@ The JSON proposal requires operation, target (`resource` and optional `artifact_
|
|
|
358
358
|
candidate_head, input_digest, context, options, recommendation and prerequisites; rationale
|
|
359
359
|
is optional. Contents are bounded and non-secret. Input digest uses the canonical service
|
|
360
360
|
input digest. Candidate head is separate from base head and the evidence journal commit.
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
Only operation classes explicitly delegated in trusted project policy are eligible
|
|
362
|
+
for sixteen-hour silence. Delegation must exist when the revision is created and
|
|
363
|
+
remain current at the deadline; a later grant cannot approve an older revision.
|
|
364
|
+
Without delegation the proposal still accepts an explicit correlated decision.
|
|
365
|
+
Fixed service scope, current head,
|
|
363
366
|
independently executed review/tests, bound running attempt and operation-specific OTP gates
|
|
364
367
|
still apply at execution. Proposal authorization never supplies credentials or runs a callback.
|
|
365
368
|
|
|
@@ -423,6 +426,8 @@ hitl:
|
|
|
423
426
|
service_uid: 1200
|
|
424
427
|
transport_uids: [1201]
|
|
425
428
|
proposal_uids: [1202]
|
|
429
|
+
silence_operations:
|
|
430
|
+
ace: [merge]
|
|
426
431
|
authorization:
|
|
427
432
|
principals:
|
|
428
433
|
"1201":
|
|
@@ -438,6 +443,11 @@ resolve silence. Missing role or project admission refuses the operation,
|
|
|
438
443
|
including direct library calls. This is a source configuration contract;
|
|
439
444
|
installed deployment adoption remains part of gad.8 acceptance.
|
|
440
445
|
|
|
446
|
+
`silence_operations` is an exact project-to-operation list, with no wildcards or
|
|
447
|
+
implicit publish/deploy/access grants. It never expands command or credential
|
|
448
|
+
authority. TLS authorities read the same trusted grants document selected by
|
|
449
|
+
`--grants-path`; certificate authentication alone grants no silence decision.
|
|
450
|
+
|
|
441
451
|
### Controlled installed proposal verification
|
|
442
452
|
|
|
443
453
|
From an ACE checkout with Ruby, tmux and the complete dependency archives cached locally:
|
data/handbook/README.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -69,6 +69,7 @@ module Ace
|
|
|
69
69
|
binding: binding_policy,
|
|
70
70
|
project: authority.project_id,
|
|
71
71
|
credential: authority.credential,
|
|
72
|
+
decision_policy: Providers::Lab.grants_policy(grants_path: grants_path(options)),
|
|
72
73
|
ssl_context: authority.ssl_context!,
|
|
73
74
|
listen: authority.listen,
|
|
74
75
|
port: authority.port,
|
|
File without changes
|
|
@@ -105,6 +105,14 @@ module Ace
|
|
|
105
105
|
def proposal?(_peer, project:)
|
|
106
106
|
false
|
|
107
107
|
end
|
|
108
|
+
|
|
109
|
+
def proposal_silence?(caller_uid:, requester:, project:, operation:)
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def silence_operation?(project:, operation:)
|
|
114
|
+
false
|
|
115
|
+
end
|
|
108
116
|
# @return [Boolean] true when the peer may deliver answers and
|
|
109
117
|
# observe pending/states for the given project
|
|
110
118
|
def transport?(_peer, project: nil)
|
|
@@ -151,13 +159,36 @@ module Ace
|
|
|
151
159
|
# an ordinary requester or transport grant.
|
|
152
160
|
def proposal?(peer, project:)
|
|
153
161
|
current = document
|
|
162
|
+
proposal_principal?(peer.uid, project, current)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# A proposer grant allows asking. Only this separate, exact project
|
|
166
|
+
# operation list delegates a decision after the acknowledged window.
|
|
167
|
+
def proposal_silence?(caller_uid:, requester:, project:, operation:)
|
|
168
|
+
current = document
|
|
169
|
+
proposal_principal?(caller_uid, project, current) &&
|
|
170
|
+
delegated_operation?(project, operation, current)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def silence_operation?(project:, operation:)
|
|
174
|
+
delegated_operation?(project, operation, document)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def proposal_principal?(uid, project, current)
|
|
154
180
|
uids = hitl_facts(current)["proposal_uids"]
|
|
155
|
-
projects = authorized_projects(
|
|
156
|
-
uids.is_a?(Array) && uids.include?(
|
|
181
|
+
projects = authorized_projects(uid, current)
|
|
182
|
+
uid.is_a?(Integer) && uids.is_a?(Array) && uids.include?(uid) && projects &&
|
|
157
183
|
(projects.empty? || projects.include?(project.to_s))
|
|
158
184
|
end
|
|
159
185
|
|
|
160
|
-
|
|
186
|
+
def delegated_operation?(project, operation, current)
|
|
187
|
+
delegations = hitl_facts(current)["silence_operations"]
|
|
188
|
+
operations = delegations.is_a?(Hash) && delegations[project]
|
|
189
|
+
operations.is_a?(Array) && operation.is_a?(String) &&
|
|
190
|
+
operation.match?(/\A[a-z][a-z0-9-]{0,63}\z/) && operations.include?(operation)
|
|
191
|
+
end
|
|
161
192
|
|
|
162
193
|
def document
|
|
163
194
|
@document || TrustedFile.read_yaml(@grants_path) || {}
|
|
@@ -54,6 +54,7 @@ module Ace
|
|
|
54
54
|
claims = proposal_journal(project).proposal_claims(record["revision_id"])
|
|
55
55
|
claim = claims.last
|
|
56
56
|
outcome = case claim && claim["state"]
|
|
57
|
+
when "pending" then "queued"
|
|
57
58
|
when "accepted" then "executing"
|
|
58
59
|
when "succeeded" then "succeeded"
|
|
59
60
|
when "failed", "failed-settled", "rejected" then "failed"
|
|
@@ -88,6 +89,7 @@ module Ace
|
|
|
88
89
|
record = proposal_for_request!(request, project: project)
|
|
89
90
|
require_transport!("proposal reconciliation", {"project" => record["project_id"]})
|
|
90
91
|
proposal_transition(record) do |current|
|
|
92
|
+
next current unless silence_delegated?(current)
|
|
91
93
|
Ace::Hitl::Proposals::Policy.resolve(current, checkpoint: checkpoint, now: proposal_now)
|
|
92
94
|
end
|
|
93
95
|
end
|
|
@@ -136,6 +138,11 @@ module Ace
|
|
|
136
138
|
|
|
137
139
|
private
|
|
138
140
|
|
|
141
|
+
def silence_delegated?(record)
|
|
142
|
+
@policy.proposal_silence?(caller_uid: record["caller_uid"], requester: record["requester"],
|
|
143
|
+
project: record["project_id"], operation: record["operation"])
|
|
144
|
+
end
|
|
145
|
+
|
|
139
146
|
def require_proposer!(project)
|
|
140
147
|
unless @policy.respond_to?(:proposal?) && @policy.proposal?(@identity, project: project)
|
|
141
148
|
raise PermissionError, "proposal requires the installed second-commander decision principal"
|
|
@@ -189,6 +196,8 @@ module Ace
|
|
|
189
196
|
def proposal_transition(record, &policy)
|
|
190
197
|
proposal_journal(record.fetch("project_id")).change_proposal(record["proposal_id"], assignment_id: record["assignment_id"],
|
|
191
198
|
attempt_id: record["attempt_id"]) do |current|
|
|
199
|
+
# Selection may change while waiting for the canonical ref lock.
|
|
200
|
+
proposal_journal(record.fetch("project_id"))
|
|
192
201
|
# A reply to an older revision cannot modify the new decision.
|
|
193
202
|
next current unless current["request_id"] == record["request_id"]
|
|
194
203
|
policy.call(current)
|
|
@@ -203,6 +212,7 @@ module Ace
|
|
|
203
212
|
replay = nil
|
|
204
213
|
record = @binding.with_active(assignment: assignment, attempt: attempt, project: project, requester: @identity.username) do
|
|
205
214
|
proposal_journal(project).change_proposal(id, assignment_id: assignment, attempt_id: attempt) do |previous|
|
|
215
|
+
proposal_journal(project)
|
|
206
216
|
if operation_id
|
|
207
217
|
replay = proposal_revision_replay(id, expected_revision, operation_id, content, project: project)
|
|
208
218
|
next previous if replay
|
|
@@ -228,6 +238,8 @@ module Ace
|
|
|
228
238
|
"attempt_id" => attempt, "project_id" => project, "requester" => @identity.username,
|
|
229
239
|
"caller_uid" => @identity.uid, "content_digest" => Digest::SHA256.hexdigest(JSON.generate(content)),
|
|
230
240
|
"state" => "awaiting-delivery", "created_at" => proposal_now,
|
|
241
|
+
"silence_delegated" => @policy.proposal_silence?(caller_uid: @identity.uid,
|
|
242
|
+
requester: @identity.username, project: project, operation: content.fetch("operation")),
|
|
231
243
|
"authorization" => request, "lifecycle_request" => projection).tap do |value|
|
|
232
244
|
if operation_id
|
|
233
245
|
value["revision_operation_id"] = operation_id
|
|
@@ -186,10 +186,12 @@ module Ace
|
|
|
186
186
|
# the service identity or root — otherwise anyone who can replace
|
|
187
187
|
# the socket can impersonate the service.
|
|
188
188
|
def verify_control_group!
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
# Named broad groups are forbidden even on a host where that
|
|
190
|
+
# deployment group has not been created yet.
|
|
191
|
+
group = Etc.getgrnam(@group) unless BROAD_CONTROL_GROUPS.include?(@group)
|
|
192
|
+
if group.nil? || group.gid.zero? || BROAD_CONTROL_GROUPS.include?(group.name)
|
|
191
193
|
raise TransportError,
|
|
192
|
-
"HITL boundary control group is too broad: #{group
|
|
194
|
+
"HITL boundary control group is too broad: #{@group} " \
|
|
193
195
|
"(configure a dedicated lifecycle group; root and #{Store::DEFAULT_GROUP} are refused)"
|
|
194
196
|
end
|
|
195
197
|
|
|
@@ -10,8 +10,9 @@ module Ace
|
|
|
10
10
|
# even when issued by the same CA, and no operation against any
|
|
11
11
|
# other project is reachable through this authority.
|
|
12
12
|
class TlsAccessPolicy < AccessPolicy
|
|
13
|
-
def initialize(project:)
|
|
13
|
+
def initialize(project:, decision_policy: AccessPolicy.new)
|
|
14
14
|
@project = project.to_s
|
|
15
|
+
@decision_policy = decision_policy
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def transport?(peer, project: nil)
|
|
@@ -22,6 +23,16 @@ module Ace
|
|
|
22
23
|
transport?(peer, project: project)
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
def proposal_silence?(caller_uid:, requester:, project:, operation:)
|
|
27
|
+
return false unless project == @project
|
|
28
|
+
# The transport peer is authenticated separately. A local proposal
|
|
29
|
+
# retains its original kernel principal when Hermes reconciles it.
|
|
30
|
+
return @decision_policy.proposal_silence?(caller_uid: caller_uid, requester: requester,
|
|
31
|
+
project: project, operation: operation) unless caller_uid.nil?
|
|
32
|
+
requester == "urn:ace:hitl:#{@project}:hermes-client" &&
|
|
33
|
+
@decision_policy.silence_operation?(project: project, operation: operation)
|
|
34
|
+
end
|
|
35
|
+
|
|
25
36
|
# A TLS authority has no Unix service uid; client endpoint
|
|
26
37
|
# verification is certificate-based.
|
|
27
38
|
def service_uid
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ipaddr"
|
|
4
4
|
require "json"
|
|
5
|
+
require "ace/runtime/molecules/protected_artifact_set"
|
|
5
6
|
require_relative "tls_credentials"
|
|
6
7
|
require_relative "credential_path_policy"
|
|
7
8
|
require_relative "tls_material"
|
|
@@ -22,16 +23,24 @@ module Ace
|
|
|
22
23
|
SCHEMA = "ace.hitl.tls.authority/v1"
|
|
23
24
|
FIELDS = %w[schema project_id listen port service_user service_group
|
|
24
25
|
tls_server_credential].freeze
|
|
26
|
+
MAX_BYTES = 65_536
|
|
25
27
|
|
|
26
28
|
attr_reader :project_id, :listen, :port, :service_user, :service_group,
|
|
27
29
|
:credential, :path
|
|
28
30
|
|
|
29
31
|
def self.load(path)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
reader = Ace::Runtime::Molecules::ProtectedArtifactSet.new(file_limit: MAX_BYTES, total_limit: MAX_BYTES)
|
|
33
|
+
reader.with do |held|
|
|
34
|
+
bytes, = held.read_path!(path)
|
|
35
|
+
config = JSON.parse(bytes, create_additions: false, allow_duplicate_key: false, max_nesting: 16)
|
|
36
|
+
authority = new(config, path: path)
|
|
37
|
+
CredentialPathPolicy.verify_runtime_file!(path, service_group: authority.service_group)
|
|
38
|
+
# Exact mode/group validation and parsing must refer to the same
|
|
39
|
+
# bounded protected inode, including every retained ancestor.
|
|
40
|
+
held.verify_unchanged!
|
|
41
|
+
authority
|
|
42
|
+
end
|
|
43
|
+
rescue JSON::ParserError, SystemCallError, IOError, Ace::Runtime::Error
|
|
35
44
|
raise TransportError, "the HITL TLS authority configuration is unavailable or malformed: #{path}"
|
|
36
45
|
end
|
|
37
46
|
|
|
@@ -35,7 +35,7 @@ module Ace
|
|
|
35
35
|
|
|
36
36
|
def initialize(root:, binding:, project:, credential:, ssl_context:,
|
|
37
37
|
listen: "127.0.0.1", port:, group: Store::DEFAULT_GROUP, vault: nil, logger: nil,
|
|
38
|
-
idle_seconds: DEFAULT_IDLE_SECONDS, proposal_clock: -> { Time.now.utc })
|
|
38
|
+
idle_seconds: DEFAULT_IDLE_SECONDS, proposal_clock: -> { Time.now.utc }, decision_policy: AccessPolicy.new)
|
|
39
39
|
@root = root
|
|
40
40
|
@binding = binding
|
|
41
41
|
@project = TlsCredentials.project_id(project)
|
|
@@ -48,6 +48,7 @@ module Ace
|
|
|
48
48
|
@logger = logger
|
|
49
49
|
@idle_seconds = idle_seconds
|
|
50
50
|
@proposal_clock = proposal_clock
|
|
51
|
+
@policy = TlsAccessPolicy.new(project: @project, decision_policy: decision_policy)
|
|
51
52
|
@max_connections = MAX_CONNECTIONS
|
|
52
53
|
@connections = 0
|
|
53
54
|
@handler_threads = []
|
|
@@ -223,7 +224,7 @@ module Ace
|
|
|
223
224
|
Store.new(
|
|
224
225
|
root: @root,
|
|
225
226
|
binding: @binding,
|
|
226
|
-
policy:
|
|
227
|
+
policy: @policy,
|
|
227
228
|
identity: peer,
|
|
228
229
|
vault: @vault,
|
|
229
230
|
proposal_clock: @proposal_clock, stopping: -> { @stopping }, group: @group
|
|
@@ -233,7 +234,7 @@ module Ace
|
|
|
233
234
|
end
|
|
234
235
|
|
|
235
236
|
def root_store
|
|
236
|
-
Store.new(root: @root, binding: @binding, policy:
|
|
237
|
+
Store.new(root: @root, binding: @binding, policy: @policy, group: @group)
|
|
237
238
|
end
|
|
238
239
|
end
|
|
239
240
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -108,6 +108,7 @@ module Ace
|
|
|
108
108
|
|
|
109
109
|
def resolve(record, checkpoint:, now:)
|
|
110
110
|
return record unless record["state"] == "awaiting-decision"
|
|
111
|
+
return record unless record["silence_delegated"] == true
|
|
111
112
|
return record if timestamp(now) < timestamp(record.fetch("deadline"))
|
|
112
113
|
valid = checkpoint["schema"] == "ace.hitl.hermes.ingress-checkpoint/v1" &&
|
|
113
114
|
checkpoint["request"] == record["request_id"] && checkpoint["revision"] == record["revision_id"] &&
|
|
@@ -16,9 +16,14 @@ module Ace
|
|
|
16
16
|
# authority. Every doubt maps to the classified BindingError —
|
|
17
17
|
# unknown identity/authority is an error, never permission.
|
|
18
18
|
class AssignmentBinding < Lifecycle::Binding
|
|
19
|
-
def initialize(coordinator: nil, repo_root: nil)
|
|
19
|
+
def initialize(coordinator: nil, repo_root: nil, project: nil, owner_guard: nil)
|
|
20
|
+
unless (project.nil? && owner_guard.nil?) ||
|
|
21
|
+
(project.is_a?(String) && !project.empty? && owner_guard.respond_to?(:call))
|
|
22
|
+
raise ArgumentError, "installed binding needs both a fixed project and its owner guard"
|
|
23
|
+
end
|
|
20
24
|
@coordinator = coordinator
|
|
21
25
|
@repo_root = repo_root
|
|
26
|
+
@project, @owner_guard = project, owner_guard
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def validate_request(assignment:, attempt:, project:, requester:, caller_pid: nil)
|
|
@@ -30,6 +35,7 @@ module Ace
|
|
|
30
35
|
# Read-only owner verification can run inside an already-held
|
|
31
36
|
# assignment exclusion without reacquiring that exclusion.
|
|
32
37
|
def reverse_address(assignment:, attempt:, project:, caller_pid:)
|
|
38
|
+
verify_selection!(project)
|
|
33
39
|
unless caller_pid.is_a?(Integer) && caller_pid.positive?
|
|
34
40
|
raise Lifecycle::BindingError, "kernel requester process identity is unavailable"
|
|
35
41
|
end
|
|
@@ -49,15 +55,26 @@ module Ace
|
|
|
49
55
|
unless project.is_a?(String) && !project.empty?
|
|
50
56
|
raise Lifecycle::BindingError, "proposal requires an explicit project"
|
|
51
57
|
end
|
|
58
|
+
verify_selection!(project)
|
|
52
59
|
coordinator.send(:journal_for)
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
def proposal_projects
|
|
63
|
+
if @project
|
|
64
|
+
verify_selection!(@project)
|
|
65
|
+
return [@project]
|
|
66
|
+
end
|
|
56
67
|
coordinator.send(:journal_for).proposals.map { |record| record.fetch("project_id") }.uniq.sort
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
private
|
|
60
71
|
|
|
72
|
+
def verify_selection!(project)
|
|
73
|
+
return unless @project
|
|
74
|
+
raise Lifecycle::BindingError, "project differs from installed HITL owner" unless project == @project
|
|
75
|
+
@owner_guard.call
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
def with_verified(assignment_id, attempt_id, project_id, requester)
|
|
62
79
|
# ONLY the authority verification is error-mapped: the
|
|
63
80
|
# caller's transition (answer hand-off, commits, effects)
|
|
@@ -66,12 +83,14 @@ module Ace
|
|
|
66
83
|
# failure (review 8x327bud). The exclusion still spans the
|
|
67
84
|
# whole transition, so the phase flag separates the two.
|
|
68
85
|
verifying = true
|
|
86
|
+
verify_selection!(project_id)
|
|
69
87
|
coordinator.with_verified_attempt(
|
|
70
88
|
assignment_id: assignment_id,
|
|
71
89
|
attempt_id: attempt_id,
|
|
72
90
|
project_id: project_id,
|
|
73
91
|
requester: requester
|
|
74
92
|
) do |attempt|
|
|
93
|
+
verify_selection!(project_id)
|
|
75
94
|
verifying = false
|
|
76
95
|
yield attempt
|
|
77
96
|
end
|
data/lib/ace/hitl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-hitl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-hitl-contract
|
|
@@ -113,28 +113,42 @@ dependencies:
|
|
|
113
113
|
requirements:
|
|
114
114
|
- - "~>"
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: '0.
|
|
116
|
+
version: '0.70'
|
|
117
117
|
type: :runtime
|
|
118
118
|
prerelease: false
|
|
119
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
120
|
requirements:
|
|
121
121
|
- - "~>"
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0.
|
|
123
|
+
version: '0.70'
|
|
124
124
|
- !ruby/object:Gem::Dependency
|
|
125
125
|
name: ace-herdr
|
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
|
127
127
|
requirements:
|
|
128
128
|
- - "~>"
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: 0.
|
|
130
|
+
version: '0.6'
|
|
131
131
|
type: :runtime
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
135
|
- - "~>"
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: 0.
|
|
137
|
+
version: '0.6'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: ace-runtime
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0.3'
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0.3'
|
|
138
152
|
description: Provides the dedicated ace-hitl package surface for human in the loop
|
|
139
153
|
(HITL) semantics and CLI workflows.
|
|
140
154
|
email:
|