ace-assign 0.71.0 → 0.73.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 +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +3 -2119
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -4
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +4 -176
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
- data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/command_execution.rb +0 -180
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -457
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -41
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
- data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
- data/lib/ace/assign/authority/ordinary_results.rb +0 -206
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
- data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -309
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
- data/lib/ace/assign/authority/retained_input.rb +0 -53
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -189
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "etc"
|
|
4
|
-
require "json"
|
|
5
|
-
require "socket"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Molecules
|
|
10
|
-
# Resolves the trusted execution-boundary identity for attempt
|
|
11
|
-
# transitions.
|
|
12
|
-
#
|
|
13
|
-
# Actor identity is derived from the execution boundary, never granted
|
|
14
|
-
# by command flags or receipt claims. Two adapters ship:
|
|
15
|
-
# - `local` — the interactive operator is the coordinator; identity
|
|
16
|
-
# comes from the kernel process account (equal real/effective UID).
|
|
17
|
-
# - `service` — a trusted service executor supplies a structured
|
|
18
|
-
# identity through a configured environment variable; the executor
|
|
19
|
-
# owns OS/process enforcement.
|
|
20
|
-
#
|
|
21
|
-
# Unknown adapter, missing actor, or malformed identity fails closed:
|
|
22
|
-
# unknown identity is an error, never permission.
|
|
23
|
-
class ExecutionIdentityResolver
|
|
24
|
-
ROLES = %w[coordinator service worker].freeze
|
|
25
|
-
TRUSTED_ROLES = %w[coordinator service].freeze
|
|
26
|
-
|
|
27
|
-
# Resolved boundary identity.
|
|
28
|
-
Identity = Struct.new(:actor, :role, :runtime, :adapter, :process_pid, :runtime_binding, keyword_init: true)
|
|
29
|
-
|
|
30
|
-
# @param adapter [String, nil] "local" or "service" (default from config)
|
|
31
|
-
# @param service_env [String, nil] Env var carrying the service identity (default from config)
|
|
32
|
-
def initialize(adapter: nil, service_env: nil, runtime_resolver: Ace::Runtime, env: ENV, caller_pid: Process.ppid)
|
|
33
|
-
section = Ace::Assign.config["attempt"] || {}
|
|
34
|
-
@adapter = adapter || section["identity_adapter"] || "local"
|
|
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
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @return [Identity] Trusted boundary identity
|
|
42
|
-
# @raise [AttemptErrors::UnauthorizedIdentity] when identity cannot be established
|
|
43
|
-
def resolve
|
|
44
|
-
case @adapter
|
|
45
|
-
when "local" then from_local_boundary
|
|
46
|
-
when "service" then from_service_env
|
|
47
|
-
else
|
|
48
|
-
raise AttemptErrors::UnauthorizedIdentity, "Unknown identity adapter '#{@adapter}'"
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# @param identity [Identity] Identity to inspect
|
|
53
|
-
# @return [Boolean] True when the role may accept privileged transitions
|
|
54
|
-
def trusted?(identity)
|
|
55
|
-
TRUSTED_ROLES.include?(identity.role)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def from_local_boundary
|
|
61
|
-
uid, euid = Process.uid, Process.euid
|
|
62
|
-
unless uid == euid
|
|
63
|
-
raise AttemptErrors::UnauthorizedIdentity, "Local execution requires equal real and effective UIDs"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
account = local_account(euid)
|
|
67
|
-
verify_local_credentials!(uid, euid)
|
|
68
|
-
binding = native_process_binding
|
|
69
|
-
verify_local_credentials!(uid, euid)
|
|
70
|
-
Identity.new(
|
|
71
|
-
actor: account.name,
|
|
72
|
-
role: "coordinator",
|
|
73
|
-
runtime: "local:#{Socket.gethostname}",
|
|
74
|
-
adapter: "local",
|
|
75
|
-
process_pid: binding&.dig("process_identity", "pid"),
|
|
76
|
-
runtime_binding: binding
|
|
77
|
-
)
|
|
78
|
-
end
|
|
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
|
-
|
|
96
|
-
def from_service_env
|
|
97
|
-
raw = ENV[@service_env].to_s.strip
|
|
98
|
-
if raw.empty?
|
|
99
|
-
raise AttemptErrors::UnauthorizedIdentity,
|
|
100
|
-
"Service identity env #{@service_env} is empty; the trusted executor must supply it"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
data = begin
|
|
104
|
-
JSON.parse(raw)
|
|
105
|
-
rescue JSON::ParserError
|
|
106
|
-
raise AttemptErrors::UnauthorizedIdentity, "Service identity env #{@service_env} is not valid JSON"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
actor = data["actor"].to_s.strip
|
|
110
|
-
role = data["role"].to_s.strip
|
|
111
|
-
runtime = data["runtime"].to_s.strip
|
|
112
|
-
if actor.empty? || runtime.empty? || !ROLES.include?(role)
|
|
113
|
-
raise AttemptErrors::UnauthorizedIdentity,
|
|
114
|
-
"Service identity must supply actor, runtime, and a role in #{ROLES.join('/')}"
|
|
115
|
-
end
|
|
116
|
-
|
|
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
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
@@ -1,345 +0,0 @@
|
|
|
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
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Molecules
|
|
9
|
-
# The shared opaque-reference primitive for fixed-effect records
|
|
10
|
-
# (spec 8x3.t.xz9.4). The reference is receiver-issued at admission and
|
|
11
|
-
# names the canonical Assign record; clients may persist and echo it
|
|
12
|
-
# but must never parse it or derive paths from it. Creation is random
|
|
13
|
-
# (not derived from binding values), so a reference cannot be guessed
|
|
14
|
-
# from public request fields, and resolution always goes through the
|
|
15
|
-
# canonical record — there is no receiver-local mapping.
|
|
16
|
-
class FixedEffectReference
|
|
17
|
-
PREFIX = "fmr1"
|
|
18
|
-
PATTERN = /\Afmr1-[A-Za-z0-9_-]{40,86}\z/
|
|
19
|
-
MAX_LENGTH = 256
|
|
20
|
-
|
|
21
|
-
def self.create
|
|
22
|
-
"#{PREFIX}-#{Base64.urlsafe_encode64(SecureRandom.random_bytes(32), padding: false)}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.valid?(value)
|
|
26
|
-
value.is_a?(String) && value.length.between?(1, MAX_LENGTH) && value.match?(PATTERN)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/herdr/molecules/bounded_process"
|
|
4
|
-
require_relative "evidence_journal"
|
|
5
|
-
require_relative "canonical_read_commands"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Molecules
|
|
10
|
-
# Read the installed owner's immutable Git objects without opening its
|
|
11
|
-
# private writer lock/cache, copying the repository, or contacting it.
|
|
12
|
-
# Source selection is supplied only by verified installation policy.
|
|
13
|
-
class InstalledJournalRead
|
|
14
|
-
include CanonicalReadCommands
|
|
15
|
-
ENVIRONMENT = {"PATH" => "/usr/bin:/bin", "LC_ALL" => "C",
|
|
16
|
-
"GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_SYSTEM" => "/dev/null",
|
|
17
|
-
"GIT_CONFIG_GLOBAL" => "/dev/null", "GIT_TERMINAL_PROMPT" => "0",
|
|
18
|
-
"GIT_ALLOW_PROTOCOL" => "", "GIT_NO_REPLACE_OBJECTS" => "1",
|
|
19
|
-
"GIT_NO_LAZY_FETCH" => "1", "GIT_OPTIONAL_LOCKS" => "0"}.freeze
|
|
20
|
-
ENTRY_LIMIT = 100_000
|
|
21
|
-
|
|
22
|
-
def initialize(repo_root:, ref:, owner_uid:)
|
|
23
|
-
unless repo_root.is_a?(String) && repo_root.start_with?("/") &&
|
|
24
|
-
!repo_root.include?("\0") && File.expand_path(repo_root) == repo_root &&
|
|
25
|
-
ref.is_a?(String) && ref.match?(%r{\Arefs/[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_-]+)+\z}) &&
|
|
26
|
-
owner_uid.is_a?(Integer) && owner_uid.positive?
|
|
27
|
-
raise ArgumentError, "installed journal selection is malformed"
|
|
28
|
-
end
|
|
29
|
-
@repo_root, @ref, @owner_uid = repo_root.dup.freeze, ref.dup.freeze, owner_uid
|
|
30
|
-
@mutex = Mutex.new
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def with(deadline:)
|
|
34
|
-
refuse!("read_busy") unless @mutex.try_lock
|
|
35
|
-
begin
|
|
36
|
-
@deadline = deadline
|
|
37
|
-
remaining
|
|
38
|
-
verify_source!
|
|
39
|
-
@view = File.join(@repo_root, ".git")
|
|
40
|
-
@commit = run!(["rev-parse", "--verify", @ref], limit: 128).stdout.strip
|
|
41
|
-
refuse!("ref_unavailable") unless @commit.match?(OID)
|
|
42
|
-
result = yield self
|
|
43
|
-
verify_source!
|
|
44
|
-
result
|
|
45
|
-
ensure
|
|
46
|
-
@view = @commit = @deadline = nil
|
|
47
|
-
@mutex.unlock
|
|
48
|
-
end
|
|
49
|
-
rescue SystemCallError, IOError, Timeout::Error, Herdr::Molecules::BoundedProcess::PostLaunchError
|
|
50
|
-
refuse!("source_unavailable")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def refuse!(reason)
|
|
56
|
-
raise AttemptErrors::EvidenceUnavailable, "installed journal read unavailable: #{reason}"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def remaining
|
|
60
|
-
refuse!("deadline") unless @deadline.is_a?(Numeric) && @deadline.finite?
|
|
61
|
-
value = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
62
|
-
refuse!("deadline") unless value.positive?
|
|
63
|
-
value
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def verify_source!
|
|
67
|
-
# Caller-writable ancestors can replace even a correctly owned leaf.
|
|
68
|
-
current = File.join(@repo_root, ".git")
|
|
69
|
-
loop do
|
|
70
|
-
stat = File.lstat(current)
|
|
71
|
-
unless stat.directory? && !stat.symlink? && [0, @owner_uid].include?(stat.uid) &&
|
|
72
|
-
(stat.mode & 0o022).zero?
|
|
73
|
-
refuse!("storage_owner")
|
|
74
|
-
end
|
|
75
|
-
if [@repo_root, File.join(@repo_root, ".git")].include?(current) && stat.uid != @owner_uid
|
|
76
|
-
refuse!("repository_owner")
|
|
77
|
-
end
|
|
78
|
-
break if current == "/"
|
|
79
|
-
current = File.dirname(current)
|
|
80
|
-
end
|
|
81
|
-
%w[commondir shallow reftable info/grafts objects/info/alternates objects/info/http-alternates].each do |name|
|
|
82
|
-
path = File.join(@repo_root, ".git", name)
|
|
83
|
-
refuse!("redirected_storage") if File.exist?(path) || File.symlink?(path)
|
|
84
|
-
end
|
|
85
|
-
@entries = 0
|
|
86
|
-
%w[objects refs config HEAD packed-refs].each do |name|
|
|
87
|
-
path = File.join(@repo_root, ".git", name)
|
|
88
|
-
next if %w[HEAD packed-refs].include?(name) && !File.exist?(path) && !File.symlink?(path)
|
|
89
|
-
verify_tree!(path)
|
|
90
|
-
end
|
|
91
|
-
verify_config!
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def verify_tree!(path)
|
|
95
|
-
remaining
|
|
96
|
-
@entries += 1
|
|
97
|
-
refuse!("storage_inventory_limit") if @entries > ENTRY_LIMIT
|
|
98
|
-
stat = File.lstat(path)
|
|
99
|
-
unless !stat.symlink? && (stat.file? || stat.directory?) &&
|
|
100
|
-
[0, @owner_uid].include?(stat.uid) && (stat.mode & 0o022).zero?
|
|
101
|
-
refuse!("storage_owner")
|
|
102
|
-
end
|
|
103
|
-
Dir.each_child(path) { |name| verify_tree!(File.join(path, name)) } if stat.directory?
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def verify_config!
|
|
107
|
-
bytes = File.open(File.join(@repo_root, ".git/config"), File::RDONLY | File::NOFOLLOW) { |io| io.read(65_537) }
|
|
108
|
-
refuse!("config_limit") if bytes.bytesize > 65_536
|
|
109
|
-
result = Herdr::Molecules::BoundedProcess.call(
|
|
110
|
-
["/usr/bin/git", "config", "--no-includes", "--null", "--list", "--file", "/dev/stdin"],
|
|
111
|
-
environment: ENVIRONMENT, chdir: "/", stdin_data: bytes,
|
|
112
|
-
timeout_s: remaining, output_limit: 65_536)
|
|
113
|
-
refuse!("config_unavailable") unless result.status.success? && !result.oversized
|
|
114
|
-
result.stdout.split("\0").each do |entry|
|
|
115
|
-
key, value = entry.split("\n", 2)
|
|
116
|
-
if key.start_with?("include.", "includeif.", "extensions.") || key == "core.worktree" ||
|
|
117
|
-
key == "core.repositoryformatversion" && value != "0"
|
|
118
|
-
refuse!("redirected_configuration")
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def read_result(argv, **options)
|
|
124
|
-
run!(argv, **options, allow_failure: true)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def run!(argv, stdin_data: "", limit:, allow_failure: false)
|
|
128
|
-
# All ref-based reads in one observation use the same immutable tip.
|
|
129
|
-
selected = argv.map { |arg| arg == @ref && @commit ? @commit : arg }
|
|
130
|
-
result = Herdr::Molecules::BoundedProcess.call([
|
|
131
|
-
"/usr/bin/git", "--git-dir=#{@view}", "--no-pager",
|
|
132
|
-
"-c", "safe.directory=#{@repo_root}", "-c", "core.hooksPath=/dev/null",
|
|
133
|
-
"-c", "core.fsmonitor=false", "-c", "protocol.allow=never", *selected],
|
|
134
|
-
environment: ENVIRONMENT, chdir: @repo_root, stdin_data: stdin_data,
|
|
135
|
-
timeout_s: remaining, output_limit: [limit, BYTE_LIMIT].min, stderr_limit: 65_536)
|
|
136
|
-
refuse!("resource_limit") if result.oversized
|
|
137
|
-
refuse!("git_unavailable") unless allow_failure || result.status.success?
|
|
138
|
-
result
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|