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,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# Publication-specific facts remain artifacts of the existing service
|
|
7
|
-
# record and canonical import owner, not a second publication journal.
|
|
8
|
-
module ServicePublicationEvidence
|
|
9
|
-
PUBLICATION_NEED_FIELDS = %w[schema classification request_id input_digest claim_binding
|
|
10
|
-
candidate_generation head registry gem_name version artifact_digest dispatch_event_digest previous_challenge_digest].freeze
|
|
11
|
-
PUBLICATION_FIELDS = %w[publication_challenge_digest publication_challenge_ref publication_challenge_event_digest
|
|
12
|
-
publication_challenge_generation publication_issuing_event_digest].freeze
|
|
13
|
-
|
|
14
|
-
def publication_result!(bytes, record, state:)
|
|
15
|
-
value = JSON.parse(bytes, allow_duplicate_key: false, allow_nan: false, max_nesting: 16)
|
|
16
|
-
fields = %w[schema publication target classification code request_id input_digest claim_binding]
|
|
17
|
-
publication = value.fetch("publication")
|
|
18
|
-
allowed = state == "succeeded" ? ["registry_exact_artifact"] :
|
|
19
|
-
%w[provider_rejected_non_otp executor_credentials_unavailable registry_artifact_conflict]
|
|
20
|
-
unless bytes.bytesize.between?(1, 65536) && value.is_a?(Hash) && value.keys.sort == fields.sort &&
|
|
21
|
-
value["schema"] == "ace.publication-result/v1" && value["classification"] == state && allowed.include?(value["code"]) &&
|
|
22
|
-
%w[request_id input_digest claim_binding target].all? { |key| value[key] == record[key] } &&
|
|
23
|
-
publication.is_a?(Hash) && publication.keys.sort == %w[gem_name version head registry artifact_relative_path].sort &&
|
|
24
|
-
publication["head"] == record.fetch("candidate_head") && publication["registry"] == "https://rubygems.org" &&
|
|
25
|
-
publication["gem_name"].is_a?(String) && publication["gem_name"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
|
|
26
|
-
publication["version"].is_a?(String) && publication["version"].bytesize.between?(1, 128) &&
|
|
27
|
-
publication["version"].match?(/\A[0-9]+(?:\.[0-9A-Za-z]+)*(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?\z/) &&
|
|
28
|
-
"rubygems:#{publication['gem_name']}:#{publication['version']}" == record.dig("target", "resource") &&
|
|
29
|
-
publication["artifact_relative_path"].is_a?(String) && publication["artifact_relative_path"].bytesize.between?(1, 4096) &&
|
|
30
|
-
publication["artifact_relative_path"].end_with?(".gem") &&
|
|
31
|
-
publication["artifact_relative_path"].split("/", -1).all? { |part| part.match?(/\A[a-zA-Z0-9_.-]+\z/) && !%w[. ..].include?(part) }
|
|
32
|
-
raise AttemptErrors::ReceiptRejected, "publication result differs from original artifact"
|
|
33
|
-
end
|
|
34
|
-
value
|
|
35
|
-
rescue JSON::ParserError, KeyError, TypeError, NoMethodError
|
|
36
|
-
raise AttemptErrors::ReceiptRejected, "publication result is invalid"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def publication_collection!(references, record, state, pending)
|
|
40
|
-
unless %w[succeeded failed].include?(state) && references.size == 1
|
|
41
|
-
raise AttemptErrors::ReceiptRejected, "publication requires one exact result artifact"
|
|
42
|
-
end
|
|
43
|
-
expected = context(record, pending: pending)
|
|
44
|
-
bytes = if pending && pending[:pending_events]
|
|
45
|
-
@canonical.read_pending(references.first, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
|
|
46
|
-
else
|
|
47
|
-
@canonical.read(references.first, **expected, commit: pending && pending[:commit] || @journal.ref_value)
|
|
48
|
-
end
|
|
49
|
-
publication_result!(bytes, record, state: state)
|
|
50
|
-
[bytes].freeze
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def publication_need!(bytes, record, previous:)
|
|
54
|
-
value = JSON.parse(bytes, allow_duplicate_key: false, allow_comments: false,
|
|
55
|
-
allow_nan: false, create_additions: false, max_nesting: 16)
|
|
56
|
-
unless bytes.bytesize.between?(1, 65536) && value.is_a?(Hash) &&
|
|
57
|
-
value.keys.sort == PUBLICATION_NEED_FIELDS.sort && value["schema"] == "ace.publication-otp-need/v1" &&
|
|
58
|
-
%w[otp-required otp-rejected otp-expired].include?(value["classification"]) &&
|
|
59
|
-
%w[request_id input_digest claim_binding candidate_generation].all? { |key| value[key] == record[key] } &&
|
|
60
|
-
value["head"] == record["candidate_head"] && value["registry"] == "https://rubygems.org" &&
|
|
61
|
-
value["previous_challenge_digest"] == previous && value["artifact_digest"] == record.dig("target", "artifact_digest") &&
|
|
62
|
-
"rubygems:#{value['gem_name']}:#{value['version']}" == record.dig("target", "resource") &&
|
|
63
|
-
value["dispatch_event_digest"].is_a?(String) && value["dispatch_event_digest"].match?(/\A[0-9a-f]{64}\z/)
|
|
64
|
-
raise AttemptErrors::ReceiptRejected, "publication OTP need differs from original dispatch"
|
|
65
|
-
end
|
|
66
|
-
value
|
|
67
|
-
rescue JSON::ParserError, KeyError, TypeError
|
|
68
|
-
raise AttemptErrors::ReceiptRejected, "publication OTP need is invalid"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def publication_challenge!(record, pending: nil)
|
|
72
|
-
context(record)
|
|
73
|
-
commit = pending && pending[:commit] || @journal.ref_value
|
|
74
|
-
events = pending && pending[:pending_events] ? pending.fetch(:current_events) + pending.fetch(:pending_events) :
|
|
75
|
-
@journal.read_events(record.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == record.fetch("attempt_id") }
|
|
76
|
-
unless Models::EvidenceEvent.chain_valid?(events)
|
|
77
|
-
raise AttemptErrors::EvidenceUnavailable, "publication challenge chain is corrupt"
|
|
78
|
-
end
|
|
79
|
-
event = events.find { |row| row["digest"] == record["publication_challenge_event_digest"] }
|
|
80
|
-
unless event && event["type"] == "service_publication_challenge" &&
|
|
81
|
-
event.dig("payload", "challenge_digest") == record["publication_challenge_digest"] &&
|
|
82
|
-
event.dig("payload", "request_id") == record["request_id"] &&
|
|
83
|
-
event.dig("payload", "generation") == record["publication_challenge_generation"]
|
|
84
|
-
raise AttemptErrors::EvidenceUnavailable, "publication challenge introduction is unavailable"
|
|
85
|
-
end
|
|
86
|
-
reference = record.fetch("publication_challenge_ref")
|
|
87
|
-
bytes = if pending && pending[:pending_events]
|
|
88
|
-
@canonical.read_pending(reference, **context(record), current_events: pending.fetch(:current_events),
|
|
89
|
-
pending_events: pending.fetch(:pending_events), blobs: pending.fetch(:blobs), commit: commit)
|
|
90
|
-
else
|
|
91
|
-
@canonical.read(reference, **context(record), commit: commit)
|
|
92
|
-
end
|
|
93
|
-
unless Digest::SHA256.hexdigest(bytes) == record.fetch("publication_challenge_digest")
|
|
94
|
-
raise AttemptErrors::EvidenceUnavailable, "publication challenge digest differs"
|
|
95
|
-
end
|
|
96
|
-
need = publication_need!(bytes, record, previous: event.dig("payload", "previous_challenge_digest"))
|
|
97
|
-
dispatch = events.find { |row| row["digest"] == need.fetch("dispatch_event_digest") }
|
|
98
|
-
unless dispatch && dispatch["type"] == "authority_mutation" && dispatch.dig("payload", "operation") == "begin_dispatch" &&
|
|
99
|
-
dispatch.dig("payload", "data", "request_id") == record.fetch("request_id") &&
|
|
100
|
-
dispatch.dig("payload", "data", "executor_process_binding") == record.fetch("executor_process_binding")
|
|
101
|
-
raise AttemptErrors::EvidenceUnavailable, "publication need has no original executor dispatch"
|
|
102
|
-
end
|
|
103
|
-
need
|
|
104
|
-
rescue KeyError, TypeError
|
|
105
|
-
raise AttemptErrors::EvidenceUnavailable, "publication challenge evidence is incomplete"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require_relative "transfer_codec"
|
|
6
|
-
require_relative "../molecules/evidence_journal"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Assign
|
|
10
|
-
module Authority
|
|
11
|
-
# Shared endpoint lifetime and bounded wire protocol. Concrete source
|
|
12
|
-
# compositions authenticate their own installed principals before dispatch.
|
|
13
|
-
# Protected deployment/kernel requirements remain in Server.
|
|
14
|
-
class SocketServer
|
|
15
|
-
CONNECTIONS = 16
|
|
16
|
-
|
|
17
|
-
def initialize(service:, lifecycle:, services: [])
|
|
18
|
-
unless services.is_a?(Array) && services.uniq.size == services.size &&
|
|
19
|
-
services.all? { |owned| owned.respond_to?(:run) && owned.respond_to?(:stop) }
|
|
20
|
-
raise ArgumentError, "authority services require distinct source-owned run/stop lifetimes"
|
|
21
|
-
end
|
|
22
|
-
@service, @lifecycle, @services = service, lifecycle, services.dup.freeze
|
|
23
|
-
@service_threads = []
|
|
24
|
-
@activation_gates = []
|
|
25
|
-
@mutex = Mutex.new
|
|
26
|
-
@connections = []
|
|
27
|
-
@handlers = []
|
|
28
|
-
@transfers = 0
|
|
29
|
-
@stopped = false
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def serve
|
|
33
|
-
verify_listener!
|
|
34
|
-
directory = File.dirname(@service.fetch("socket_path"))
|
|
35
|
-
wire.root_path!(directory, directory: true, owner: @service.fetch("uid"))
|
|
36
|
-
lock = File.open(File.join(directory, "listener.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
|
|
37
|
-
unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
38
|
-
raise AttemptErrors::Conflict, "assignment authority already owns its listener"
|
|
39
|
-
end
|
|
40
|
-
path = @service.fetch("socket_path")
|
|
41
|
-
if File.exist?(path) || File.symlink?(path)
|
|
42
|
-
raise AttemptErrors::Conflict, "existing authority endpoint requires verified installer recovery"
|
|
43
|
-
end
|
|
44
|
-
@listener = UNIXServer.new(path)
|
|
45
|
-
@endpoint = wire.socket_identity(path)
|
|
46
|
-
File.chmod(0o660, path)
|
|
47
|
-
start_services!
|
|
48
|
-
verify_services!
|
|
49
|
-
@activation_gates.each { |gate| gate << :activate }
|
|
50
|
-
until @stopped
|
|
51
|
-
verify_services!
|
|
52
|
-
ready = IO.select([@listener], nil, nil, 0.25)
|
|
53
|
-
next unless ready
|
|
54
|
-
socket = @listener.accept_nonblock(exception: false)
|
|
55
|
-
next if socket == :wait_readable
|
|
56
|
-
admitted = @mutex.synchronize do
|
|
57
|
-
if @connections.size < CONNECTIONS
|
|
58
|
-
@connections << socket
|
|
59
|
-
true
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
unless admitted
|
|
63
|
-
refusal(socket, "busy")
|
|
64
|
-
socket.close
|
|
65
|
-
next
|
|
66
|
-
end
|
|
67
|
-
@mutex.synchronize do
|
|
68
|
-
@handlers.reject! { |handler| !handler.alive? }
|
|
69
|
-
@handlers << Thread.new(socket) { |connection| receive(connection) }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
rescue IOError, Errno::EBADF
|
|
73
|
-
raise unless @stopped
|
|
74
|
-
ensure
|
|
75
|
-
begin
|
|
76
|
-
stop
|
|
77
|
-
stop_services!
|
|
78
|
-
ensure
|
|
79
|
-
# Closing a stream does not cancel an admitted transaction. Hold
|
|
80
|
-
# the owner lock and endpoint until every handler has ended.
|
|
81
|
-
begin
|
|
82
|
-
@mutex.synchronize { @handlers.dup }.each(&:join)
|
|
83
|
-
@lifecycle.close if @endpoint && @lifecycle.respond_to?(:close)
|
|
84
|
-
ensure
|
|
85
|
-
begin
|
|
86
|
-
File.unlink(path) if @endpoint && same_endpoint?(path)
|
|
87
|
-
ensure
|
|
88
|
-
lock&.close
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def request_stop
|
|
95
|
-
@stopped = true
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def stop
|
|
99
|
-
@stopped = true
|
|
100
|
-
@listener&.close unless @listener&.closed?
|
|
101
|
-
@mutex.synchronize { @connections.each { |socket| socket.close unless socket.closed? } }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
# Installed HITL and merge listeners share the assignment owner lock.
|
|
107
|
-
# Every selected boundary must be ready before assignment ingress opens;
|
|
108
|
-
# a stopped boundary closes admission until the complete owner restarts.
|
|
109
|
-
def start_services!
|
|
110
|
-
@services.each do |owned|
|
|
111
|
-
ready = Queue.new
|
|
112
|
-
activation = Queue.new
|
|
113
|
-
@activation_gates << activation
|
|
114
|
-
thread = Thread.new do
|
|
115
|
-
owned.run(on_ready: lambda do
|
|
116
|
-
ready << :ready
|
|
117
|
-
unless activation.pop == :activate
|
|
118
|
-
raise Ace::Runtime::RuntimeUnavailableError, "authority startup was cancelled"
|
|
119
|
-
end
|
|
120
|
-
end)
|
|
121
|
-
rescue Exception => error
|
|
122
|
-
ready << error
|
|
123
|
-
ensure
|
|
124
|
-
ready << :stopped
|
|
125
|
-
end
|
|
126
|
-
thread.report_on_exception = false
|
|
127
|
-
@service_threads << [owned, thread]
|
|
128
|
-
result = ready.pop(timeout: 5)
|
|
129
|
-
raise Ace::Runtime::RuntimeUnavailableError, "authority service startup did not become ready" unless result == :ready || result.is_a?(Exception)
|
|
130
|
-
raise result if result.is_a?(Exception)
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def verify_services!
|
|
135
|
-
unless @service_threads.all? { |_, thread| thread.alive? }
|
|
136
|
-
raise Ace::Runtime::RuntimeUnavailableError, "an owned service stopped before its authority"
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def stop_services!
|
|
141
|
-
errors = []
|
|
142
|
-
@activation_gates.each { |gate| gate << :cancel }
|
|
143
|
-
@service_threads.reverse_each do |owned, _|
|
|
144
|
-
owned.stop
|
|
145
|
-
rescue Exception => error
|
|
146
|
-
errors << error
|
|
147
|
-
end
|
|
148
|
-
@service_threads.each do |_, thread|
|
|
149
|
-
thread.join
|
|
150
|
-
rescue Exception => error
|
|
151
|
-
errors << error
|
|
152
|
-
end
|
|
153
|
-
raise errors.first unless errors.empty?
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def same_endpoint?(path)
|
|
157
|
-
wire.socket_identity(path) == @endpoint
|
|
158
|
-
rescue StandardError
|
|
159
|
-
false
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def verify_listener!
|
|
163
|
-
raise NotImplementedError, "source composition must verify its installed listener"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def dispatch_request(socket, request, peer, role, deadline, frame_bytes:)
|
|
167
|
-
params = request.fetch("params")
|
|
168
|
-
admitted_transfer = false
|
|
169
|
-
Molecules::EvidenceJournal.with_event_read_operation do
|
|
170
|
-
binding = @lifecycle.transfer_binding(request) if @lifecycle.respond_to?(:transfer_binding)
|
|
171
|
-
if binding
|
|
172
|
-
raise AttemptErrors::UnauthorizedIdentity, "transfer role differs" unless binding.fetch(:roles).include?(role)
|
|
173
|
-
raise ArgumentError, "transfer control header is oversized" if frame_bytes > 16_384
|
|
174
|
-
authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
|
|
175
|
-
admitted_transfer = @mutex.synchronize do
|
|
176
|
-
if @transfers < 2
|
|
177
|
-
@transfers += 1
|
|
178
|
-
true
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
raise AttemptErrors::Conflict, "transfer capacity is busy" unless admitted_transfer
|
|
182
|
-
codec = transfer_codec
|
|
183
|
-
if binding.fetch(:direction) == :upload
|
|
184
|
-
result = codec.receive(socket, descriptor: params.fetch("transfer"), purpose: binding.fetch(:purpose), deadline: deadline) do |input|
|
|
185
|
-
dispatch_lifecycle(request: request, peer: peer, role: role, transfer: input, deadline: deadline)
|
|
186
|
-
end
|
|
187
|
-
else
|
|
188
|
-
result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
|
|
189
|
-
parts = result.fetch(:transfer_parts)
|
|
190
|
-
descriptor = codec.descriptor(parts, purpose: binding.fetch(:purpose))
|
|
191
|
-
result = result.merge(data: result.fetch(:data).merge("transfer" => descriptor))
|
|
192
|
-
end
|
|
193
|
-
else
|
|
194
|
-
raise ArgumentError, "unexpected transfer descriptor" if params.key?("transfer")
|
|
195
|
-
result = dispatch_lifecycle(request: request, peer: peer, role: role, deadline: deadline)
|
|
196
|
-
end
|
|
197
|
-
wire.write(socket, {"status" => "ok", "data" => result.fetch(:data),
|
|
198
|
-
"transport" => {"replayed" => result.fetch(:replayed, false)}}, deadline: deadline, limit: 16_384)
|
|
199
|
-
if binding && binding.fetch(:direction) == :download
|
|
200
|
-
codec.send(socket, parts: parts, descriptor: descriptor, purpose: binding.fetch(:purpose), deadline: deadline)
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
ensure
|
|
204
|
-
@mutex.synchronize { @transfers -= 1 } if admitted_transfer
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def authorize_transfer!(request:, peer:, role:, deadline:)
|
|
208
|
-
@lifecycle.authorize_transfer!(request: request, peer: peer, role: role)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def dispatch_lifecycle(request:, peer:, role:, deadline:, transfer: nil)
|
|
212
|
-
options = {request: request, peer: peer, role: role}
|
|
213
|
-
options[:transfer] = transfer if transfer
|
|
214
|
-
@lifecycle.dispatch(**options)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# These closed read operations have no request body. Half-close proves
|
|
218
|
-
# framing completion before status or artifact data can be exposed.
|
|
219
|
-
def bodyless_read!(socket, deadline)
|
|
220
|
-
loop do
|
|
221
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
222
|
-
raise ArgumentError, "read request framing is incomplete" unless remaining.positive? && IO.select([socket], nil, nil, remaining)
|
|
223
|
-
bytes = socket.read_nonblock(1, exception: false)
|
|
224
|
-
next if bytes == :wait_readable
|
|
225
|
-
raise ArgumentError, "unexpected read request body" unless bytes.nil?
|
|
226
|
-
return
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def transfer_codec
|
|
231
|
-
@mutex.synchronize do
|
|
232
|
-
@transfer_codec ||= begin
|
|
233
|
-
root = @service.fetch("state_root")
|
|
234
|
-
PrivateDirectory.verify!(root)
|
|
235
|
-
spool = File.join(root, "transfers")
|
|
236
|
-
Dir.mkdir(spool, 0o700) unless File.exist?(spool) || File.symlink?(spool)
|
|
237
|
-
TransferCodec.new(root: spool)
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def refusal(socket, code)
|
|
243
|
-
wire.write(socket, {"status" => "error", "error" => {"code" => code, "message" => "Protected authority refused #{code}"}},
|
|
244
|
-
deadline: wire.deadline(0.1))
|
|
245
|
-
rescue StandardError
|
|
246
|
-
nil
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def wire
|
|
250
|
-
Ace::Runtime::Molecules::ProtectedSocket
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# Structural admission of source-owned pins. Artifact authentication belongs
|
|
7
|
-
# to the selected entry owner; these references are never an execution grant.
|
|
8
|
-
module TaskContextEntry
|
|
9
|
-
LIMITS = {"manifest" => 65_536, "wrapper" => 33_554_432}.freeze
|
|
10
|
-
|
|
11
|
-
def self.validate!(value)
|
|
12
|
-
unless value.is_a?(Hash) && value.keys.sort == LIMITS.keys.sort
|
|
13
|
-
raise ArgumentError, "task context entry fields differ"
|
|
14
|
-
end
|
|
15
|
-
LIMITS.each do |key, limit|
|
|
16
|
-
reference = value[key]
|
|
17
|
-
unless reference.is_a?(Hash) && reference.keys.sort == %w[bytes path sha256] &&
|
|
18
|
-
reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, limit) &&
|
|
19
|
-
reference["sha256"].is_a?(String) && reference["sha256"].match?(/\A[0-9a-f]{64}\z/) &&
|
|
20
|
-
reference["path"].is_a?(String) && reference["path"].encoding == Encoding::UTF_8 &&
|
|
21
|
-
reference["path"].valid_encoding? && reference["path"].bytesize.between?(2, 4096) &&
|
|
22
|
-
!reference["path"].include?("\0") && reference["path"].start_with?("/") &&
|
|
23
|
-
File.expand_path(reference["path"]) == reference["path"]
|
|
24
|
-
raise ArgumentError, "task context entry reference differs"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
value
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "tempfile"
|
|
5
|
-
require "timeout"
|
|
6
|
-
require "ace/runtime/molecules/protected_socket"
|
|
7
|
-
require_relative "private_directory"
|
|
8
|
-
require_relative "../atoms/candidate_limits"
|
|
9
|
-
|
|
10
|
-
module Ace
|
|
11
|
-
module Assign
|
|
12
|
-
module Authority
|
|
13
|
-
# Binary framing after the existing authenticated control header. The
|
|
14
|
-
# Server selects purpose from a source handler's fixed operation table,
|
|
15
|
-
# admits a transfer slot before calling, and holds no journal lock here.
|
|
16
|
-
class TransferCodec
|
|
17
|
-
LIMITS = {prompt_text: [16_384, 1, 16_384], scope_boundary_observation: [65_536, 1, 65_536], candidate: [Atoms::CandidateLimits::BYTES, 1, Atoms::CandidateLimits::BYTES],
|
|
18
|
-
release_artifact: [64 * 1024 * 1024, 1, 64 * 1024 * 1024],
|
|
19
|
-
artifacts: [256 * 1024, 16, 64 * 1024],
|
|
20
|
-
receipt_artifacts: [272 * 1024, 17, 64 * 1024],
|
|
21
|
-
service_input: [64 * 1024, 1, 64 * 1024], assignment_snapshot: [Atoms::CandidateLimits::BYTES, 1, Atoms::CandidateLimits::BYTES]}.freeze
|
|
22
|
-
SHA256 = /\A[0-9a-f]{64}\z/
|
|
23
|
-
|
|
24
|
-
class Input
|
|
25
|
-
def initialize(file, descriptor)
|
|
26
|
-
@file, @descriptor = file, descriptor
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def bytes(index: 0)
|
|
30
|
-
unless index.is_a?(Integer) && index.between?(0, count - 1)
|
|
31
|
-
raise ArgumentError, "Invalid transferred part index"
|
|
32
|
-
end
|
|
33
|
-
part = @descriptor.fetch("parts").fetch(index)
|
|
34
|
-
offset = @descriptor.fetch("parts").take(index).sum { |entry| entry.fetch("bytes") }
|
|
35
|
-
@file.seek(offset)
|
|
36
|
-
bytes = (@file.read(part.fetch("bytes")) || "").b
|
|
37
|
-
unless bytes.bytesize == part["bytes"] && Digest::SHA256.hexdigest(bytes) == part["sha256"]
|
|
38
|
-
raise AttemptErrors::ReceiptRejected, "Transferred bytes no longer match accepted binding"
|
|
39
|
-
end
|
|
40
|
-
bytes
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def count
|
|
44
|
-
@descriptor.fetch("parts").length
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def initialize(root: nil)
|
|
49
|
-
@root = root && PrivateDirectory.verify!(root)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Upload write-EOF is required before invoking the mutation consumer.
|
|
53
|
-
# Persistent launch prompt streams use their fixed framed entry point below.
|
|
54
|
-
def receive(socket, descriptor:, purpose:, deadline:)
|
|
55
|
-
receive_parts(socket, descriptor: descriptor, purpose: purpose, deadline: deadline, eof: true) { |input| yield input }
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Fixed authenticated launch_control stream only. A declared exact
|
|
59
|
-
# prompt body is followed by the next bounded control frame; unlike
|
|
60
|
-
# public uploads, this private duplex connection remains writable.
|
|
61
|
-
def receive_launch_prompt(socket, descriptor:, transfer_id:, deadline:)
|
|
62
|
-
deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
|
|
63
|
-
marker = launch_prompt_marker(transfer_id)
|
|
64
|
-
receive_parts(socket, descriptor: descriptor, purpose: :prompt_text, deadline: deadline, eof: false) do |input|
|
|
65
|
-
boundary = Ace::Runtime::Molecules::ProtectedSocket.read(socket, deadline: deadline, limit: 16_384, with_size: true)
|
|
66
|
-
unless boundary.fetch(:data) == marker && boundary.fetch(:bytesize) == (JSON.generate(marker) + "\n").bytesize
|
|
67
|
-
reject!("Private prompt boundary differs")
|
|
68
|
-
end
|
|
69
|
-
yield input
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def send_launch_prompt(socket, bytes:, descriptor:, transfer_id:, deadline:)
|
|
74
|
-
deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
|
|
75
|
-
marker = launch_prompt_marker(transfer_id)
|
|
76
|
-
send(socket, parts: [bytes], descriptor: descriptor, purpose: :prompt_text, deadline: deadline)
|
|
77
|
-
Ace::Runtime::Molecules::ProtectedSocket.write(socket, marker, deadline: deadline, limit: 16_384)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def launch_prompt_marker(transfer_id)
|
|
81
|
-
unless transfer_id.is_a?(String) && transfer_id.match?(/\A[0-9a-f]{32}\z/)
|
|
82
|
-
raise ArgumentError, "invalid private prompt transfer identity"
|
|
83
|
-
end
|
|
84
|
-
{"type" => "launch_prompt_end", "transfer_id" => transfer_id}
|
|
85
|
-
end
|
|
86
|
-
private :launch_prompt_marker
|
|
87
|
-
|
|
88
|
-
def receive_parts(socket, descriptor:, purpose:, deadline:, eof:)
|
|
89
|
-
validate!(descriptor, purpose)
|
|
90
|
-
descriptor = descriptor.merge("parts" => descriptor.fetch("parts").map { |part| part.dup.freeze }.freeze).freeze
|
|
91
|
-
deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
|
|
92
|
-
raise ArgumentError, "receive requires protected source scratch" unless @root
|
|
93
|
-
PrivateDirectory.verify!(@root)
|
|
94
|
-
Tempfile.create(["transfer-", ".bytes"], @root, binmode: true) do |file|
|
|
95
|
-
digest = Digest::SHA256.new
|
|
96
|
-
descriptor.fetch("parts").each do |part|
|
|
97
|
-
part_digest = Digest::SHA256.new
|
|
98
|
-
remaining = part.fetch("bytes")
|
|
99
|
-
while remaining.positive?
|
|
100
|
-
chunk = read(socket, [remaining, 65_536].min, deadline)
|
|
101
|
-
reject!("Transferred bytes ended early") if chunk.nil?
|
|
102
|
-
file.write(chunk)
|
|
103
|
-
digest.update(chunk)
|
|
104
|
-
part_digest.update(chunk)
|
|
105
|
-
remaining -= chunk.bytesize
|
|
106
|
-
end
|
|
107
|
-
reject!("Transferred part digest differs") unless part_digest.hexdigest == part["sha256"]
|
|
108
|
-
end
|
|
109
|
-
reject!("Transferred aggregate digest differs") unless digest.hexdigest == descriptor["sha256"]
|
|
110
|
-
reject!("Transferred bytes exceed declared size") if eof && !read(socket, 1, deadline).nil?
|
|
111
|
-
file.flush
|
|
112
|
-
file.rewind
|
|
113
|
-
yield Input.new(file, descriptor)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
private :receive_parts
|
|
117
|
-
|
|
118
|
-
def descriptor(parts, purpose:)
|
|
119
|
-
reject!("Invalid transfer parts") unless parts.is_a?(Array) && parts.all? { |part| part.is_a?(String) }
|
|
120
|
-
digest = Digest::SHA256.new
|
|
121
|
-
parts.each { |part| digest.update(part.b) }
|
|
122
|
-
result = {"version" => 1, "bytes" => parts.sum(&:bytesize), "sha256" => digest.hexdigest,
|
|
123
|
-
"parts" => parts.map { |part| {"bytes" => part.bytesize, "sha256" => Digest::SHA256.hexdigest(part.b)} }}
|
|
124
|
-
validate!(result, purpose)
|
|
125
|
-
result
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# The caller writes the bounded JSON descriptor first, then these raw
|
|
129
|
-
# bytes. Both directions share the Server's original monotonic deadline.
|
|
130
|
-
def send(socket, parts:, descriptor:, purpose:, deadline:)
|
|
131
|
-
expected = self.descriptor(parts, purpose: purpose)
|
|
132
|
-
reject!("Export descriptor differs from bytes") unless expected == descriptor
|
|
133
|
-
deadline = [deadline, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30].min
|
|
134
|
-
parts.each do |part|
|
|
135
|
-
offset = 0
|
|
136
|
-
while offset < part.bytesize
|
|
137
|
-
wait(socket, deadline, write: true)
|
|
138
|
-
written = socket.write_nonblock(part.byteslice(offset, [65_536, part.bytesize - offset].min), exception: false)
|
|
139
|
-
offset += written unless written == :wait_writable
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def validate!(descriptor, purpose)
|
|
145
|
-
total, count, each = LIMITS.fetch(purpose) { reject!("Unknown source transfer purpose") }
|
|
146
|
-
unless descriptor.is_a?(Hash) && descriptor.keys.sort == %w[bytes parts sha256 version] &&
|
|
147
|
-
descriptor["version"] == 1 && descriptor["bytes"].is_a?(Integer) &&
|
|
148
|
-
descriptor["bytes"].between?(0, total) && descriptor["sha256"].is_a?(String) && descriptor["sha256"].match?(SHA256) &&
|
|
149
|
-
descriptor["parts"].is_a?(Array) && descriptor["parts"].length.between?(1, count) &&
|
|
150
|
-
descriptor["parts"].all? { |part| part.is_a?(Hash) && part.keys.sort == %w[bytes sha256] &&
|
|
151
|
-
part["bytes"].is_a?(Integer) && part["bytes"].between?(0, each) &&
|
|
152
|
-
part["sha256"].is_a?(String) && part["sha256"].match?(SHA256) } &&
|
|
153
|
-
descriptor["bytes"] == descriptor["parts"].sum { |part| part["bytes"] } &&
|
|
154
|
-
(!%i[candidate service_input prompt_text assignment_snapshot release_artifact].include?(purpose) || descriptor["bytes"].positive?)
|
|
155
|
-
reject!("Invalid or oversized transfer descriptor")
|
|
156
|
-
end
|
|
157
|
-
if purpose == :receipt_artifacts
|
|
158
|
-
receipt, *evidence = descriptor.fetch("parts")
|
|
159
|
-
unless receipt.fetch("bytes").between?(1, 16 * 1024) && evidence.sum { |part| part.fetch("bytes") } <= 256 * 1024
|
|
160
|
-
reject!("Receipt or evidence exceeds its fixed transfer limit")
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
true
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
def read(socket, count, deadline)
|
|
169
|
-
loop do
|
|
170
|
-
wait(socket, deadline)
|
|
171
|
-
bytes = socket.read_nonblock(count, exception: false)
|
|
172
|
-
return bytes unless bytes == :wait_readable
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def wait(socket, deadline, write: false)
|
|
177
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
178
|
-
raise Timeout::Error, "Transfer deadline exceeded" unless remaining.positive?
|
|
179
|
-
ready = IO.select(write ? nil : [socket], write ? [socket] : nil, nil, remaining)
|
|
180
|
-
raise Timeout::Error, "Transfer deadline exceeded" unless ready
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def reject!(message)
|
|
184
|
-
raise AttemptErrors::MalformedTransfer, message
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require_relative "../../../authority/protected_assignment_context"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module CLI
|
|
9
|
-
module Commands
|
|
10
|
-
# Public `ace-assign attempt` command namespace (interface contract:
|
|
11
|
-
# start, status, finish, reconcile).
|
|
12
|
-
module Attempt
|
|
13
|
-
# Shared coordinator construction, option validation, and JSON output.
|
|
14
|
-
module Base
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def protected_context(options)
|
|
18
|
-
keys = %i[mapping assignment result head candidate_generation mutation expected_generation inbox_context registration signature]
|
|
19
|
-
keys.each do |key|
|
|
20
|
-
if options.key?(key) && options[key].to_s.strip.empty?
|
|
21
|
-
raise Ace::Support::Cli::Error, "Empty protected --#{key.to_s.tr('_', '-')} selector"
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
context = @protected_assignment_context ||= Ace::Assign::Authority::ProtectedAssignmentContext.load
|
|
25
|
-
selected = keys.any? do |key|
|
|
26
|
-
options.key?(key)
|
|
27
|
-
end
|
|
28
|
-
context if context.protected_participant? || selected || context.mapping_hint?
|
|
29
|
-
rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
|
|
30
|
-
protected_transport_unavailable!
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def protected_attempt_request(context, options, usage)
|
|
34
|
-
params = {"assignment_id" => require_option(options, :assignment, usage),
|
|
35
|
-
"attempt_id" => require_option(options, :attempt, usage),
|
|
36
|
-
"expected_generation" => integer_option(options, :expected_generation, usage)}
|
|
37
|
-
context.verify_attempt_hints!(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"))
|
|
38
|
-
[context.client(options: options), params, require_option(options, :mutation, usage)]
|
|
39
|
-
rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
|
|
40
|
-
protected_transport_unavailable!
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def protected_call(client, operation, params, mutation)
|
|
44
|
-
client.call(operation, params, mutation_id: mutation, timeout: 30).data
|
|
45
|
-
rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
|
|
46
|
-
protected_transport_unavailable!
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def protected_transport_unavailable!
|
|
50
|
-
raise AttemptErrors::EvidenceUnavailable, "Protected authority outcome unavailable; retain original selectors and mutation for reconciliation"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def integer_option(options, key, usage, positive: false)
|
|
54
|
-
value = options[key]
|
|
55
|
-
unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A(?:0|[1-9][0-9]*)\z/))
|
|
56
|
-
raise Ace::Support::Cli::Error, "--#{key.to_s.tr('_', '-')} requires an exact integer: #{usage}"
|
|
57
|
-
end
|
|
58
|
-
number = value.is_a?(Integer) ? value : Integer(value, 10)
|
|
59
|
-
unless positive ? number.positive? : number >= 0
|
|
60
|
-
raise Ace::Support::Cli::Error, "--#{key.to_s.tr('_', '-')} is out of range"
|
|
61
|
-
end
|
|
62
|
-
number
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def build_coordinator
|
|
66
|
-
Organisms::AttemptCoordinator.new
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def require_option(options, key, usage)
|
|
70
|
-
value = options[key].to_s.strip
|
|
71
|
-
raise Ace::Support::Cli::Error, "Missing --#{key}: usage: ace-assign attempt #{usage}" if value.empty?
|
|
72
|
-
|
|
73
|
-
value
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def emit_json(payload)
|
|
77
|
-
puts JSON.generate(payload)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|