ace-lab 0.8.0 → 0.13.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +363 -92
  3. data/README.md +66 -100
  4. data/docs/usage.md +529 -404
  5. data/lib/ace/lab/atoms/install_schema.rb +373 -0
  6. data/lib/ace/lab/cli/commands/install.rb +161 -0
  7. data/lib/ace/lab/cli/commands/prepare.rb +118 -0
  8. data/lib/ace/lab/cli/commands/project_add.rb +101 -0
  9. data/lib/ace/lab/cli/commands/status.rb +109 -0
  10. data/lib/ace/lab/cli/commands/verify.rb +115 -0
  11. data/lib/ace/lab/cli.rb +21 -13
  12. data/lib/ace/lab/install_manifest_path.rb +23 -0
  13. data/lib/ace/lab/models/status_report.rb +38 -0
  14. data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
  15. data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
  16. data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
  17. data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
  18. data/lib/ace/lab/molecules/grant_resolver.rb +49 -23
  19. data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
  20. data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
  21. data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
  22. data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
  23. data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
  24. data/lib/ace/lab/molecules/project_registry.rb +251 -0
  25. data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
  26. data/lib/ace/lab/molecules/role_verifier.rb +223 -0
  27. data/lib/ace/lab/organisms/install.rb +213 -0
  28. data/lib/ace/lab/organisms/prepare.rb +156 -0
  29. data/lib/ace/lab/organisms/project_add.rb +170 -0
  30. data/lib/ace/lab/organisms/status_service.rb +70 -0
  31. data/lib/ace/lab/organisms/verify.rb +768 -0
  32. data/lib/ace/lab/version.rb +1 -1
  33. data/lib/ace/lab.rb +34 -6
  34. metadata +34 -102
  35. data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
  36. data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
  37. data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
  38. data/lib/ace/lab/atoms/service_input.rb +0 -92
  39. data/lib/ace/lab/cli/commands/assignment.rb +0 -45
  40. data/lib/ace/lab/cli/commands/service.rb +0 -240
  41. data/lib/ace/lab/models/service_receipt.rb +0 -18
  42. data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
  43. data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
  44. data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
  45. data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
  46. data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
  47. data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -139
  48. data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
  49. data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
  50. data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
  51. data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
  52. data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
  53. data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
  54. data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
  55. data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
  56. data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
  57. data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
  58. data/lib/ace/lab/molecules/service_executor.rb +0 -99
  59. data/lib/ace/lab/molecules/service_policy.rb +0 -289
  60. data/lib/ace/lab/organisms/authority_composition.rb +0 -87
  61. data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -149
  62. data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -123
  63. data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -688
  64. data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
  65. data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
  66. data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
  67. data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
  68. data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
  69. data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
  70. data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
  71. data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
  72. data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
  73. data/lib/ace/lab/organisms/service_request_service.rb +0 -249
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
- require "digest"
4
- require "net/http"
5
- require "timeout"
6
- require "rubygems/package"
7
- require "etc"
8
- require "ace/herdr/molecules/bounded_process"
9
- require "ace/assign/authority/private_directory"
10
-
11
- module Ace
12
- module Lab
13
- module Molecules
14
- # The admitted receiver owns permission. This boundary only invokes the
15
- # existing single-artifact publisher and independently checks registry bytes.
16
- class ProtectedPublicationPublisher
17
- def initialize(process: Ace::Herdr::Molecules::BoundedProcess, http: Net::HTTP)
18
- @process, @http = process, http
19
- end
20
-
21
- def verify!(publication:, artifact_digest:, platform:, deadline:)
22
- remaining = remaining!(deadline)
23
- path = "/api/v2/rubygems/#{publication.fetch('gem_name')}/versions/#{publication.fetch('version')}.json?platform=#{URI.encode_www_form_component(platform)}"
24
- connection = @http.new("rubygems.org", 443, nil)
25
- connection.use_ssl = true
26
- connection.open_timeout = remaining
27
- connection.read_timeout = remaining
28
- connection.write_timeout = remaining
29
- connection.max_retries = 0
30
- response = nil
31
- body = +""
32
- Timeout.timeout(remaining, IOError) do
33
- connection.start do |session|
34
- connection.read_timeout = remaining!(deadline)
35
- session.request(Net::HTTP::Get.new(path)) do |received|
36
- response = received
37
- received.read_body do |chunk|
38
- remaining!(deadline)
39
- raise IOError, "registry response exceeds bound" if body.bytesize + chunk.bytesize > 65_536
40
- body << chunk
41
- connection.read_timeout = remaining!(deadline)
42
- end
43
- end
44
- end
45
- end
46
- remaining!(deadline)
47
- return result("absent", "registry_version_absent") if response.is_a?(Net::HTTPNotFound)
48
- return result("uncertain", "registry_verification_unavailable") unless response.is_a?(Net::HTTPSuccess)
49
- value = JSON.parse(body, object_class: UniqueObject, max_nesting: 16)
50
- unless value.is_a?(Hash) && value["name"] == publication.fetch("gem_name") &&
51
- value["version"] == publication.fetch("version") && value["platform"] == platform &&
52
- value["sha"].is_a?(String) && value["sha"].match?(/\A[0-9a-f]{64}\z/) && value["yanked"] == false
53
- return result("uncertain", "registry_verification_invalid")
54
- end
55
- value["sha"] == artifact_digest ? result("succeeded", "registry_exact_artifact") : result("failed", "registry_artifact_conflict")
56
- rescue StandardError
57
- result("uncertain", "registry_verification_unavailable")
58
- end
59
-
60
- def push!(operation:, publication:, artifact:, artifact_digest:, otp:, deadline:)
61
- unless Process.uid == operation.fetch("executor_uid") && Process.euid == Process.uid &&
62
- operation.fetch("argv").last == "--scoped-publication" &&
63
- (otp.nil? || otp.is_a?(String) && otp.match?(/\A[0-9]{6}\z/))
64
- raise SecurityError, "fixed publication executor differs"
65
- end
66
- environment = {"PATH" => "/usr/bin:/bin", "LANG" => "C.UTF-8", "HOME" => Etc.getpwuid(Process.uid).dir}
67
- environment["GEM_HOST_OTP_CODE"] = otp if otp
68
- input = {"artifact" => artifact, "artifact_digest" => artifact_digest,
69
- "gem_name" => publication.fetch("gem_name"), "version" => publication.fetch("version")}
70
- process = @process.call(operation.fetch("argv"), stdin_data: JSON.generate(input),
71
- timeout_s: [remaining!(deadline), 30].min, output_limit: 1024, stderr_limit: 1024,
72
- cleanup_group: true, environment: environment, chdir: File.dirname(artifact))
73
- remaining!(deadline)
74
- return result("uncertain", "publisher_reply_unavailable") if process.oversized || !process.status.success?
75
- value = JSON.parse(process.stdout, object_class: UniqueObject, max_nesting: 4)
76
- allowed = {"otp-required" => "provider_rejected_before_acceptance", "otp-rejected" => "provider_rejected_before_acceptance",
77
- "otp-expired" => "provider_rejected_before_acceptance", "failed" => %w[provider_rejected_non_otp executor_credentials_unavailable invalid_artifact_selection artifact_identity_mismatch artifact_metadata_mismatch invalid_scoped_input],
78
- "uncertain" => %w[registry_verification_required provider_result_ambiguous unclassified_push_result publisher_interrupted artifact_changed_during_push]}
79
- unless value.is_a?(Hash) && value.keys.sort == %w[classification code] && Array(allowed[value["classification"]]).include?(value["code"])
80
- return result("uncertain", "publisher_reply_invalid")
81
- end
82
- value.freeze
83
- rescue StandardError
84
- result("uncertain", "publisher_reply_unavailable")
85
- ensure
86
- environment&.delete("GEM_HOST_OTP_CODE")
87
- end
88
-
89
- private
90
-
91
- class UniqueObject < Hash
92
- def []=(key, value)
93
- raise JSON::ParserError, "duplicate registry field" if key?(key)
94
- super
95
- end
96
- end
97
-
98
- def remaining!(deadline)
99
- value = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
100
- raise IOError, "publication deadline expired" unless value.positive?
101
- value
102
- end
103
-
104
- def result(classification, code)
105
- {"classification" => classification, "code" => code}.freeze
106
- end
107
- end
108
- end
109
- end
110
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ace/herdr/molecules/bounded_process"
5
- require "timeout"
6
- require "ace/assign/authority/private_directory"
7
-
8
- module Ace
9
- module Lab
10
- module Molecules
11
- # Source-owned receiver handler. Permission admission is a separate
12
- # authority operation; this runner never grants or reconstructs it.
13
- class ProtectedServiceHandler
14
- MAX_STDOUT = 16 * 1024
15
- MAX_STDERR = 8 * 1024
16
-
17
- def execute(operation:, envelope:, candidate_root:)
18
- unless Process.uid == operation.fetch("executor_uid") && Process.euid == operation.fetch("executor_uid")
19
- raise SecurityError, "handler executor differs"
20
- end
21
- root = Ace::Assign::Authority::PrivateDirectory.verify!(candidate_root)
22
- result = Ace::Herdr::Molecules::BoundedProcess.call(operation.fetch("argv"),
23
- stdin_data: JSON.generate(envelope), timeout_s: 30,
24
- output_limit: MAX_STDOUT, stderr_limit: MAX_STDERR, cleanup_group: true,
25
- environment: {"PATH" => "/usr/bin:/bin", "LANG" => "C.UTF-8", "HOME" => root}, chdir: root)
26
- return nil if result.oversized || !result.status.success?
27
- out = result.stdout
28
- response = JSON.parse(out)
29
- request = envelope.fetch("request")
30
- unless response.is_a?(Hash) && response.keys.sort == %w[evidence input_digest outcome request_id] &&
31
- response["request_id"] == request.fetch("request_id") && response["input_digest"] == request.fetch("input_digest") &&
32
- %w[succeeded failed].include?(response["outcome"]) && response["evidence"].is_a?(Array)
33
- return nil
34
- end
35
- response
36
- rescue JSON::ParserError, Timeout::Error, Ace::Herdr::Molecules::BoundedProcess::PostLaunchError,
37
- IOError, SystemCallError
38
- nil
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "etc"
5
- require "ace/assign/atoms/evidence_digest"
6
- require_relative "../authorization_path"
7
- require_relative "../atoms/service_input"
8
- require "ace/git/atoms/service_pr_input"
9
- require_relative "grant_resolver"
10
- require_relative "caller_authorizer"
11
- require_relative "service_policy"
12
- require_relative "../atoms/protected_workspace_prune_input"
13
- require_relative "../atoms/protected_publication_input"
14
-
15
- module Ace
16
- module Lab
17
- module Molecules
18
- # The receiver and authority share the existing input/policy owners.
19
- # Only composition supplies collaborators; no wire-selected policy,
20
- # executable, input path or caller identity is accepted here.
21
- class ProtectedServicePolicy
22
- def initialize(proposal_resolver:, document_loader: nil, cleanup_owner: nil)
23
- @proposal_resolver = proposal_resolver
24
- @cleanup_owner = cleanup_owner
25
- @document_loader = document_loader || -> { GrantResolver.trusted_document(Ace::Lab.authorization_path) }
26
- end
27
-
28
- def input_binding(bytes, expected_digest:, expected_target:, operation:)
29
- unless bytes.is_a?(String) && bytes.bytesize.between?(1, Atoms::ServiceInput::MAX_BYTES)
30
- raise ArgumentError, "structured service input exceeds its fixed limit"
31
- end
32
- input = if operation == "prune-preserved-workspace"
33
- Atoms::ProtectedWorkspacePruneInput.parse(bytes)
34
- elsif operation == "publish"
35
- Atoms::ProtectedPublicationInput.decode!(bytes)
36
- else
37
- JSON.parse(bytes)
38
- end
39
- raise ArgumentError, "structured service input must be an object" unless input.is_a?(Hash)
40
- Atoms::ServiceInput.validate!(input)
41
- if Ace::Git::Atoms::ServicePrInput::OPERATIONS.include?(operation)
42
- Ace::Git::Atoms::ServicePrInput.validate(input, operation: operation)
43
- end
44
- digest = Atoms::ServiceInput.digest(input)
45
- target = Atoms::ServiceInput.target(input)
46
- unless digest == expected_digest && target == expected_target
47
- raise SecurityError, "structured service input differs from the bound digest or target"
48
- end
49
- deep_freeze(input: input, input_digest: digest, target: target)
50
- rescue JSON::ParserError
51
- raise ArgumentError, "structured service input must be valid JSON"
52
- end
53
-
54
- def prepare!(binding, input_bytes:)
55
- input = input_binding(input_bytes, expected_digest: binding.fetch("input_digest"),
56
- expected_target: binding.fetch("target"), operation: binding.fetch("operation"))
57
- if binding["operation"] == "publish" && input.fetch(:input).dig("publication", "head") != binding.fetch("candidate_head")
58
- raise SecurityError, "publication input head differs from admitted candidate"
59
- end
60
- authorized = authorize!(binding)
61
- canonical = snapshot(binding).merge("executor_uid" => authorized.fetch(:operation).fetch("executor_uid"), "transport" => "unix")
62
- deep_freeze(authorized.merge(binding: canonical, input: input.fetch(:input)))
63
- end
64
-
65
- # Selected by installed composition, independently of receiver fields.
66
- # Ordinary services never acquire this domain-specific root capability.
67
- def dispatch_owner_binding!(binding)
68
- return nil unless binding.fetch("operation") == "prune-preserved-workspace"
69
- unless @cleanup_owner
70
- raise SecurityError, "fixed cleanup owner composition is unavailable"
71
- end
72
- @cleanup_owner.identity!
73
- end
74
-
75
- # Called again inside each claim/begin CAS retry using the immutable
76
- # binding and ephemeral parsed input, never a caller filesystem path.
77
- def authorize!(binding)
78
- uid = binding.fetch("caller_uid")
79
- document = visible_document!(project: binding.fetch("project_id"), uid: uid)
80
- resolver = ->(reference, exact) { @proposal_resolver.call(binding.fetch("project_id"), reference, exact) }
81
- policy = ServicePolicy.new(document, resolver)
82
- operation = policy.operation!(binding.fetch("operation"), project: binding.fetch("project_id"), service_id: binding.fetch("service_id"))
83
- unless operation.fetch("transport", "local") == "local"
84
- raise Ace::Lab::InvalidConfigurationError, "protected receiver requires a fixed local handler operation"
85
- end
86
- unless operation.fetch("executor_uid").positive? && operation.fetch("executor_uid") != uid
87
- raise SecurityError, "protected executor must be a separate nonroot account"
88
- end
89
- if binding.key?("executor_uid") && binding["executor_uid"] != operation.fetch("executor_uid")
90
- raise SecurityError, "configured executor differs from the recorded claim"
91
- end
92
- decision = policy.authorize!(binding.fetch("authorization"), binding)
93
- digest = Ace::Assign::Atoms::EvidenceDigest.digest("operation" => operation, "authorization" => decision,
94
- "caller_uid" => uid, "project_id" => binding.fetch("project_id"))
95
- if binding.key?("policy_digest") && binding["policy_digest"] != digest
96
- raise SecurityError, "service policy changed after claim"
97
- end
98
- deep_freeze(operation: snapshot(operation), policy_digest: digest,
99
- operation_digest: Ace::Assign::Atoms::EvidenceDigest.digest(operation))
100
- end
101
-
102
- # Read-only selection of the installed named cleanup receiver; no
103
- # proposal authorization or effect capability is minted by preview.
104
- def workspace_prune_receiver!(project:, uid:, service_id:, executor_uid:)
105
- document = visible_document!(project: project, uid: uid)
106
- policy = ServicePolicy.new(document, ->(*) { raise SecurityError, "preview cannot resolve proposal grants" })
107
- operation = policy.operation!("prune-preserved-workspace", project: project, service_id: service_id)
108
- unless operation.fetch("executor_uid") == executor_uid && executor_uid.is_a?(Integer) && executor_uid.positive? && executor_uid != uid
109
- raise SecurityError, "preview receiver differs from the installed cleanup operation"
110
- end
111
- true
112
- end
113
-
114
- def visible!(project:, uid:)
115
- visible_document!(project: project, uid: uid)
116
- true
117
- end
118
-
119
- def authorize_update!(existing, replacement, _pending)
120
- initial_effect = existing.nil? && %w[accepted uncertain].include?(replacement["state"])
121
- fresh_dispatch = existing && existing["dispatch_phase"] == "issued" && replacement["dispatch_phase"] == "dispatch_started"
122
- authorize!(replacement) if initial_effect || fresh_dispatch
123
- true
124
- end
125
-
126
- private
127
-
128
- def visible_document!(project:, uid:)
129
- raise SecurityError, "protected caller UID is invalid" unless uid.is_a?(Integer) && uid.positive?
130
- username = begin
131
- Etc.getpwuid(uid).name
132
- rescue ArgumentError
133
- nil
134
- end
135
- document = @document_loader.call
136
- identities = CallerAuthorizer.matchable_identities(username, uid)
137
- visibility = CallerAuthorizer.new(principals: document.fetch("principals", {}), identity: identities)
138
- raise SecurityError, "caller project grant has been revoked" unless visibility.authorized?(project)
139
- document
140
- end
141
-
142
- def snapshot(value)
143
- JSON.parse(JSON.generate(value))
144
- end
145
-
146
- def deep_freeze(value)
147
- case value
148
- when Hash then value.each { |key, item| deep_freeze(key); deep_freeze(item) }
149
- when Array then value.each { |item| deep_freeze(item) }
150
- end
151
- value.freeze
152
- end
153
- end
154
- end
155
- end
156
- end
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ace/herdr/molecules/bounded_process"
5
- require "socket"
6
- require "timeout"
7
- require "time"
8
-
9
- module Ace
10
- module Lab
11
- module Molecules
12
- # Executes one fixed, deployment-configured argv under its OS account.
13
- # Domain handlers receive structured JSON on stdin and return a small
14
- # receipt; their stdout/stderr are never journaled or returned to callers.
15
- class ServiceExecutor
16
- # The trusted policy is reloaded through +policy_loader+ immediately
17
- # before the effect: a revocation or operation change after the claim
18
- # must still stop dispatch. The candidate head is revalidated through
19
- # +head_loader+ for the same reason: a concurrent commit must not let
20
- # a stale reviewed candidate execute.
21
- def execute(operation:, request:, input:, policy_loader:, authorization:, head_loader:)
22
- fresh = policy_loader.call
23
- current = fresh.operation!(request.fetch("operation"), project: request.fetch("project_id"),
24
- service_id: request.fetch("service_id"))
25
- raise SecurityError, "executor operation changed before dispatch" unless current == operation
26
- fresh.authorize!(authorization, request)
27
- unless head_loader.call == request.fetch("candidate_head")
28
- raise SecurityError, "candidate head changed before dispatch"
29
- end
30
- if Time.iso8601(current.fetch("lease_expires_at")) <= Time.now.utc
31
- raise SecurityError, "executor lease has expired"
32
- end
33
- out = if operation.fetch("transport", "local") == "unix"
34
- invoke_unix(operation, request, input)
35
- else
36
- invoke_local(operation, request, input)
37
- end
38
- return nil unless out && out.bytesize <= 16 * 1024
39
- response = JSON.parse(out)
40
- return nil unless valid_response?(response, request)
41
- {"outcome" => response.fetch("outcome"), "evidence" => response.fetch("evidence"),
42
- "executor_uid" => current.fetch("executor_uid")}
43
- rescue JSON::ParserError, Timeout::Error, SystemCallError, IOError,
44
- Ace::Herdr::Molecules::BoundedProcess::PostLaunchError
45
- # Transport failures after the claim (broken pipe, connection
46
- # reset, socket loss) leave the effect unknown: uncertainty, not a
47
- # classified crash, is the honest outcome.
48
- nil
49
- end
50
-
51
- private
52
-
53
- # The existing process owner bounds both streams and the whole run,
54
- # and cleans the owned process group on timeout and normal exit.
55
- def invoke_local(operation, request, input)
56
- unless Process.uid == operation.fetch("executor_uid") && Process.euid == operation.fetch("executor_uid")
57
- raise SecurityError, "current OS identity is not the configured executor"
58
- end
59
- result = Ace::Herdr::Molecules::BoundedProcess.call(operation.fetch("argv"),
60
- stdin_data: JSON.generate({"request" => request, "input" => input}),
61
- timeout_s: 30, output_limit: 16 * 1024, stderr_limit: 8192, cleanup_group: true)
62
- result.stdout if result.status.success? && !result.oversized
63
- end
64
-
65
- def invoke_unix(operation, request, input)
66
- path = operation.fetch("socket_path")
67
- stat = File.lstat(path)
68
- unless stat.socket? && stat.uid == operation.fetch("executor_uid") && (stat.mode & 0o002).zero?
69
- raise SecurityError, "service socket owner or permissions do not match the executor"
70
- end
71
- # One deadline covers connecting, writing, and reading: a stalled
72
- # peer must leave the claimed effect uncertain, never block.
73
- Timeout.timeout(30) do
74
- UNIXSocket.open(path) do |socket|
75
- peer_uid, = socket.getpeereid
76
- raise SecurityError, "service peer identity does not match the executor" unless peer_uid == stat.uid
77
- socket.write(JSON.generate({"request" => request, "input" => input}) + "\n")
78
- socket.flush
79
- socket.gets(16 * 1024 + 1)
80
- end
81
- end
82
- end
83
-
84
- def valid_response?(response, request)
85
- response.is_a?(Hash) && response.keys.sort == %w[evidence input_digest outcome request_id] &&
86
- response["request_id"] == request["request_id"] &&
87
- response["input_digest"] == request["input_digest"] &&
88
- %w[succeeded failed].include?(response["outcome"]) &&
89
- response["evidence"].is_a?(Array) && !response["evidence"].empty? &&
90
- response["evidence"].all? do |item|
91
- item.is_a?(Hash) && item.keys.sort == %w[ref sha256] &&
92
- item["ref"].is_a?(String) && item["ref"].match?(/\A[a-zA-Z0-9_.:\/-]{1,256}\z/) &&
93
- item["sha256"].is_a?(String) && item["sha256"].match?(/\A[0-9a-f]{64}\z/)
94
- end
95
- end
96
- end
97
- end
98
- end
99
- end