ace-assign 0.63.1 → 0.66.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/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +398 -0
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +59 -0
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +286 -0
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +187 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +38 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +56 -4
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -13,7 +13,7 @@ module Ace
|
|
|
13
13
|
# Actor identity is derived from the execution boundary, never granted
|
|
14
14
|
# by command flags or receipt claims. Two adapters ship:
|
|
15
15
|
# - `local` — the interactive operator is the coordinator; identity
|
|
16
|
-
# comes from the
|
|
16
|
+
# comes from the kernel process account (equal real/effective UID).
|
|
17
17
|
# - `service` — a trusted service executor supplies a structured
|
|
18
18
|
# identity through a configured environment variable; the executor
|
|
19
19
|
# owns OS/process enforcement.
|
|
@@ -25,14 +25,17 @@ module Ace
|
|
|
25
25
|
TRUSTED_ROLES = %w[coordinator service].freeze
|
|
26
26
|
|
|
27
27
|
# Resolved boundary identity.
|
|
28
|
-
Identity = Struct.new(:actor, :role, :runtime, :adapter, keyword_init: true)
|
|
28
|
+
Identity = Struct.new(:actor, :role, :runtime, :adapter, :process_pid, :runtime_binding, keyword_init: true)
|
|
29
29
|
|
|
30
30
|
# @param adapter [String, nil] "local" or "service" (default from config)
|
|
31
31
|
# @param service_env [String, nil] Env var carrying the service identity (default from config)
|
|
32
|
-
def initialize(adapter: nil, service_env: nil)
|
|
32
|
+
def initialize(adapter: nil, service_env: nil, runtime_resolver: Ace::Runtime, env: ENV, caller_pid: Process.ppid)
|
|
33
33
|
section = Ace::Assign.config["attempt"] || {}
|
|
34
34
|
@adapter = adapter || section["identity_adapter"] || "local"
|
|
35
35
|
@service_env = service_env || section["service_identity_env"] || "ACE_ASSIGN_SERVICE_IDENTITY"
|
|
36
|
+
@runtime_resolver = runtime_resolver
|
|
37
|
+
@env = env
|
|
38
|
+
@caller_pid = caller_pid
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
# @return [Identity] Trusted boundary identity
|
|
@@ -55,19 +58,41 @@ module Ace
|
|
|
55
58
|
private
|
|
56
59
|
|
|
57
60
|
def from_local_boundary
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
raise AttemptErrors::UnauthorizedIdentity, "
|
|
61
|
+
uid, euid = Process.uid, Process.euid
|
|
62
|
+
unless uid == euid
|
|
63
|
+
raise AttemptErrors::UnauthorizedIdentity, "Local execution requires equal real and effective UIDs"
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
account = local_account(euid)
|
|
67
|
+
verify_local_credentials!(uid, euid)
|
|
68
|
+
binding = native_process_binding
|
|
69
|
+
verify_local_credentials!(uid, euid)
|
|
63
70
|
Identity.new(
|
|
64
|
-
actor:
|
|
71
|
+
actor: account.name,
|
|
65
72
|
role: "coordinator",
|
|
66
73
|
runtime: "local:#{Socket.gethostname}",
|
|
67
|
-
adapter: "local"
|
|
74
|
+
adapter: "local",
|
|
75
|
+
process_pid: binding&.dig("process_identity", "pid"),
|
|
76
|
+
runtime_binding: binding
|
|
68
77
|
)
|
|
69
78
|
end
|
|
70
79
|
|
|
80
|
+
def local_account(euid)
|
|
81
|
+
account = Etc.getpwuid(euid)
|
|
82
|
+
unless account && account.uid == euid && account.name.is_a?(String) && !account.name.strip.empty?
|
|
83
|
+
raise AttemptErrors::UnauthorizedIdentity, "No valid kernel process account for effective UID #{euid}"
|
|
84
|
+
end
|
|
85
|
+
account
|
|
86
|
+
rescue ArgumentError, SystemCallError
|
|
87
|
+
raise AttemptErrors::UnauthorizedIdentity, "Cannot resolve kernel process account for effective UID #{euid}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def verify_local_credentials!(uid, euid)
|
|
91
|
+
unless Process.uid == uid && Process.euid == euid
|
|
92
|
+
raise AttemptErrors::UnauthorizedIdentity, "Local execution credentials changed during identity resolution"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
71
96
|
def from_service_env
|
|
72
97
|
raw = ENV[@service_env].to_s.strip
|
|
73
98
|
if raw.empty?
|
|
@@ -89,7 +114,22 @@ module Ace
|
|
|
89
114
|
"Service identity must supply actor, runtime, and a role in #{ROLES.join('/')}"
|
|
90
115
|
end
|
|
91
116
|
|
|
92
|
-
|
|
117
|
+
pid = data["process_pid"]
|
|
118
|
+
if pid && (!pid.is_a?(Integer) || !pid.positive?)
|
|
119
|
+
raise AttemptErrors::UnauthorizedIdentity, "Service process_pid must be a positive integer"
|
|
120
|
+
end
|
|
121
|
+
Identity.new(actor: actor, role: role, runtime: runtime, adapter: "service", process_pid: pid)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def native_process_binding
|
|
125
|
+
adapter = Ace::Runtime::Molecules::RuntimeSelector.new(
|
|
126
|
+
config: Ace::Runtime.config, registry: @runtime_resolver, env: @env).resolve
|
|
127
|
+
context = adapter.context
|
|
128
|
+
return nil unless context[:in_runtime] && context[:pane]
|
|
129
|
+
|
|
130
|
+
adapter.process_binding(pane: context[:pane], caller_pid: @caller_pid)
|
|
131
|
+
rescue Ace::Runtime::Error
|
|
132
|
+
nil
|
|
93
133
|
end
|
|
94
134
|
end
|
|
95
135
|
end
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/evidence_event"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Assign
|
|
8
|
+
module Molecules
|
|
9
|
+
# Canonical lineage only. Live owner/boundary observation and lifecycle
|
|
10
|
+
# exclusion are mandatory before recording or consuming a fresh proof.
|
|
11
|
+
# This reader never promotes cached population into a proof event.
|
|
12
|
+
class ExecutionScopeLineage
|
|
13
|
+
BINDING_FIELDS = %w[project_id assignment_id attempt_id mapping_id slot_id reservation_generation
|
|
14
|
+
scope_generation deployment_digest boot_id slice_invocation_id cgroup_identity resource_mount_namespace_identity resource_identities network_installation_selection network_namespace_identity boot_baseline_selection].freeze
|
|
15
|
+
PROOF_FIELDS = %w[scope_generation scope_binding_event_id seal_event_id boot_id slice_invocation_id
|
|
16
|
+
cgroup_identity populated].freeze
|
|
17
|
+
PROCESS_FIELDS = %w[pid uid gid groups started_at host parent_pid].freeze
|
|
18
|
+
RESOURCE_FIELDS = %w[host_path view_path mount_id filesystem_type device inode uid gid].freeze
|
|
19
|
+
CGROUP_FIELDS = %w[path mount_id filesystem_type device inode].freeze
|
|
20
|
+
ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
|
|
21
|
+
DIGEST = /\A[0-9a-f]{64}\z/
|
|
22
|
+
INVOCATION = /\A[0-9a-f]{32}\z/
|
|
23
|
+
BOOT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
|
|
24
|
+
|
|
25
|
+
def self.validate_process_identity!(value, boot_id:)
|
|
26
|
+
unless boot_id.is_a?(String) && BOOT.match?(boot_id) && value.is_a?(Hash) && value.keys.sort == PROCESS_FIELDS.sort
|
|
27
|
+
raise AttemptErrors::EvidenceUnavailable, "scope process identity fields differ"
|
|
28
|
+
end
|
|
29
|
+
groups = value["groups"]
|
|
30
|
+
unless %w[pid uid gid parent_pid].all? { |key| value[key].is_a?(Integer) && value[key].positive? } &&
|
|
31
|
+
groups.is_a?(Array) && groups.all? { |id| id.is_a?(Integer) && id.positive? } && groups == groups.sort.uniq &&
|
|
32
|
+
value["started_at"].is_a?(String) && value["started_at"].match?(/\Alinux:#{Regexp.escape(boot_id)}:[0-9]+\z/) &&
|
|
33
|
+
value["host"].is_a?(String) && !value["host"].empty?
|
|
34
|
+
raise AttemptErrors::EvidenceUnavailable, "scope process birth/credentials are malformed"
|
|
35
|
+
end
|
|
36
|
+
value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.validate_network_selection!(selection)
|
|
40
|
+
unless selection.is_a?(Hash) && selection.keys.sort == %w[installer_artifact policy_export profile report]
|
|
41
|
+
raise AttemptErrors::EvidenceUnavailable, "network installation selection is not closed"
|
|
42
|
+
end
|
|
43
|
+
selection.each_value do |ref|
|
|
44
|
+
unless ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] && ref["path"].is_a?(String) &&
|
|
45
|
+
ref["path"].bytesize.between?(1, 4096) && ref["path"].start_with?("/") && !ref["path"].include?("\0") &&
|
|
46
|
+
File.expand_path(ref["path"]) == ref["path"] && ref["sha256"].is_a?(String) && DIGEST.match?(ref["sha256"]) &&
|
|
47
|
+
ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 1_048_576)
|
|
48
|
+
raise AttemptErrors::EvidenceUnavailable, "network installation artifact reference differs"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
selection
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.validate_boot_baseline_selection!(ref)
|
|
55
|
+
unless ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] && ref["path"].is_a?(String) &&
|
|
56
|
+
ref["path"].bytesize.between?(1, 4096) && ref["path"].start_with?("/") && !ref["path"].include?("\0") &&
|
|
57
|
+
File.expand_path(ref["path"]) == ref["path"] && ref["sha256"].is_a?(String) && DIGEST.match?(ref["sha256"]) &&
|
|
58
|
+
ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 16_384)
|
|
59
|
+
raise AttemptErrors::EvidenceUnavailable, "boot baseline artifact reference differs"
|
|
60
|
+
end
|
|
61
|
+
ref
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_reader :binding_event, :native_event, :child_event, :seal_event, :proof_event, :admission_event
|
|
65
|
+
|
|
66
|
+
def initialize(events:, project_id:, assignment_id:, attempt_id:, mapping_id:)
|
|
67
|
+
events = freeze_tree(JSON.parse(JSON.generate(events)))
|
|
68
|
+
unless events.is_a?(Array) && Models::EvidenceEvent.chain_valid?(events) &&
|
|
69
|
+
events.all? { |event| event["attempt_id"] == attempt_id }
|
|
70
|
+
unavailable!("scope requires its intact exact attempt chain")
|
|
71
|
+
end
|
|
72
|
+
@expected = {"project_id" => project_id, "assignment_id" => assignment_id,
|
|
73
|
+
"attempt_id" => attempt_id, "mapping_id" => mapping_id}
|
|
74
|
+
generation = 0
|
|
75
|
+
events.each do |event|
|
|
76
|
+
generation += 1 if event["type"] == "authority_mutation"
|
|
77
|
+
accept_reservation!(event, generation) if event["type"] == "authority_mutation" &&
|
|
78
|
+
event.dig("payload", "operation") == "reserve_attempt"
|
|
79
|
+
accept_admission!(event, generation) if event["type"] == "authority_mutation" &&
|
|
80
|
+
event.dig("payload", "operation") == "scope_service_admission"
|
|
81
|
+
case event["type"]
|
|
82
|
+
when "scope_bound" then accept_binding!(event, generation + 1)
|
|
83
|
+
when "scope_native_bound" then accept_native!(event)
|
|
84
|
+
when "scope_child_bound" then accept_child!(event)
|
|
85
|
+
when "scope_sealed" then accept_seal!(event)
|
|
86
|
+
when "scope_closed_no_writers" then accept_proof!(event)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
rescue KeyError, NoMethodError, TypeError
|
|
90
|
+
unavailable!("canonical scope lineage is incomplete")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def binding
|
|
94
|
+
binding_event&.fetch("payload")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def sealed?
|
|
98
|
+
!seal_event.nil?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def proof_id
|
|
102
|
+
proof_event&.fetch("digest")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def require_open!
|
|
106
|
+
unavailable!("canonical scope binding is missing") unless binding
|
|
107
|
+
raise AttemptErrors::Conflict, "execution generation is sealed" if sealed?
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def require_launch_bound!
|
|
112
|
+
require_open!
|
|
113
|
+
unavailable!("original native and child stages are missing") unless native_event && child_event
|
|
114
|
+
child_event.fetch("payload").fetch("original_process_binding")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def require_positive!(scope_generation:, scope_binding_event_id:, seal_event_id:, proof_id:)
|
|
118
|
+
unless proof_event && binding.fetch("scope_generation") == scope_generation &&
|
|
119
|
+
binding_event.fetch("digest") == scope_binding_event_id && seal_event.fetch("digest") == seal_event_id &&
|
|
120
|
+
proof_event.fetch("digest") == proof_id
|
|
121
|
+
unavailable!("exact canonical scope proof is unavailable")
|
|
122
|
+
end
|
|
123
|
+
proof_event.fetch("payload")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def accept_reservation!(event, generation)
|
|
129
|
+
unavailable!("canonical reservation is repeated") if @reservation
|
|
130
|
+
value = event.dig("payload", "data")
|
|
131
|
+
unless value.is_a?(Hash) && value.slice(*@expected.keys) == @expected &&
|
|
132
|
+
value["reservation_generation"] == generation && value["generation"] == generation &&
|
|
133
|
+
value["launch_ticket"].is_a?(String) && ID.match?(value["launch_ticket"])
|
|
134
|
+
unavailable!("canonical reservation binding differs")
|
|
135
|
+
end
|
|
136
|
+
@reservation = value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def freeze_tree(value)
|
|
140
|
+
case value
|
|
141
|
+
when Hash then value.each { |key, item| key.freeze; freeze_tree(item) }
|
|
142
|
+
when Array then value.each { |item| freeze_tree(item) }
|
|
143
|
+
end
|
|
144
|
+
value.freeze
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def accept_binding!(event, generation)
|
|
148
|
+
unavailable!("scope binding cannot be replaced") if binding_event
|
|
149
|
+
value = event.fetch("payload")
|
|
150
|
+
exact_fields!(value, BINDING_FIELDS)
|
|
151
|
+
unless value.slice(*@expected.keys) == @expected &&
|
|
152
|
+
value["slot_id"].is_a?(String) && ID.match?(value["slot_id"]) &&
|
|
153
|
+
value["scope_generation"] == generation && @reservation &&
|
|
154
|
+
value["reservation_generation"] == @reservation["reservation_generation"] &&
|
|
155
|
+
digest?(value["deployment_digest"]) && boot?(value["boot_id"]) &&
|
|
156
|
+
invocation?(value["slice_invocation_id"])
|
|
157
|
+
unavailable!("scope generation binding differs")
|
|
158
|
+
end
|
|
159
|
+
cgroup!(value.fetch("cgroup_identity"))
|
|
160
|
+
namespace!(value.fetch("resource_mount_namespace_identity"))
|
|
161
|
+
namespace!(value.fetch("network_namespace_identity"))
|
|
162
|
+
self.class.validate_network_selection!(value.fetch("network_installation_selection"))
|
|
163
|
+
self.class.validate_boot_baseline_selection!(value.fetch("boot_baseline_selection"))
|
|
164
|
+
resources = value.fetch("resource_identities")
|
|
165
|
+
resources!(resources)
|
|
166
|
+
@binding_event = event
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def stage_reference!(value)
|
|
170
|
+
unavailable!("scope stage requires its original open parent") unless binding && !sealed?
|
|
171
|
+
unless value["scope_generation"] == binding.fetch("scope_generation") &&
|
|
172
|
+
value["scope_binding_event_id"] == binding_event.fetch("digest")
|
|
173
|
+
unavailable!("scope stage references another parent")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def accept_admission!(event, generation)
|
|
178
|
+
unavailable!("native admission cannot replace an existing stage") if admission_event || native_event
|
|
179
|
+
value = event.fetch("payload").fetch("data")
|
|
180
|
+
stage_reference!(value)
|
|
181
|
+
unless value.slice(*@expected.keys) == @expected && value["generation"] == generation &&
|
|
182
|
+
value["native_admission"] == "issued_uncertain"
|
|
183
|
+
unavailable!("native admission does not name the exact current reserved owner")
|
|
184
|
+
end
|
|
185
|
+
installation = value.fetch("network_installation")
|
|
186
|
+
exact_fields!(installation, %w[report_id boot_id slot_id namespace_path namespace_identity profile_sha256 policy_export_sha256 report_sha256 installer_artifact_sha256])
|
|
187
|
+
namespace!(installation.fetch("namespace_identity"))
|
|
188
|
+
selection = binding.fetch("network_installation_selection")
|
|
189
|
+
unless boot?(installation["report_id"]) && installation["boot_id"] == binding["boot_id"] &&
|
|
190
|
+
installation["slot_id"] == binding["slot_id"] && installation["namespace_identity"] == binding["network_namespace_identity"] &&
|
|
191
|
+
installation["namespace_path"].is_a?(String) && installation["namespace_path"].start_with?("/") &&
|
|
192
|
+
!installation["namespace_path"].include?("\0") && File.expand_path(installation["namespace_path"]) == installation["namespace_path"] &&
|
|
193
|
+
%w[profile policy_export report installer_artifact].all? { |key| installation["#{key}_sha256"] == selection.fetch(key).fetch("sha256") }
|
|
194
|
+
unavailable!("native admission installation differs from original parent selection")
|
|
195
|
+
end
|
|
196
|
+
@admission_event = event
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def accept_native!(event)
|
|
200
|
+
unavailable!("native binding cannot be replaced") if native_event
|
|
201
|
+
value = event.fetch("payload")
|
|
202
|
+
exact_fields!(value, %w[scope_generation scope_binding_event_id service_invocation_id server_identity socket_identity workspace_id
|
|
203
|
+
mount_namespace_identity resource_observer_identity resource_identities network_namespace_identity network_admission_event_id])
|
|
204
|
+
stage_reference!(value)
|
|
205
|
+
namespace!(value.fetch("network_namespace_identity"))
|
|
206
|
+
unless admission_event && value["network_admission_event_id"] == admission_event.fetch("digest") &&
|
|
207
|
+
value["network_namespace_identity"] == binding.fetch("network_namespace_identity")
|
|
208
|
+
unavailable!("native server does not join the original admitted network installation")
|
|
209
|
+
end
|
|
210
|
+
process!(value.fetch("server_identity"), binding.fetch("boot_id"))
|
|
211
|
+
socket = value.fetch("socket_identity")
|
|
212
|
+
unless invocation?(value["service_invocation_id"]) && value["workspace_id"].is_a?(String) &&
|
|
213
|
+
value["workspace_id"].match?(/\Aw[1-9][0-9]{0,8}\z/) && socket.is_a?(Array) && socket.size == 3 &&
|
|
214
|
+
socket.all? { |part| part.is_a?(Integer) && part >= 0 } && socket.last == value.dig("server_identity", "uid")
|
|
215
|
+
unavailable!("scope native incarnation differs")
|
|
216
|
+
end
|
|
217
|
+
namespace!(value.fetch("mount_namespace_identity"))
|
|
218
|
+
observer = value.fetch("resource_observer_identity")
|
|
219
|
+
process!(observer, binding.fetch("boot_id"))
|
|
220
|
+
server = value.fetch("server_identity")
|
|
221
|
+
unless observer.values_at("uid", "gid", "groups", "host") == server.values_at("uid", "gid", "groups", "host") &&
|
|
222
|
+
observer["pid"] != server["pid"]
|
|
223
|
+
unavailable!("native resource observer is not the distinct fixed same-user hook")
|
|
224
|
+
end
|
|
225
|
+
resources!(value.fetch("resource_identities"))
|
|
226
|
+
parent_resources = binding.fetch("resource_identities").to_h { |resource| [resource.values_at("host_path", "view_path"), resource] }
|
|
227
|
+
value.fetch("resource_identities").each do |resource|
|
|
228
|
+
original = parent_resources[resource.values_at("host_path", "view_path")]
|
|
229
|
+
if original && original.slice("device", "inode", "filesystem_type", "uid", "gid") != resource.slice("device", "inode", "filesystem_type", "uid", "gid")
|
|
230
|
+
unavailable!("native view substitutes a parent-pinned backing object")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
@native_event = event
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def accept_child!(event)
|
|
237
|
+
unavailable!("child binding requires its single native predecessor") unless native_event && !child_event
|
|
238
|
+
value = event.fetch("payload")
|
|
239
|
+
exact_fields!(value, %w[scope_generation scope_binding_event_id native_binding_event_id original_process_binding])
|
|
240
|
+
stage_reference!(value)
|
|
241
|
+
unless value["native_binding_event_id"] == native_event.fetch("digest")
|
|
242
|
+
unavailable!("child references another native incarnation")
|
|
243
|
+
end
|
|
244
|
+
native = native_event.fetch("payload")
|
|
245
|
+
original = value.fetch("original_process_binding")
|
|
246
|
+
exact_fields!(original, %w[runtime session pane terminal_id process_identity shell_identity native_origin])
|
|
247
|
+
process!(original.fetch("process_identity"), binding.fetch("boot_id"))
|
|
248
|
+
socket = native.fetch("socket_identity")
|
|
249
|
+
server = native.fetch("server_identity")
|
|
250
|
+
child = original.fetch("process_identity")
|
|
251
|
+
origin = original.fetch("native_origin")
|
|
252
|
+
exact_fields!(origin, %w[workspace tab pane server_identity socket_identity command cwd])
|
|
253
|
+
command = origin.fetch("command")
|
|
254
|
+
unless child["parent_pid"] == server["pid"] &&
|
|
255
|
+
child.values_at("uid", "gid", "groups") == server.values_at("uid", "gid", "groups") &&
|
|
256
|
+
original["runtime"] == "herdr" && original["shell_identity"] == child &&
|
|
257
|
+
original["session"] == native["workspace_id"] && origin["workspace"] == native["workspace_id"] &&
|
|
258
|
+
origin["server_identity"] == server && origin["socket_identity"] == socket &&
|
|
259
|
+
origin["pane"] == original["pane"] && %w[pane terminal_id].all? { |key| original[key].is_a?(String) && !original[key].empty? } &&
|
|
260
|
+
origin["tab"].is_a?(String) && !origin["tab"].empty? && path?(origin["cwd"]) &&
|
|
261
|
+
command.is_a?(Array) && command.size == 3 && path?(command.first) && command[1] == binding["mapping_id"] &&
|
|
262
|
+
command.last == @reservation.fetch("launch_ticket")
|
|
263
|
+
unavailable!("scope original-child lineage differs")
|
|
264
|
+
end
|
|
265
|
+
@child_event = event
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def accept_seal!(event)
|
|
269
|
+
unavailable!("scope seal has no immutable binding or is repeated") unless binding && !seal_event
|
|
270
|
+
value = event.fetch("payload")
|
|
271
|
+
exact_fields!(value, %w[scope_generation scope_binding_event_id])
|
|
272
|
+
unless value == {"scope_generation" => binding.fetch("scope_generation"),
|
|
273
|
+
"scope_binding_event_id" => binding_event.fetch("digest")}
|
|
274
|
+
unavailable!("scope seal references another incarnation")
|
|
275
|
+
end
|
|
276
|
+
@seal_event = event
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def accept_proof!(event)
|
|
280
|
+
unavailable!("scope proof requires its original seal and single observation") unless sealed? && !proof_event
|
|
281
|
+
value = event.fetch("payload")
|
|
282
|
+
exact_fields!(value, PROOF_FIELDS)
|
|
283
|
+
expected = binding.slice("scope_generation", "boot_id", "slice_invocation_id", "cgroup_identity")
|
|
284
|
+
expected.merge!("scope_binding_event_id" => binding_event.fetch("digest"),
|
|
285
|
+
"seal_event_id" => seal_event.fetch("digest"), "populated" => 0)
|
|
286
|
+
unavailable!("scope proof is not exact sealed empty population") unless value == expected
|
|
287
|
+
@proof_event = event
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def namespace!(value)
|
|
291
|
+
exact_fields!(value, %w[device inode])
|
|
292
|
+
unless value["device"].is_a?(Integer) && value["device"] >= 0 && positive_integer?(value["inode"])
|
|
293
|
+
unavailable!("scope mount namespace identity is malformed")
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def resources!(resources)
|
|
298
|
+
unless resources.is_a?(Array) && resources.size <= 64 &&
|
|
299
|
+
resources.all? { |resource| resource.is_a?(Hash) } &&
|
|
300
|
+
resources.map { |resource| resource.values_at("host_path", "view_path") }.uniq.size == resources.size &&
|
|
301
|
+
resources.map { |resource| resource["view_path"] }.uniq.size == resources.size
|
|
302
|
+
unavailable!("scope resource observations are duplicate or oversized")
|
|
303
|
+
end
|
|
304
|
+
resources.each do |resource|
|
|
305
|
+
exact_fields!(resource, RESOURCE_FIELDS)
|
|
306
|
+
unless path?(resource["host_path"]) && path?(resource["view_path"]) && positive_integer?(resource["mount_id"]) &&
|
|
307
|
+
%w[device inode uid gid].all? { |key| resource[key].is_a?(Integer) && resource[key] >= 0 } &&
|
|
308
|
+
resource["filesystem_type"].is_a?(String) && resource["filesystem_type"].bytesize.between?(1, 64) &&
|
|
309
|
+
resource["filesystem_type"].ascii_only? && !resource["filesystem_type"].match?(/[\s\0]/)
|
|
310
|
+
unavailable!("scope resource identity is malformed")
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def cgroup!(value)
|
|
316
|
+
exact_fields!(value, CGROUP_FIELDS)
|
|
317
|
+
unless path?(value["path"]) && value["path"].start_with?("/sys/fs/cgroup/") &&
|
|
318
|
+
value["filesystem_type"] == "cgroup2" &&
|
|
319
|
+
%w[mount_id device inode].all? { |key| value[key].is_a?(Integer) && value[key] >= 0 }
|
|
320
|
+
unavailable!("scope parent object identity is malformed")
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def process!(value, boot_id)
|
|
325
|
+
self.class.validate_process_identity!(value, boot_id: boot_id)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def exact_fields!(value, keys)
|
|
329
|
+
unavailable!("scope event schema differs") unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def path?(value)
|
|
333
|
+
value.is_a?(String) && value.bytesize.between?(2, 4096) && !value.include?("\0") &&
|
|
334
|
+
value.start_with?("/") && File.expand_path(value) == value
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def positive_integer?(value) = value.is_a?(Integer) && value.positive?
|
|
338
|
+
def digest?(value) = value.is_a?(String) && DIGEST.match?(value)
|
|
339
|
+
def boot?(value) = value.is_a?(String) && BOOT.match?(value)
|
|
340
|
+
def invocation?(value) = value.is_a?(String) && INVOCATION.match?(value)
|
|
341
|
+
def unavailable!(message) = raise(AttemptErrors::EvidenceUnavailable, message)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
@@ -85,14 +85,22 @@ module Ace
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def launch_provider_session(assignment_id:, fork_root:, provider:, cli_args: nil, timeout: nil, cache_dir: nil,
|
|
88
|
-
last_message_file: nil, session_meta_file: nil)
|
|
88
|
+
last_message_file: nil, session_meta_file: nil, prepared_input: nil)
|
|
89
89
|
ensure_not_same_scoped_refork!(assignment_id: assignment_id, fork_root: fork_root)
|
|
90
90
|
resolved_provider = provider || config.dig("execution", "provider") || DEFAULT_PROVIDER
|
|
91
91
|
resolved_timeout = timeout || config.dig("execution", "timeout") || DEFAULT_TIMEOUT
|
|
92
92
|
scoped_assignment = "#{assignment_id}@#{fork_root}"
|
|
93
|
-
|
|
93
|
+
if prepared_input && prepared_input.descriptor.values_at("assignment_id", "scope") != [assignment_id, fork_root]
|
|
94
|
+
raise Error, "Prepared provider scope differs"
|
|
95
|
+
end
|
|
96
|
+
prompt = prepared_input ? prepared_input.drive_prompt : "/as-assign-drive #{scoped_assignment}"
|
|
94
97
|
last_msg_file = last_message_file || build_last_message_file(cache_dir, fork_root)
|
|
95
98
|
scope_env = self.class.fork_scope_env(assignment_id: assignment_id, fork_root: fork_root)
|
|
99
|
+
if prepared_input
|
|
100
|
+
scope_env = scope_env.merge("ACE_ASSIGN_LAUNCH_MAPPING" => prepared_input.descriptor.fetch("mapping_id"),
|
|
101
|
+
"ACE_ASSIGN_ASSIGNMENT_ID" => assignment_id, "ACE_ASSIGN_ATTEMPT_ID" => prepared_input.descriptor.fetch("attempt_id"),
|
|
102
|
+
"ACE_ASSIGN_TASK_CONTEXT_ENTRY" => JSON.generate(prepared_input.descriptor.fetch("task_context_entry").fetch("manifest")))
|
|
103
|
+
end
|
|
96
104
|
|
|
97
105
|
result = query_interface.query(
|
|
98
106
|
resolved_provider,
|
|
@@ -102,7 +110,8 @@ module Ace
|
|
|
102
110
|
timeout: resolved_timeout,
|
|
103
111
|
fallback: false,
|
|
104
112
|
last_message_file: last_msg_file,
|
|
105
|
-
subprocess_env: scope_env
|
|
113
|
+
subprocess_env: scope_env,
|
|
114
|
+
working_dir: prepared_input&.working_directory
|
|
106
115
|
)
|
|
107
116
|
|
|
108
117
|
if last_msg_file && result[:text] && !result[:text].strip.empty?
|
|
@@ -110,7 +119,8 @@ module Ace
|
|
|
110
119
|
File.write(last_msg_file, result[:text]) if existing.empty?
|
|
111
120
|
end
|
|
112
121
|
|
|
113
|
-
write_session_metadata(last_msg_file, result, prompt: prompt, session_meta_file: session_meta_file
|
|
122
|
+
write_session_metadata(last_msg_file, result, prompt: prompt, session_meta_file: session_meta_file,
|
|
123
|
+
discover_session: prepared_input.nil?)
|
|
114
124
|
|
|
115
125
|
result
|
|
116
126
|
rescue Ace::LLM::Error => e
|
|
@@ -220,12 +230,12 @@ module Ace
|
|
|
220
230
|
File.write(session_meta_file, meta.to_yaml) unless meta.empty?
|
|
221
231
|
end
|
|
222
232
|
|
|
223
|
-
def write_session_metadata(last_msg_file, result, prompt:, session_meta_file: nil)
|
|
233
|
+
def write_session_metadata(last_msg_file, result, prompt:, session_meta_file: nil, discover_session: true)
|
|
224
234
|
return unless last_msg_file
|
|
225
235
|
|
|
226
236
|
session_id = result.dig(:metadata, :session_id)
|
|
227
237
|
|
|
228
|
-
if session_id.nil? || session_id.to_s.strip.empty?
|
|
238
|
+
if discover_session && (session_id.nil? || session_id.to_s.strip.empty?)
|
|
229
239
|
detected = detect_provider_session(result[:provider], prompt)
|
|
230
240
|
session_id = detected&.dig(:session_id)
|
|
231
241
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "execution_scope_lineage"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Assign
|
|
6
|
+
module Molecules
|
|
7
|
+
module JournalInputInhibition
|
|
8
|
+
INPUT_INHIBITION_FIELDS = %w[assignment_id attempt_id mapping_id original_binding_digest project_id seal_event_id].freeze
|
|
9
|
+
|
|
10
|
+
def observe_input_inhibition(selection:, evidence:, &authentication)
|
|
11
|
+
raise ArgumentError, "Original inhibition authentication is required" unless authentication
|
|
12
|
+
input = selection.merge("guarded_evidence" => evidence)
|
|
13
|
+
digest = Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(input)))
|
|
14
|
+
mutate(assignment_id: selection.fetch("assignment_id"), attempt_id: selection.fetch("attempt_id"),
|
|
15
|
+
mutation_id: "input-inhibit.#{selection.fetch('seal_event_id')}", operation: "input_inhibition_observation",
|
|
16
|
+
parameters_digest: digest, expected_generation: nil, generation_mode: :input_inhibition,
|
|
17
|
+
input_inhibition: input, with_replay: true) do |events, commit, generation|
|
|
18
|
+
original = authentication.call(events, commit, generation)
|
|
19
|
+
unless original.is_a?(Hash) && original.keys.sort == %w[binding_digest origin] && original["binding_digest"] == selection.fetch("original_binding_digest")
|
|
20
|
+
raise AttemptErrors::EvidenceUnavailable, "Original input inhibition record differs"
|
|
21
|
+
end
|
|
22
|
+
verify_input_drained_evidence!(evidence, original.fetch("origin"))
|
|
23
|
+
{events: [{type: "input_inhibited", payload: selection.merge("guarded_evidence" => evidence)}], blobs: {},
|
|
24
|
+
data: selection.slice("original_binding_digest", "seal_event_id")}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def verify_input_drained_evidence!(evidence, original)
|
|
29
|
+
unless original.is_a?(Hash) && evidence.is_a?(Hash) && evidence.keys.sort == %w[input_state origin outcome pending_input] &&
|
|
30
|
+
evidence["outcome"] == "inhibited" && evidence["input_state"] == "inhibited" &&
|
|
31
|
+
evidence["pending_input"].is_a?(Integer) && evidence["pending_input"].zero?
|
|
32
|
+
raise AttemptErrors::EvidenceUnavailable, "Native input drainage is unconfirmed"
|
|
33
|
+
end
|
|
34
|
+
received = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(evidence.fetch("origin"),
|
|
35
|
+
terminal_id: original.fetch("terminal_id"), child: original.fetch("child"))
|
|
36
|
+
raise AttemptErrors::EvidenceUnavailable, "Native input drainage origin differs" unless received == original
|
|
37
|
+
true
|
|
38
|
+
rescue Ace::Runtime::RuntimeUnavailableError, KeyError, TypeError
|
|
39
|
+
raise AttemptErrors::EvidenceUnavailable, "Native input drainage is unverifiable"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def verify_input_inhibition!(input, mutation_id:, digest:, assignment_id:, attempt_id:, commit:)
|
|
45
|
+
unless input.is_a?(Hash) && input.keys.sort == (INPUT_INHIBITION_FIELDS + ["guarded_evidence"]).sort &&
|
|
46
|
+
INPUT_INHIBITION_FIELDS.all? { |key| input[key].is_a?(String) } &&
|
|
47
|
+
%w[original_binding_digest seal_event_id].all? { |key| input.fetch(key).match?(/\A[0-9a-f]{64}\z/) } &&
|
|
48
|
+
input["assignment_id"] == assignment_id && input["attempt_id"] == attempt_id &&
|
|
49
|
+
mutation_id == "input-inhibit.#{input.fetch('seal_event_id')}" &&
|
|
50
|
+
digest == Digest::SHA256.hexdigest(JSON.generate(canonical_prompt_value(input)))
|
|
51
|
+
raise ArgumentError, "Invalid fixed input inhibition selector"
|
|
52
|
+
end
|
|
53
|
+
events = read_events(assignment_id, commit: commit).select { |event| event["attempt_id"] == attempt_id }
|
|
54
|
+
authority_generation(events)
|
|
55
|
+
record = events.find { |event| event["digest"] == input.fetch("original_binding_digest") && event["type"] == "authority_mutation" &&
|
|
56
|
+
event.dig("payload", "operation") == "record_launch" }
|
|
57
|
+
data = record&.dig("payload", "data")
|
|
58
|
+
seal = events.find { |event| event["digest"] == input.fetch("seal_event_id") && event["type"] == "scope_sealed" }
|
|
59
|
+
accepted_seal = seal && events.find { |event| event["type"] == "authority_mutation" && event["previous_digest"] == seal.fetch("digest") &&
|
|
60
|
+
%w[close_execution_scope stop_attempt].include?(event.dig("payload", "operation")) }
|
|
61
|
+
lineage = ExecutionScopeLineage.new(events: events, project_id: input.fetch("project_id"), assignment_id: assignment_id,
|
|
62
|
+
attempt_id: attempt_id, mapping_id: input.fetch("mapping_id"))
|
|
63
|
+
unless data.is_a?(Hash) && %w[assignment_id attempt_id mapping_id].all? { |key| data[key] == input.fetch(key) } &&
|
|
64
|
+
seal && accepted_seal && lineage.seal_event == seal && events.index(record) < events.index(seal) &&
|
|
65
|
+
events.drop(events.index(seal) + 1).none? { |event| event["type"] == "prompt_issued" } &&
|
|
66
|
+
events.none? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
|
|
67
|
+
raise AttemptErrors::EvidenceUnavailable, "Input inhibition does not join accepted original seal"
|
|
68
|
+
end
|
|
69
|
+
origin = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(data.fetch("guarded_origin"),
|
|
70
|
+
terminal_id: data.fetch("process_binding").fetch("terminal_id"), child: data.fetch("process_binding").fetch("process_identity"))
|
|
71
|
+
verify_input_drained_evidence!(input.fetch("guarded_evidence"), origin)
|
|
72
|
+
rescue KeyError, TypeError, Ace::Runtime::RuntimeUnavailableError
|
|
73
|
+
raise AttemptErrors::EvidenceUnavailable, "Input inhibition canonical selection is malformed"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|