ace-assign 0.71.0 → 0.73.1

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