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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +3 -2119
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  35. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  36. data/lib/ace/assign/version.rb +1 -1
  37. data/lib/ace/assign.rb +0 -6
  38. metadata +4 -176
  39. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  40. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  41. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  42. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  43. data/docs/protected-authority.md +0 -282
  44. data/exe/ace-scope-ready +0 -55
  45. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  46. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  47. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  52. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  53. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  54. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  55. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  56. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  57. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  58. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  59. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  60. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  61. data/lib/ace/assign/authority/client.rb +0 -257
  62. data/lib/ace/assign/authority/command_execution.rb +0 -180
  63. data/lib/ace/assign/authority/deployment.rb +0 -627
  64. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  65. data/lib/ace/assign/authority/endcap.rb +0 -457
  66. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  67. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  68. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  69. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  70. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  71. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  72. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  73. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  74. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  75. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  76. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  77. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  78. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  79. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  80. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  81. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  82. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  83. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  84. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  85. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  86. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  87. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  88. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  89. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  90. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  91. data/lib/ace/assign/authority/launch_review.rb +0 -91
  92. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  93. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  94. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  95. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  96. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  97. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  98. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  99. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  100. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  101. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  102. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  103. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  104. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  105. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  106. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  107. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  108. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  109. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  110. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  111. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  112. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  113. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  114. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  115. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  116. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  117. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  118. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  119. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  120. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  121. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  122. data/lib/ace/assign/authority/private_directory.rb +0 -33
  123. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  124. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  125. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  126. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  127. data/lib/ace/assign/authority/retained_input.rb +0 -53
  128. data/lib/ace/assign/authority/router.rb +0 -81
  129. data/lib/ace/assign/authority/server.rb +0 -131
  130. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  131. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  132. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  133. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  134. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  135. data/lib/ace/assign/authority/socket_server.rb +0 -255
  136. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  137. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  138. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  139. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  140. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  141. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  142. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  143. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  144. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  145. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  146. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  147. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  148. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  149. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  151. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  152. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  153. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  154. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  155. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  156. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  157. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  158. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  159. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  160. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  161. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  162. data/lib/ace/assign/models/attempt.rb +0 -178
  163. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  164. data/lib/ace/assign/models/evidence_event.rb +0 -86
  165. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  166. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  167. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  168. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  169. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  170. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  171. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  172. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  173. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  174. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  175. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  176. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  177. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  178. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  179. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  180. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  181. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  182. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  183. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  184. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  185. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  186. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  187. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  188. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  189. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  190. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  191. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  192. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  193. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  194. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  195. 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