ace-assign 0.70.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 -2099
- 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 -1
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
- 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/molecules/runtime_control_surface_runner.rb +2 -1
- 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 -161
- 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/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/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_transfer.rb +0 -327
- 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/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -455
- 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 -207
- 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_settlement.rb +0 -152
- data/lib/ace/assign/authority/endcap_services.rb +0 -598
- 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/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 -94
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
- 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_results.rb +0 -196
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- 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 -304
- 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/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 -187
- 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 -153
- 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 -1912
- 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 -273
- 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 -314
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- 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,196 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "receipt_transfer"
|
|
4
|
-
require_relative "../molecules/canonical_evidence"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Authority
|
|
9
|
-
# A worker submits an attributable result. This never accepts a terminal
|
|
10
|
-
# outcome, an independent review, or an external delivery effect.
|
|
11
|
-
module OrdinaryResults
|
|
12
|
-
RESULT_OPERATIONS = %w[submit_result result_status result_snapshot].freeze
|
|
13
|
-
RESULT_TRANSFERS = {
|
|
14
|
-
"submit_result" => {direction: :upload, purpose: :receipt_artifacts, roles: %i[worker supervisor]},
|
|
15
|
-
"result_snapshot" => {direction: :download, purpose: :receipt_artifacts, roles: %i[worker supervisor]}
|
|
16
|
-
}.freeze
|
|
17
|
-
RESULT_KEYS = %w[assignment_id attempt_id expected_generation pane head receipt_sha256 transfer].freeze
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def validate_result!(request)
|
|
22
|
-
params = request.fetch("params")
|
|
23
|
-
if request.fetch("operation") != "submit_result"
|
|
24
|
-
snapshot = request.fetch("operation") == "result_snapshot"
|
|
25
|
-
strict!(params, %w[assignment_id attempt_id] + (snapshot ? ["result_id"] : []))
|
|
26
|
-
token!(params.fetch("result_id")) if snapshot
|
|
27
|
-
raise ArgumentError, "ordinary result status mutation must be null" unless request.fetch("mutation_id").nil?
|
|
28
|
-
else
|
|
29
|
-
strict!(params, RESULT_KEYS)
|
|
30
|
-
token!(request.fetch("mutation_id"))
|
|
31
|
-
unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation").positive? &&
|
|
32
|
-
params.fetch("head").is_a?(String) && params.fetch("head").match?(/\A[0-9a-f]{40}\z/) &&
|
|
33
|
-
params.fetch("receipt_sha256").is_a?(String) && params.fetch("receipt_sha256").match?(/\A[0-9a-f]{64}\z/)
|
|
34
|
-
raise ArgumentError, "ordinary result selection is invalid"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
%w[assignment_id attempt_id].each { |key| token!(params.fetch(key)) }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def ordinary_result(request, peer, role, deadline, transfer)
|
|
41
|
-
validate_result!(request)
|
|
42
|
-
params = request.fetch("params")
|
|
43
|
-
commit = @journal.ref_value
|
|
44
|
-
registered = registration(params.fetch("assignment_id"), commit: commit)
|
|
45
|
-
raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
|
|
46
|
-
if request.fetch("operation") != "submit_result"
|
|
47
|
-
events = result_events(params, commit)
|
|
48
|
-
result = retained_ordinary_result(params, events, commit)
|
|
49
|
-
if request.fetch("operation") == "result_snapshot"
|
|
50
|
-
unless result && result.fetch("result_id") == params.fetch("result_id")
|
|
51
|
-
raise AttemptErrors::NotFound, "ordinary exact result is unavailable"
|
|
52
|
-
end
|
|
53
|
-
receipt = result.fetch("receipt")
|
|
54
|
-
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
55
|
-
parts = [JSON.generate(receipt)] + receipt.fetch("artifacts").map { |artifact|
|
|
56
|
-
canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
57
|
-
**ordinary_result_context(result.fetch("binding")), commit: commit)
|
|
58
|
-
}
|
|
59
|
-
authorize!(request, peer, role, deadline)
|
|
60
|
-
return {data: ordinary_result_projection(result).merge("journal_commit" => commit), transfer_parts: parts, replayed: false}
|
|
61
|
-
end
|
|
62
|
-
authorize!(request, peer, role, deadline)
|
|
63
|
-
return {data: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
|
|
64
|
-
"journal_commit" => commit, "generation" => @journal.authority_generation(events),
|
|
65
|
-
"state" => Molecules::CanonicalAttemptState.derive(events), "submitted_result" => result && ordinary_result_projection(result)}, replayed: false}
|
|
66
|
-
end
|
|
67
|
-
unless %i[worker supervisor].include?(role)
|
|
68
|
-
raise AttemptErrors::UnauthorizedIdentity, "ordinary result requires its original worker"
|
|
69
|
-
end
|
|
70
|
-
authorize_start_replay!(request, peer, role, deadline)
|
|
71
|
-
admitted = ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
|
|
72
|
-
artifact_field: "artifacts", reference_key: "path")
|
|
73
|
-
digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
|
|
74
|
-
keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
|
|
75
|
-
@exclusion.with_shared_multi(keys, deadline: deadline) do
|
|
76
|
-
@journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
77
|
-
mutation_id: request.fetch("mutation_id"), operation: "submit_result", parameters_digest: digest,
|
|
78
|
-
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
79
|
-
replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, generation|
|
|
80
|
-
authorize_start_replay!(request, peer, role, deadline)
|
|
81
|
-
unless Molecules::CanonicalAttemptState.derive(events) == "running"
|
|
82
|
-
raise AttemptErrors::Conflict, "ordinary result requires the original running attempt"
|
|
83
|
-
end
|
|
84
|
-
raise AttemptErrors::Conflict, "ordinary attempt already submitted its result" if retained_ordinary_result(params, events, prefix)
|
|
85
|
-
observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: params.fetch("head"), deadline: deadline)
|
|
86
|
-
start_replay_owner!(params, observed.fetch(:identity))
|
|
87
|
-
ordinary_result_plan(params, peer, admitted, observed, events, prefix, generation + 1)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def result_events(params, commit)
|
|
93
|
-
events = @journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
94
|
-
.select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
|
|
95
|
-
unless Models::EvidenceEvent.chain_valid?(events) && events.one? { |event| event["type"] == "process_start" }
|
|
96
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary result attempt evidence is unavailable"
|
|
97
|
-
end
|
|
98
|
-
events
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def ordinary_result_plan(params, peer, admitted, observed, events, commit, generation)
|
|
102
|
-
original = admitted.fetch(:receipt)
|
|
103
|
-
original_receipt = Models::ExecutionReceipt.from_h(original)
|
|
104
|
-
unless original_receipt.digest == Atoms::EvidenceDigest.digest(original_receipt.digest_payload)
|
|
105
|
-
raise AttemptErrors::ReceiptRejected, "ordinary original result digest differs"
|
|
106
|
-
end
|
|
107
|
-
identity = observed.fetch(:identity)
|
|
108
|
-
expected_producer = {"actor" => identity.actor, "role" => identity.role, "runtime" => identity.runtime}
|
|
109
|
-
validate_ordinary_result_receipt!(original, expected_producer)
|
|
110
|
-
id = SecureRandom.hex(16)
|
|
111
|
-
binding = params.slice("assignment_id", "attempt_id", "head").merge("authority" => "ordinary", "project_id" => @project,
|
|
112
|
-
"result_id" => id, "worker_uid" => peer.fetch("uid"), "producer" => expected_producer,
|
|
113
|
-
"runtime_binding" => identity.runtime_binding, "checkout" => observed.fetch(:checkout),
|
|
114
|
-
"generation" => generation, "uploaded_receipt_sha256" => admitted.fetch(:receipt_sha256),
|
|
115
|
-
"original_receipt_digest" => original_receipt.digest)
|
|
116
|
-
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
117
|
-
plan = admitted.fetch(:artifacts).empty? ? {events: [], blobs: {}, references: []} :
|
|
118
|
-
canonical.import_plan(**ordinary_result_context(binding), artifacts: admitted.fetch(:artifacts),
|
|
119
|
-
admitted_after_event_digest: events.last.fetch("digest"))
|
|
120
|
-
receipt = original.except("digest", "recorded_at").merge("artifacts" => plan.fetch(:references).map { |reference|
|
|
121
|
-
{"path" => reference.fetch("ref"), "sha256" => reference.fetch("sha256")}
|
|
122
|
-
})
|
|
123
|
-
pending = @journal.send(:chain_mutation_events, params.fetch("attempt_id"), events.last.fetch("digest"), plan.fetch(:events))
|
|
124
|
-
reader = ->(_data, artifact) { canonical.read_pending({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
125
|
-
**ordinary_result_context(binding), current_events: events, pending_events: pending, blobs: plan.fetch(:blobs), commit: commit) }
|
|
126
|
-
verified = verify_ordinary_result_receipt(params, receipt, binding, commit, reader)
|
|
127
|
-
if JSON.generate(verified.to_h).bytesize > 16_384
|
|
128
|
-
raise AttemptErrors::ReceiptRejected, "canonical ordinary receipt exceeds its transfer limit"
|
|
129
|
-
end
|
|
130
|
-
payload = {"authority" => "ordinary", "result_id" => id, "binding" => binding, "receipt" => verified.to_h}
|
|
131
|
-
plan.merge(events: plan.fetch(:events) + [{type: "result_submitted", payload: payload}],
|
|
132
|
-
data: ordinary_result_projection(payload))
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def ordinary_result_projection(payload)
|
|
136
|
-
payload.slice("result_id").merge(payload.fetch("binding").slice("head", "generation"),
|
|
137
|
-
"receipt_digest" => payload.fetch("receipt").fetch("digest"), "verdict" => payload.fetch("receipt").fetch("verdict"),
|
|
138
|
-
"state" => "running", "authority" => "ordinary")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def ordinary_result_context(binding)
|
|
142
|
-
{kind: "result", project_id: @project, assignment_id: binding.fetch("assignment_id"), attempt_id: binding.fetch("attempt_id"),
|
|
143
|
-
peer_uid: binding.fetch("worker_uid"), binding: binding, request_id_or_event_id: binding.fetch("result_id"), generation: binding.fetch("generation")}
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def validate_ordinary_result_receipt!(receipt, producer)
|
|
147
|
-
unless receipt["producer"] == producer && (receipt.keys - Models::ExecutionReceipt.from_h(receipt).to_h.keys).empty? &&
|
|
148
|
-
!receipt["campaign"] && !receipt["review"] && receipt["operation"] != "review" &&
|
|
149
|
-
!Molecules::ReceiptVerifier::EXTERNAL_EFFECT_OPERATIONS.include?(receipt["operation"])
|
|
150
|
-
raise AttemptErrors::ReceiptRejected, "ordinary result producer or operation requires another evidence owner"
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def verify_ordinary_result_receipt(params, receipt, binding, commit, reader)
|
|
155
|
-
validate_ordinary_result_receipt!(receipt, binding.fetch("producer"))
|
|
156
|
-
attempt = @journal.derived_attempts(params.fetch("assignment_id"), commit: commit).find { |item| item.attempt_id == params.fetch("attempt_id") }
|
|
157
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary result attempt is unavailable" unless attempt
|
|
158
|
-
Array(receipt["artifacts"]).each { |artifact| reader.call(receipt, artifact) }
|
|
159
|
-
Molecules::ReceiptVerifier.new(artifact_reader: reader).verify_result!(receipt,
|
|
160
|
-
attempt: attempt, live_head: binding.fetch("head"), repo_root: @journal.repo_root)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def retained_ordinary_result(params, events, commit)
|
|
164
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary result chain is corrupt" unless Models::EvidenceEvent.chain_valid?(events)
|
|
165
|
-
records = events.select { |event| event["type"] == "result_submitted" }
|
|
166
|
-
replies = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "submit_result" }
|
|
167
|
-
return nil if records.empty? && replies.empty?
|
|
168
|
-
unless records.one? && replies.one?
|
|
169
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary result provenance is missing or duplicated"
|
|
170
|
-
end
|
|
171
|
-
payload = records.first.fetch("payload")
|
|
172
|
-
binding = payload.fetch("binding")
|
|
173
|
-
start = events.find { |event| event["type"] == "process_start" }.fetch("payload")
|
|
174
|
-
unless payload.keys.sort == %w[authority binding receipt result_id] && payload["authority"] == "ordinary" &&
|
|
175
|
-
payload["result_id"].is_a?(String) && payload["result_id"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
176
|
-
binding.keys.sort == %w[assignment_id attempt_id authority checkout generation head original_receipt_digest producer project_id result_id runtime_binding uploaded_receipt_sha256 worker_uid] &&
|
|
177
|
-
binding["authority"] == "ordinary" && binding["result_id"] == payload["result_id"] &&
|
|
178
|
-
binding["head"] == binding.dig("checkout", "head") &&
|
|
179
|
-
binding.values_at("project_id", "assignment_id", "attempt_id", "runtime_binding") ==
|
|
180
|
-
[@project, params.fetch("assignment_id"), params.fetch("attempt_id"), start.fetch("runtime_binding")] &&
|
|
181
|
-
binding.fetch("producer") == start.slice("actor", "role", "runtime") &&
|
|
182
|
-
binding.fetch("worker_uid") == start.dig("runtime_binding", "process_identity", "uid") &&
|
|
183
|
-
payload.fetch("receipt").fetch("producer") == binding.fetch("producer") &&
|
|
184
|
-
replies.first.dig("payload", "data").slice(*ordinary_result_projection(payload).keys) == ordinary_result_projection(payload)
|
|
185
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary result owner differs"
|
|
186
|
-
end
|
|
187
|
-
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
188
|
-
reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
189
|
-
**ordinary_result_context(binding), commit: commit) }
|
|
190
|
-
verify_ordinary_result_receipt(params, payload.fetch("receipt"), binding, commit, reader)
|
|
191
|
-
payload
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "socket_server"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Assign
|
|
7
|
-
module Authority
|
|
8
|
-
# Explicit ordinary admission over Assign's existing wire/transfer owner.
|
|
9
|
-
# The Lab composition supplies a trusted installed admission object and
|
|
10
|
-
# closed handlers. No protected Deployment or kernel adapter is replaced.
|
|
11
|
-
class OrdinaryServer < SocketServer
|
|
12
|
-
def initialize(admission:, lifecycle:, services: [])
|
|
13
|
-
@admission = admission
|
|
14
|
-
super(service: admission.service, lifecycle: lifecycle, services: services)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def verify_listener!
|
|
20
|
-
@admission.verify_listener!
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def authorize_transfer!(request:, peer:, role:, deadline:)
|
|
24
|
-
@lifecycle.authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def dispatch_lifecycle(request:, peer:, role:, deadline:, transfer: nil)
|
|
28
|
-
@lifecycle.dispatch(request: request, peer: peer, role: role, deadline: deadline, transfer: transfer)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def receive(socket)
|
|
32
|
-
deadline = wire.deadline(30)
|
|
33
|
-
peer = @admission.peer(socket, deadline: deadline)
|
|
34
|
-
ingress = Ace::Runtime::Molecules::ProtectedSocket::Ingress.new(socket)
|
|
35
|
-
frame = wire.read(ingress, deadline: [deadline, wire.deadline(5)].min, limit: 16_384, with_size: true)
|
|
36
|
-
request = frame.fetch(:data)
|
|
37
|
-
unless request.is_a?(Hash) && request.keys.sort == %w[mutation_id operation params project_id version] &&
|
|
38
|
-
request["version"].is_a?(Integer) && request["version"] == 1 &&
|
|
39
|
-
request["operation"].is_a?(String) && request["params"].is_a?(Hash)
|
|
40
|
-
raise ArgumentError, "invalid ordinary authority envelope"
|
|
41
|
-
end
|
|
42
|
-
role = @admission.authorize!(request: request, peer: peer, deadline: deadline)
|
|
43
|
-
binding = @lifecycle.transfer_binding(request)
|
|
44
|
-
bodyless_read!(ingress, deadline) unless binding && binding.fetch(:direction) == :upload
|
|
45
|
-
# Both binary bodies and EOF use ancillary-rejecting reads. Replies
|
|
46
|
-
# use the original duplex socket through the wrapper below.
|
|
47
|
-
dispatch_request(Duplex.new(socket, ingress), request, peer, role, deadline, frame_bytes: frame.fetch(:bytesize))
|
|
48
|
-
rescue ArgumentError, KeyError, AttemptErrors::MalformedTransfer
|
|
49
|
-
refusal(socket, "invalid_input")
|
|
50
|
-
rescue AttemptErrors::UnauthorizedIdentity, SecurityError
|
|
51
|
-
refusal(socket, "unauthorized")
|
|
52
|
-
rescue AttemptErrors::Conflict
|
|
53
|
-
refusal(socket, "conflict")
|
|
54
|
-
rescue AttemptErrors::NotFound
|
|
55
|
-
refusal(socket, "missing")
|
|
56
|
-
rescue AttemptErrors::BoundedResultUnavailable
|
|
57
|
-
refusal(socket, "bounded_result")
|
|
58
|
-
rescue StandardError
|
|
59
|
-
refusal(socket, "evidence_unavailable")
|
|
60
|
-
ensure
|
|
61
|
-
socket.close unless socket.closed?
|
|
62
|
-
@mutex.synchronize { @connections.delete(socket) }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
class Duplex
|
|
66
|
-
def initialize(socket, ingress) = (@socket, @ingress = socket, ingress)
|
|
67
|
-
def to_io = @socket
|
|
68
|
-
def read_nonblock(*args, **options) = @ingress.read_nonblock(*args, **options)
|
|
69
|
-
def write_nonblock(*args, **options) = @socket.write_nonblock(*args, **options)
|
|
70
|
-
end
|
|
71
|
-
private_constant :Duplex
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Authority
|
|
6
|
-
# Original descriptor selection shared by canonical Inbox consumers.
|
|
7
|
-
# The held historical manifest/key owner supplies the exact artifact.
|
|
8
|
-
class OriginalInboxSelection
|
|
9
|
-
def initialize(history:, authority_id:)
|
|
10
|
-
@history, @authority_id = history, authority_id
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def load!(events:, params:, map:, journal:, commit:)
|
|
14
|
-
provisioning = events.select { |event| event["type"] == "scope_provisioning" }
|
|
15
|
-
reservations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
|
|
16
|
-
unless provisioning.one? && reservations.one?
|
|
17
|
-
raise AttemptErrors::EvidenceUnavailable, "context completion original selection is ambiguous"
|
|
18
|
-
end
|
|
19
|
-
identity = provisioning.first.fetch("payload")
|
|
20
|
-
reserved = reservations.first.fetch("payload").fetch("data")
|
|
21
|
-
original = @history.descriptor!(sha256: identity.fetch("descriptor_sha256"))
|
|
22
|
-
selected = original.mapping(params.fetch("mapping_id"))
|
|
23
|
-
unless identity.keys.sort == %w[deployment_digest descriptor_sha256 reservation_generation slot_id] &&
|
|
24
|
-
reserved.values_at("project_id", "assignment_id", "attempt_id", "mapping_id") ==
|
|
25
|
-
[map.fetch("project_id"), params.fetch("assignment_id"), params.fetch("attempt_id"), params.fetch("mapping_id")] &&
|
|
26
|
-
identity.fetch("reservation_generation") == reserved.fetch("reservation_generation") &&
|
|
27
|
-
identity.fetch("deployment_digest") == original.mapping_digest(params.fetch("mapping_id")) &&
|
|
28
|
-
selected.fetch("project_id") == map.fetch("project_id") && selected.fetch("authority_id") == @authority_id &&
|
|
29
|
-
identity.fetch("slot_id") == selected.fetch("execution_scope").fetch("slot_id") &&
|
|
30
|
-
original.project(selected.fetch("project_id")).values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
|
|
31
|
-
[journal.repo_root, journal.ref, journal.checkout_root]
|
|
32
|
-
raise AttemptErrors::EvidenceUnavailable, "context completion original association differs"
|
|
33
|
-
end
|
|
34
|
-
journal.event_commit!(assignment_id: params.fetch("assignment_id"), event_digest: provisioning.first.fetch("digest"), commit: commit)
|
|
35
|
-
[original, selected]
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "fiddle"
|
|
3
|
-
|
|
4
|
-
module Ace
|
|
5
|
-
module Assign
|
|
6
|
-
module Authority
|
|
7
|
-
# Linux POSIX access ACL evaluation for a fixed unprivileged principal.
|
|
8
|
-
# No credential switching; unsupported or unreadable policy refuses admission.
|
|
9
|
-
class PosixAcl
|
|
10
|
-
def entries(path, attribute: "system.posix_acl_access")
|
|
11
|
-
unless %w[system.posix_acl_access system.posix_acl_default].include?(attribute)
|
|
12
|
-
raise ArgumentError, "unsupported POSIX ACL attribute"
|
|
13
|
-
end
|
|
14
|
-
raise Ace::Runtime::RuntimeUnavailableError, "receiver ACL inspection requires Linux" unless RUBY_PLATFORM.include?("linux")
|
|
15
|
-
function = Fiddle::Function.new(Fiddle::Handle::DEFAULT["lgetxattr"],
|
|
16
|
-
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T], Fiddle::TYPE_SSIZE_T)
|
|
17
|
-
buffer = Fiddle::Pointer.malloc(8192, Fiddle::RUBY_FREE)
|
|
18
|
-
count = function.call(path, attribute, buffer, 8192)
|
|
19
|
-
return nil if count == -1 && Fiddle.last_error == Errno::ENODATA::Errno
|
|
20
|
-
raise Ace::Runtime::RuntimeUnavailableError, "receiver access ACL is unreadable" if count < 4 || (count - 4) % 8 != 0
|
|
21
|
-
bytes = buffer[0, count]
|
|
22
|
-
raise Ace::Runtime::RuntimeUnavailableError, "unknown receiver ACL version" unless bytes.unpack1("L<") == 2
|
|
23
|
-
rows = bytes.byteslice(4..).unpack("S<S<L<" * ((count - 4) / 8)).each_slice(3).to_a
|
|
24
|
-
validate!(rows)
|
|
25
|
-
rows
|
|
26
|
-
rescue Fiddle::DLError
|
|
27
|
-
raise Ace::Runtime::RuntimeUnavailableError, "receiver ACL inspection is unavailable"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def searchable?(path, stat:, uid:, groups:)
|
|
31
|
-
rows = entries(path)
|
|
32
|
-
unless rows
|
|
33
|
-
shift = uid == stat.uid ? 6 : (groups.include?(stat.gid) ? 3 : 0)
|
|
34
|
-
return ((stat.mode >> shift) & 1) == 1
|
|
35
|
-
end
|
|
36
|
-
return (rows.find { |tag, _perm, _id| tag == 1 }[1] & 1) == 1 if uid == stat.uid
|
|
37
|
-
mask = rows.find { |tag, _perm, _id| tag == 16 }&.at(1) || 7
|
|
38
|
-
named = rows.find { |tag, _perm, id| tag == 2 && id == uid }
|
|
39
|
-
return (named[1] & mask & 1) == 1 if named
|
|
40
|
-
matched = rows.select { |tag, _perm, id| (tag == 4 && groups.include?(stat.gid)) || (tag == 8 && groups.include?(id)) }
|
|
41
|
-
return matched.any? { |_tag, perm, _id| (perm & mask & 1) == 1 } unless matched.empty?
|
|
42
|
-
(rows.find { |tag, _perm, _id| tag == 32 }[1] & 1) == 1
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def validate!(rows)
|
|
48
|
-
valid = rows.all? { |tag, perm, id| [1, 2, 4, 8, 16, 32].include?(tag) && (0..7).cover?(perm) &&
|
|
49
|
-
([2, 8].include?(tag) ? id != 0xffffffff : id == 0xffffffff) }
|
|
50
|
-
valid &&= [1, 4, 32].all? { |tag| rows.count { |row| row[0] == tag } == 1 }
|
|
51
|
-
valid &&= rows.count { |row| row[0] == 16 } <= 1
|
|
52
|
-
valid &&= [2, 8].all? { |tag| ids = rows.select { |row| row[0] == tag }.map(&:last); ids.uniq == ids }
|
|
53
|
-
valid &&= !rows.any? { |row| [2, 8].include?(row[0]) } || rows.count { |row| row[0] == 16 } == 1
|
|
54
|
-
raise Ace::Runtime::RuntimeUnavailableError, "invalid receiver access ACL" unless valid
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "shellwords"
|
|
4
|
-
require_relative "client"
|
|
5
|
-
require_relative "prepared_work"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Assign
|
|
9
|
-
module Authority
|
|
10
|
-
# One authenticated original fetch and one immutable verified work graph.
|
|
11
|
-
# Caller identifiers select; the maintained Client/Endcap authenticates.
|
|
12
|
-
class PreparedInput
|
|
13
|
-
attr_reader :descriptor, :work, :working_directory
|
|
14
|
-
|
|
15
|
-
def self.fetch(client:, assignment_id:, attempt_id:)
|
|
16
|
-
reply = client.call("evidence_fetch", {"assignment_id" => assignment_id, "attempt_id" => attempt_id,
|
|
17
|
-
"kind" => "prepared_work", "purpose_id" => "original_prepared_work", "artifact_id" => "prepared_bundle"},
|
|
18
|
-
download: true, purpose: :candidate, timeout: 30)
|
|
19
|
-
new(reply: reply, candidate_loader: -> {
|
|
20
|
-
client.call("evidence_fetch", {"assignment_id" => assignment_id, "attempt_id" => attempt_id,
|
|
21
|
-
"kind" => "campaign_candidate", "purpose_id" => "original_campaign_candidate", "artifact_id" => "candidate_bundle"},
|
|
22
|
-
download: true, purpose: :candidate, timeout: 30)
|
|
23
|
-
})
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def initialize(reply:, candidate_reply: nil, candidate_loader: nil)
|
|
27
|
-
@descriptor = reply.data.fetch("descriptor")
|
|
28
|
-
Molecules::LifecycleExclusion.workspace_reader(projection: descriptor.fetch("workspace_exclusion"))
|
|
29
|
-
unless descriptor.dig("workspace_exclusion", "root_resource", "view_path") ==
|
|
30
|
-
"/run/ace/lifecycle-exclusion/#{descriptor.fetch('mapping_id')}"
|
|
31
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: original lifecycle mapping differs"
|
|
32
|
-
end
|
|
33
|
-
unless reply.parts.is_a?(Array) && reply.parts.one?
|
|
34
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_unavailable: one original bundle required"
|
|
35
|
-
end
|
|
36
|
-
root = PrivateDirectory.verify!(descriptor.fetch("original_worker_scratch_root"))
|
|
37
|
-
@work = PreparedWork.admit(bytes: reply.parts.first, head: descriptor.fetch("prepared_head"),
|
|
38
|
-
tree: descriptor.fetch("prepared_tree"), sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), root: root)
|
|
39
|
-
expected = work.reference(head: descriptor.fetch("prepared_head"), tree: descriptor.fetch("prepared_tree"))
|
|
40
|
-
definition = work.definition_bytes(head: descriptor.fetch("prepared_head"), tree: descriptor.fetch("prepared_tree"))
|
|
41
|
-
unless expected == descriptor.slice(*expected.keys) &&
|
|
42
|
-
descriptor.values_at("assignment_id", "project_id") == work.manifest.values_at("assignment_id", "project_id") &&
|
|
43
|
-
Digest::SHA256.hexdigest(definition) == descriptor.fetch("definition_digest")
|
|
44
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_mismatch: original graph association"
|
|
45
|
-
end
|
|
46
|
-
execution = work.definition["campaign_execution"]
|
|
47
|
-
if execution
|
|
48
|
-
candidate_reply ||= candidate_loader&.call
|
|
49
|
-
candidate = candidate_reply&.data&.fetch("descriptor")
|
|
50
|
-
unless candidate.is_a?(Hash) && candidate.keys.sort == %w[head tree bytes sha256 candidate_generation campaign_execution mapping_id assignment_id attempt_id project_id original_worker_identity original_worker_scratch_root original_binding_digest].sort &&
|
|
51
|
-
candidate.slice("mapping_id", "assignment_id", "attempt_id", "project_id", "original_worker_identity", "original_worker_scratch_root", "original_binding_digest") ==
|
|
52
|
-
descriptor.slice("mapping_id", "assignment_id", "attempt_id", "project_id", "original_worker_identity", "original_worker_scratch_root", "original_binding_digest") &&
|
|
53
|
-
candidate.fetch("campaign_execution") == execution && candidate.fetch("head") == execution.fetch("head") &&
|
|
54
|
-
candidate.fetch("candidate_generation") == execution.fetch("parent_candidate_generation") &&
|
|
55
|
-
candidate_reply.parts.is_a?(Array) && candidate_reply.parts.one?
|
|
56
|
-
raise AttemptErrors::EvidenceUnavailable, "campaign original candidate input differs"
|
|
57
|
-
end
|
|
58
|
-
materialized = CandidateTransfer.new(root: root).materialize_repository(bytes: candidate_reply.parts.first,
|
|
59
|
-
sha256: candidate.fetch("sha256"), size: candidate.fetch("bytes"), head: candidate.fetch("head"),
|
|
60
|
-
tree: candidate.fetch("tree"), root: root)
|
|
61
|
-
@working_directory = materialized.fetch("directory").freeze
|
|
62
|
-
elsif candidate_reply
|
|
63
|
-
raise AttemptErrors::EvidenceUnavailable, "ordinary prepared input forbids a campaign candidate"
|
|
64
|
-
end
|
|
65
|
-
@descriptor = JSON.parse(JSON.generate(descriptor))
|
|
66
|
-
pending = [@descriptor]
|
|
67
|
-
until pending.empty?
|
|
68
|
-
value = pending.pop
|
|
69
|
-
pending.concat(value.values) if value.is_a?(Hash)
|
|
70
|
-
pending.concat(value) if value.is_a?(Array)
|
|
71
|
-
value.freeze
|
|
72
|
-
end
|
|
73
|
-
freeze
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def render(text)
|
|
77
|
-
values = descriptor.slice("mapping_id", "assignment_id", "scope", "attempt_id")
|
|
78
|
-
rendered = +""
|
|
79
|
-
cursor = 0
|
|
80
|
-
# Only an opening delimiter introduces a token. Closing braces in
|
|
81
|
-
# literal JSON are preserved; inserted selector values are not parsed.
|
|
82
|
-
while (opening = text.index("{{", cursor))
|
|
83
|
-
rendered << text[cursor...opening]
|
|
84
|
-
closing = text.index("}}", opening + 2)
|
|
85
|
-
unless closing
|
|
86
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_invalid: unresolved instruction token"
|
|
87
|
-
end
|
|
88
|
-
token = text[(opening + 2)...closing]
|
|
89
|
-
if token.include?("{") || token.include?("}")
|
|
90
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_invalid: unresolved instruction token"
|
|
91
|
-
end
|
|
92
|
-
field = token.delete_prefix("admitted.")
|
|
93
|
-
unless token == "admitted." + field && values.key?(field)
|
|
94
|
-
raise AttemptErrors::EvidenceUnavailable, "prepared_input_invalid: unknown instruction token"
|
|
95
|
-
end
|
|
96
|
-
rendered << Shellwords.escape(values.fetch(field))
|
|
97
|
-
cursor = closing + 2
|
|
98
|
-
end
|
|
99
|
-
rendered << text[cursor..]
|
|
100
|
-
rendered.freeze
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def drive_prompt
|
|
104
|
-
target = Shellwords.escape(descriptor.fetch("assignment_id") + "@" + descriptor.fetch("scope"))
|
|
105
|
-
selectors = "--assignment #{target} --mapping #{Shellwords.escape(descriptor.fetch('mapping_id'))} --attempt #{Shellwords.escape(descriptor.fetch('attempt_id'))}"
|
|
106
|
-
instructions = ["Drive the authenticated captured assignment subtree. Use ace-assign status #{selectors}, step #{selectors}, start #{selectors}, finish #{selectors} --message REPORT, and fail #{selectors} --message REASON. Do not fork-run, add, retry, renumber, or expand another graph."]
|
|
107
|
-
if (execution = work.definition["campaign_execution"])
|
|
108
|
-
instructions << "This is a registered campaign #{execution.fetch('phase')} child. The provider working directory is the private authenticated parent candidate repository at #{Shellwords.escape(execution.fetch('head'))}, with original base #{Shellwords.escape(execution.fetch('base'))}. Do not edit or advance that candidate. Execute the captured #{Shellwords.escape(execution.fetch('operation'))} phase and retain its actual #{Shellwords.escape(execution.fetch('check_name'))} check and report artifacts. Submit an ordinary campaign-free result (campaign null), operation #{Shellwords.escape(execution.fetch('operation'))}, exact original head/base, and actual check outcome. The existing independent reviewer must validate those same artifact bytes; an approval child must retain the exact canonical independent review actor/verdict. No session metadata or completed queue step counts as a settled R1 campaign round."
|
|
109
|
-
end
|
|
110
|
-
original = "--mapping #{Shellwords.escape(descriptor.fetch('mapping_id'))} --assignment #{Shellwords.escape(descriptor.fetch('assignment_id'))} --attempt #{Shellwords.escape(descriptor.fetch('attempt_id'))}"
|
|
111
|
-
instructions << "Before the original worker exits, explicitly submit its tested candidate and real execution result through the protected authority. Queue completion is not candidate/result acceptance. Read ace-assign authority status #{original}; persist its authority_generation with the complete next command arguments and ordered byte inputs. For each NEW operation use a new explicit mutation and a newly selected generation; for exact retries preserve the original generation, mutation and bytes."
|
|
112
|
-
instructions << "Submit one prepared Git bundle: ace-assign submit-candidate #{original} --head HEAD --candidate-generation CURRENT_CANDIDATE --expected-generation G --mutation CANDIDATE_MUTATION --bundle FILE. Select CURRENT_CANDIDATE from status result_candidate_generation, using 0 when null. Persist the returned candidate_generation and head as REVIEWED_CANDIDATE and HEAD; never derive the next counter by arithmetic or from queue completion."
|
|
113
|
-
instructions << "Have an independent installed reviewer run ace-overseer review --project #{Shellwords.escape(descriptor.fetch('project_id'))} --agent #{Shellwords.escape(descriptor.fetch('mapping_id'))} --assignment #{Shellwords.escape(descriptor.fetch('assignment_id'))} --attempt #{Shellwords.escape(descriptor.fetch('attempt_id'))} --head HEAD --candidate-generation REVIEWED_CANDIDATE --expected-generation G --mutation REVIEW_REQUEST --accept-mutation REVIEW_ACCEPT. Do not impersonate that principal. The review consumer owns actual candidate export, independent checks and canonical acceptance. After review, read authority status again before the first result submission."
|
|
114
|
-
instructions << "Produce the real receipt with Ace::Assign::Models::ExecutionReceipt.new/to_h: original assignment/attempt/project and scope #{Shellwords.escape(descriptor.fetch('scope'))}, exact tested candidate head, installed worker actor/role worker/runtime herdr, actual operation/verdict/executed checks and ordered artifact path/SHA256 pairs. No invented successful check or queue-to-receipt conversion. Submit JSON bytes and matching ordered artifact files with ace-assign submit-result #{original} --head HEAD --candidate-generation REVIEWED_CANDIDATE --expected-generation G --mutation RESULT_MUTATION --receipt FILE --artifact FILE ... . Failed empty artifacts use no --artifact; succeeded requires evidence. Supervisor finish is separate and revalidates result, independent review and no-writer/settlement proof."
|
|
115
|
-
instructions << "For an authorized merge step captured INSIDE this subtree, use the protected delivery rule in wfi://assign/drive. Never execute an outer preset step or expand this graph. Retain the exact approved candidate, structured input bytes, authorization and request ID before the first request; select authority_generation for that new request only."
|
|
116
|
-
service = "--project #{Shellwords.escape(descriptor.fetch('project_id'))} #{original} --scope #{Shellwords.escape(descriptor.fetch('scope'))} --candidate-head HEAD --candidate-generation REVIEWED_CANDIDATE"
|
|
117
|
-
instructions << "Request the uniquely installed authorized merge receiver: ace-lab service request #{service} --service SERVICE --operation merge --authorization AUTHORIZATION --request-id REQUEST --expected-generation G --input INPUT_FILE. Credentials alone are not authorization. Persist the returned original input_digest and target with all request arguments. Lost/uncertain replies never permit another effect or refreshed-generation retry."
|
|
118
|
-
instructions << "Inspect the same request: ace-lab service status #{service} --request REQUEST --input-digest INPUT_DIGEST --target TARGET. Only canonical succeeded evidence permits ace-assign delivery #{original} --scope #{Shellwords.escape(descriptor.fetch('scope'))} --operation merge --service-request REQUEST --candidate-head HEAD --candidate-generation REVIEWED_CANDIDATE --input-digest INPUT_DIGEST --target TARGET. This consumption verifies the imported receipt and never executes merge. Refusal, unavailable or uncertain leaves the active delivery step unfinished; report its exact blocker. Finish the queue step only after succeeded canonical delivery."
|
|
119
|
-
work.manifest.fetch("context").each do |entry|
|
|
120
|
-
instructions << render(work.files.fetch(entry.fetch("text").fetch("filename")))
|
|
121
|
-
end
|
|
122
|
-
work.manifest.fetch("steps").each do |entry|
|
|
123
|
-
instructions << "Step #{entry.fetch('number')}:\n" + render(work.parse_queue_step!(work.files.fetch("steps/" + entry.fetch("filename"))).fetch(:body))
|
|
124
|
-
end
|
|
125
|
-
instructions.join("\n\n").freeze
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|