ace-assign 0.69.0 → 0.71.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/presets/deliver-release.yml +11 -8
- data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
- data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
- data/CHANGELOG.md +27 -0
- data/handbook/skills/as-assign-drive/SKILL.md +5 -2
- data/handbook/workflow-instructions/assign/drive.wf.md +77 -0
- data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
- data/lib/ace/assign/atoms/candidate_limits.rb +20 -0
- data/lib/ace/assign/atoms/git_blob_batch.rb +59 -0
- data/lib/ace/assign/authority/assignment_definition.rb +35 -0
- data/lib/ace/assign/authority/candidate_blob_batch.rb +55 -0
- data/lib/ace/assign/authority/candidate_head_comparison.rb +139 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +57 -27
- data/lib/ace/assign/authority/command_execution.rb +180 -0
- data/lib/ace/assign/authority/endcap.rb +4 -2
- data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +1 -1
- data/lib/ace/assign/authority/endcap_publication.rb +0 -166
- data/lib/ace/assign/authority/endcap_service_context.rb +38 -0
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +176 -0
- data/lib/ace/assign/authority/{endcap_services.rb → fixed_service_lifecycle.rb} +42 -46
- data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
- data/lib/ace/assign/authority/ordinary_client.rb +70 -0
- data/lib/ace/assign/authority/ordinary_completion.rb +104 -0
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +227 -0
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +81 -0
- data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
- data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
- data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +143 -0
- data/lib/ace/assign/authority/ordinary_results.rb +206 -0
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +180 -0
- data/lib/ace/assign/authority/ordinary_reviews.rb +214 -0
- data/lib/ace/assign/authority/ordinary_server.rb +75 -0
- data/lib/ace/assign/authority/ordinary_supervisor.rb +99 -0
- data/lib/ace/assign/authority/prepared_work.rb +9 -4
- data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
- data/lib/ace/assign/authority/release_artifact_manifest.rb +91 -0
- data/lib/ace/assign/authority/retained_input.rb +53 -0
- data/lib/ace/assign/authority/router.rb +11 -6
- data/lib/ace/assign/authority/server.rb +11 -180
- data/lib/ace/assign/authority/socket_server.rb +255 -0
- data/lib/ace/assign/authority/transfer_codec.rb +5 -3
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
- data/lib/ace/assign/cli/commands/submit_candidate.rb +1 -1
- data/lib/ace/assign/molecules/canonical_evidence.rb +25 -8
- data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
- data/lib/ace/assign/molecules/evidence_journal.rb +157 -63
- data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +7 -4
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
- data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
- data/lib/ace/assign/molecules/receipt_verifier.rb +8 -2
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- data/lib/ace/assign/organisms/attempt_coordinator.rb +88 -36
- data/lib/ace/assign/organisms/ordinary_driver.rb +310 -0
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
- data/lib/ace/assign/version.rb +1 -1
- metadata +39 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5330cebc5f70e683451c7a69082c04f7fb6d8a5d92b24b92c73b37d30c56c4a9
|
|
4
|
+
data.tar.gz: d1687b8ba44c21a2a0618e61199c3043a67a8c6f0ecf7be38635635696c57b71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e792c5dae1dda172bfa11a6bf7cc08c074796e70dd5fd67b2ba42294975b71b46ef7fbef858ca7e848675ae83fc4d567dcffb5cf5e5867ea3425b6aba25e161a
|
|
7
|
+
data.tar.gz: 564c47ff609931ae807a8a0f2cd8c1806160272ab9ecc3405a90118e42f184dc93c180b6b7b834844cc3668699f2bcc2fef3632db773ba7da4aa6e29cb0fd912
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Usage: ace-assign create --task TASK[,TASK...] --preset deliver-release
|
|
3
3
|
|
|
4
4
|
name: deliver-release
|
|
5
|
-
description:
|
|
5
|
+
description: Deliver a bound release cut through the project-authorized publisher and installation owners
|
|
6
6
|
|
|
7
7
|
parameters:
|
|
8
8
|
taskrefs:
|
|
@@ -37,15 +37,18 @@ steps:
|
|
|
37
37
|
final artifact digest, and update the manifest before it is accepted. Preparation
|
|
38
38
|
is not publication, installation, deployment, or proof of any of those effects.
|
|
39
39
|
|
|
40
|
-
- name:
|
|
40
|
+
- name: request-authorized-publication
|
|
41
41
|
number: "040"
|
|
42
42
|
instructions: |
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
Resolve the concrete publication workflow and executor from project policy and
|
|
44
|
+
invoke that existing workflow with the exact accepted manifest through the separate
|
|
45
|
+
publication authority. This step grants no credentials or permission by itself.
|
|
46
|
+
Missing publisher identity, executable binding, or admission is BLOCKED(provision, owner).
|
|
47
|
+
Use the project's existing publisher, dependency order and scoped HITL OTP route.
|
|
48
|
+
Record the accepted operation reference and reconciliation path before WAIT.
|
|
49
|
+
Track published, pending and uncertain artifacts separately. Verify registry version
|
|
50
|
+
and digest before retry; never republish an already matching artifact or repeat an
|
|
51
|
+
uncertain effect blindly. Keep delivery open after partial or unknown publication.
|
|
49
52
|
|
|
50
53
|
- name: record-release-outcome
|
|
51
54
|
number: "050"
|
|
@@ -32,11 +32,16 @@ steps:
|
|
|
32
32
|
- name: delegate-renewed-evidence
|
|
33
33
|
number: "030"
|
|
34
34
|
instructions: |
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
Require an initial independent reviewer verdict, then record an accepted-head
|
|
36
|
+
assessment after every candidate movement. Retain old/new head and base SHAs,
|
|
37
|
+
the executed range-diff/content comparison, full repository context, risks and
|
|
38
|
+
fresh relevant tests. Proven identical rebases may carry the initial review;
|
|
39
|
+
patch-id alone is insufficient. An integrator may accept a minor safe correction
|
|
40
|
+
with recorded reasoning and tests. Material or uncertain changes, including small
|
|
41
|
+
authority/security changes, require independent delta review. Keep the initial
|
|
42
|
+
review and formal forge approval actor/state/SHA separate from this assessment.
|
|
43
|
+
Post and read back the evidence before merge; use the exact accepted head in the
|
|
44
|
+
server-side merge guard. Failed publication of evidence blocks integration.
|
|
40
45
|
|
|
41
46
|
- name: reevaluate-integration-readiness
|
|
42
47
|
number: "040"
|
|
@@ -20,8 +20,8 @@ steps:
|
|
|
20
20
|
|
|
21
21
|
These are references to producer tasks; never reparent, duplicate, reopen, or
|
|
22
22
|
change producer task status. A PR-ready worker remains terminal even while this
|
|
23
|
-
delivery assignment stays open.
|
|
24
|
-
be created. Treat an empty observation as current input only: Transiently empty
|
|
23
|
+
delivery assignment stays open. A local objective queue may store goals, next actions, blockers and references;
|
|
24
|
+
no second execution queue, journal, daemon, or cache may be created. Treat an empty observation as current input only: Transiently empty
|
|
25
25
|
is not accepted completion.
|
|
26
26
|
|
|
27
27
|
- name: supervise-delivery-queue
|
|
@@ -42,8 +42,10 @@ steps:
|
|
|
42
42
|
- policy/plan revision and evidence references used for the decision.
|
|
43
43
|
|
|
44
44
|
For every entry, evaluate dependency state, exact candidate head SHA, fresh
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
tests, initial independent review and a documented accepted-head assessment.
|
|
46
|
+
An identical rebase needs executed content-identity proof; a minor safe correction
|
|
47
|
+
needs integrator reasoning and tests; material or uncertain delta needs independent
|
|
48
|
+
delta review. Evaluate required authority, transport and grouping constraints. Re-record the decision after dependency,
|
|
47
49
|
candidate head, evidence, authority, transport, or target-branch movement.
|
|
48
50
|
|
|
49
51
|
- name: reconcile-integration-operation
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.71.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Compare two independently retained candidate bundles through bounded bare Git object reads, including nonancestor rebases, full context inventories, binary patches and historical symlink bytes without executing or materializing candidate code.
|
|
15
|
+
- Ground candidate and assignment-snapshot wire/expansion ceilings in closed 128MiB budgets backed by a shared fixed `cat-file --batch` codec, explicit 20,000-object bounds (per tree inventory and across the comparison's unique-blob union), and chunked batch reads instead of one Git process per blob; historical journal reads reuse the same codec.
|
|
16
|
+
- Bound candidate, prepared and head-comparison tree paths to finite total/component lengths (4096/255 bytes) before any inventory, delta or filesystem prefix work, so a hostile tree cannot push unbounded path bytes into inventories or directory creation.
|
|
17
|
+
- Retain built ordinary release gems as canonical binary evidence bound to the actual native producer, current accepted candidate, independent review and exact release decision. Recheck authorization on replay; preserve report limits and keep publication dispatch separate.
|
|
18
|
+
- Admit ordinary candidates and independently started native reviewers into the canonical journal. Revalidate original review artifacts and root-witnessed test executions before fixed merge admission and effects; preserve strict protected-review attribution.
|
|
19
|
+
- Drive ordinary work through the installed canonical owner with retained requests, accepted context snapshots and result adoption. Record coordinator replacement in the existing definition chain only after positive old-process termination; preserve worker attempts and reject stale supervisor completion.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Preserve exact lost-response result replay while allowing canonical-generation CAS conflicts to recover without replacing original inputs.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Keep protected launch identity construction and comparison in its boundary owner while shared fixed service transitions reuse the same checks.
|
|
28
|
+
- Require ace-review 0.61 for the shared complete-head-context ceiling; raise the candidate transfer deadline from 30s to a measured 60s with byte/count bounds unchanged (two full admits plus the complete comparison of the real 82,809,675-byte candidate legitimately need 23–27s on a loaded host; 30s failed closed under recorded load).
|
|
29
|
+
|
|
30
|
+
## [0.70.0] - 2026-10-16
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Admit ordinary assignments, native starts, progress, submitted results and terminal settlement through the installed canonical owner. Retain original mutations and immutable snapshots across restart, isolate sibling scopes, and join current HITL, service and Inbox evidence.
|
|
35
|
+
- Preserve historical review checks at their recorded head and align delivery presets with accepted-head assessment and serial integration.
|
|
36
|
+
|
|
10
37
|
## [0.69.0] - 2026-10-16
|
|
11
38
|
|
|
12
39
|
### Changed
|
|
@@ -6,12 +6,13 @@ description: Drive agent execution through an active assignment
|
|
|
6
6
|
user-invocable: true
|
|
7
7
|
allowed-tools:
|
|
8
8
|
- Bash(ace-assign:*)
|
|
9
|
+
- Bash(ace-lab:*)
|
|
9
10
|
- Bash(ace-bundle:*)
|
|
10
11
|
- Read
|
|
11
12
|
- Write
|
|
12
13
|
- AskUserQuestion
|
|
13
14
|
- Skill
|
|
14
|
-
argument-hint: "[assignment[@scope]]"
|
|
15
|
+
argument-hint: "[assignment[@scope] | --ordinary <context.json>]"
|
|
15
16
|
last_modified: 2026-04-23
|
|
16
17
|
source: ace-assign
|
|
17
18
|
skill:
|
|
@@ -23,6 +24,8 @@ skill:
|
|
|
23
24
|
|
|
24
25
|
Load and run `ace-bundle wfi://assign/drive` in the current project, then follow the loaded workflow as the source of truth and execute it end-to-end instead of only summarizing it.
|
|
25
26
|
|
|
27
|
+
When `--ordinary` or `ACE_ASSIGN_ORDINARY_CONTEXT` is present, follow the workflow’s ordinary installed branch before resolving local assignment state. Its canonical `ace-lab assignment drive` operations own status, start, progress, results and recovery; local commands below apply only outside that branch.
|
|
28
|
+
|
|
26
29
|
Hard stop rule:
|
|
27
30
|
|
|
28
31
|
- Do not stop after intermediate progress.
|
|
@@ -30,7 +33,7 @@ Hard stop rule:
|
|
|
30
33
|
- Treat `ace-assign status --assignment <id>@<root>` as the source of truth for fork completion; quiet terminal output is not enough reason to stop or declare a stall.
|
|
31
34
|
- If a prior terminal or drive session ended, re-enter from assignment state and continue from the next runnable work instead of depending on the old terminal handle.
|
|
32
35
|
- Before any final response, re-check pinned assignment status. If any runnable `pending` or `active` work remains, continue driving.
|
|
33
|
-
-
|
|
36
|
+
- Preserve unrelated dirty files; commit only task-owned changes.
|
|
34
37
|
- Use progress updates for partial status only.
|
|
35
38
|
- Return a final user-facing completion response only when the assignment is complete or the workflow reaches an explicit blocker/failure stop condition.
|
|
36
39
|
- If pending work remains runnable, continue driving.
|
|
@@ -3,6 +3,7 @@ name: assign-drive
|
|
|
3
3
|
description: Drive an ace-assign assignment until completion or an explicit blocker.
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Bash(ace-assign:*)
|
|
6
|
+
- Bash(ace-lab:*)
|
|
6
7
|
- Bash(ace-bundle:*)
|
|
7
8
|
- Read
|
|
8
9
|
- Write
|
|
@@ -27,6 +28,82 @@ Drive agent execution through an active assignment by continuously checking stat
|
|
|
27
28
|
- An active assignment exists (created via `ace-assign create` or `/as-assign-create`)
|
|
28
29
|
- Assignment has at least one pending or active step
|
|
29
30
|
|
|
31
|
+
## Ordinary installed execution
|
|
32
|
+
|
|
33
|
+
Select this branch **before resolving any local assignment** when invoked with
|
|
34
|
+
`--ordinary <context.json>` or when `ACE_ASSIGN_ORDINARY_CONTEXT` is set. Require
|
|
35
|
+
`ACE_ASSIGN_ORDINARY_PROJECT` and use the exact retained context path throughout.
|
|
36
|
+
Conflicting explicit and environment paths are an error. In this branch every
|
|
37
|
+
execution read and write uses `ace-lab assignment drive --project <project>
|
|
38
|
+
--context <context.json>` against the installed owner. The local assignment
|
|
39
|
+
commands below, `.current`, `.latest`, `fork-run`, and protected `work-on --project`
|
|
40
|
+
are not fallback routes.
|
|
41
|
+
|
|
42
|
+
1. Run `--operation status`. If `not_started`, run `--operation start` from the
|
|
43
|
+
actual native worker pane. A launch/send acknowledgement is not a start receipt.
|
|
44
|
+
Verify canonical status is `running` with the expected assignment, attempt,
|
|
45
|
+
scope, native binding and producer. Never adopt another pane's execution.
|
|
46
|
+
2. Run `--operation bootstrap --output <new-private-directory>`. Read the returned
|
|
47
|
+
accepted task/context/report paths and retain their exact manifest and file
|
|
48
|
+
digests in the work report. These bytes come from canonical prepared work;
|
|
49
|
+
mutable task files in a checkout cannot replace them. Resolve referenced
|
|
50
|
+
project workflows/guides with the selected installed packages and record each
|
|
51
|
+
effective path and digest. A missing or conflicting required asset is a blocker.
|
|
52
|
+
3. Run `--operation next`. Execute the returned current step, or start the returned
|
|
53
|
+
next step using `--operation step-start --step <number> --generation <observed>`.
|
|
54
|
+
The accepted root's children run in this already admitted worker; do not fork
|
|
55
|
+
another execution because a prepared root originated in a batch preset.
|
|
56
|
+
4. After executing a step, write its bounded UTF-8 report and run
|
|
57
|
+
`--operation step-finish --step <number> --report <path> --generation <observed>`.
|
|
58
|
+
On an unrecoverable step failure, use `--operation fail --message <reason>
|
|
59
|
+
--generation <observed>`. Read `next` again after every acknowledged mutation.
|
|
60
|
+
Do not mark an unexecuted step successful. Nested skills may perform the work,
|
|
61
|
+
but their local Assign bookkeeping must not replace these canonical operations.
|
|
62
|
+
5. When the scope is complete or failed, prepare an execution receipt and exact
|
|
63
|
+
artifact files using the receipt contract in the Lab usage documentation.
|
|
64
|
+
Its producer is the canonical start producer; HEAD is the actual committed
|
|
65
|
+
candidate; checks describe executed checks. Submit with `--operation result
|
|
66
|
+
--generation <observed> --receipt <json> --artifact <file>` in receipt order.
|
|
67
|
+
Submission is not terminal acceptance, merge, publication or installation.
|
|
68
|
+
6. Keep the worker's native process and checkout available until the supervisor
|
|
69
|
+
accepts the result through `--operation adopt` and canonical `status` becomes
|
|
70
|
+
terminal. The filesystem watch emits only a hint. Never infer completion from
|
|
71
|
+
a quiet pane, a hint file, an old message or a send acknowledgement.
|
|
72
|
+
|
|
73
|
+
Each mutation prints its retained original request path. After any lost reply,
|
|
74
|
+
read canonical `status`/`next` first and use `--operation replay --request <path>`
|
|
75
|
+
only for that original input. Preserve assignment, attempt, mutation, candidate
|
|
76
|
+
and pane identities. Never edit an original request's generation or bytes. For a
|
|
77
|
+
result/finish CAS refusal, reread canonical status. An accepted result replays its
|
|
78
|
+
exact original input; an accepted terminal outcome needs no new finish. If no
|
|
79
|
+
result/terminal was accepted and the attempt generation advanced, the original
|
|
80
|
+
CAS can no longer accept: result/finish may retain a separate request at the new
|
|
81
|
+
observed generation. The driver preserves all old inputs and performs these
|
|
82
|
+
checks; unavailable status remains a blocker. Changed content, project or owner
|
|
83
|
+
is not a generation retry, and never authorizes another worker.
|
|
84
|
+
|
|
85
|
+
A supervisor installs the `watch_command` returned by `ace-overseer ordinary`
|
|
86
|
+
exactly once in its native session, runs a harmless turn to persist it, and uses
|
|
87
|
+
exact-session resume thereafter. Do not recreate an intentionally removed watch.
|
|
88
|
+
A watch merely requests canonical inspection; recurring native progress audits
|
|
89
|
+
also find accepted results when a wake or reply is lost. Wrong project/attempt
|
|
90
|
+
hints carry no authority. Existing inbound Inbox settlement remains a separate
|
|
91
|
+
owner configuration: without it, nonempty Inbox references refuse completion.
|
|
92
|
+
|
|
93
|
+
After replacement of the coordinator process, read `--operation supervisor`.
|
|
94
|
+
A new coordinator uses `--operation adopt-supervisor --pane <actual-pane>
|
|
95
|
+
--generation <old-selection-generation> --previous-binding-sha256 <old-digest>`.
|
|
96
|
+
The installed owner authenticates its actual placement and positively verifies
|
|
97
|
+
that the original process generation ended. Live or unobservable old generations
|
|
98
|
+
refuse adoption. Replay the original adoption request after a lost reply. This
|
|
99
|
+
changes only the canonical supervisor selection; existing workers, uncertain
|
|
100
|
+
launches, attempts, pending results and mutation IDs remain intact. Then resume
|
|
101
|
+
canonical inspection and result adoption. Never re-register or relaunch workers
|
|
102
|
+
because the coordinator restarted.
|
|
103
|
+
|
|
104
|
+
**End of ordinary branch.** The remaining sections apply only to local assignment
|
|
105
|
+
execution; do not enter them from an ordinary installed context.
|
|
106
|
+
|
|
30
107
|
## Assignment Context Propagation
|
|
31
108
|
|
|
32
109
|
When working with multiple concurrent assignments, the active assignment is resolved in this order:
|
|
@@ -47,6 +47,11 @@ module Ace
|
|
|
47
47
|
rescue Ace::Assign::AttemptErrors::InvalidScope
|
|
48
48
|
false
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
# An owner of a subtree also owns every descendant scope.
|
|
52
|
+
def self.overlap?(left, right)
|
|
53
|
+
equal?(left, right) || left.start_with?("#{right}.") || right.start_with?("#{left}.")
|
|
54
|
+
end
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Atoms
|
|
6
|
+
# Closed candidate-purpose budgets. Reports and release gems retain their
|
|
7
|
+
# separate bounds; callers cannot select a larger wire/storage ceiling.
|
|
8
|
+
module CandidateLimits
|
|
9
|
+
BYTES = 128 * 1024 * 1024
|
|
10
|
+
PREPARED_BYTES = 64 * 1024 * 1024
|
|
11
|
+
REVIEW_SNAPSHOT_BYTES = 64 * 1024 * 1024
|
|
12
|
+
# Explicit finite path shape (PATH_MAX/NAME_MAX-sized) so a hostile
|
|
13
|
+
# tree entry cannot push unbounded path bytes into inventories,
|
|
14
|
+
# filesystem prefix creation or per-component scans.
|
|
15
|
+
PATH_BYTES = 4096
|
|
16
|
+
PATH_COMPONENT_BYTES = 255
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Atoms
|
|
8
|
+
# Git's fixed cat-file batch framing. Owners select process, immutable
|
|
9
|
+
# repository, deadline and byte/count limits before invoking this codec.
|
|
10
|
+
module GitBlobBatch
|
|
11
|
+
OID = /\A[0-9a-f]{40}\z/
|
|
12
|
+
class Invalid < StandardError
|
|
13
|
+
attr_reader :reason
|
|
14
|
+
def initialize(reason)
|
|
15
|
+
@reason = reason
|
|
16
|
+
super("invalid Git blob batch #{reason}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.sizes(oids, output)
|
|
21
|
+
unless oids.is_a?(Array) && oids.all? { |oid| oid.is_a?(String) && OID.match?(oid) } && output.is_a?(String)
|
|
22
|
+
raise Invalid, :selector
|
|
23
|
+
end
|
|
24
|
+
lines = output.b.lines
|
|
25
|
+
raise Invalid, :count unless lines.size == oids.size
|
|
26
|
+
lines.zip(oids).map do |line, oid|
|
|
27
|
+
match = /\A([0-9a-f]{40}) blob (0|[1-9][0-9]*)\n\z/.match(line)
|
|
28
|
+
raise Invalid, :metadata unless match && match[1] == oid
|
|
29
|
+
match[2].to_i
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.decode(entries, output)
|
|
34
|
+
unless entries.is_a?(Array) && entries.all? { |entry|
|
|
35
|
+
entry.is_a?(Array) && entry.size == 2 && entry[0].is_a?(String) && OID.match?(entry[0]) &&
|
|
36
|
+
entry[1].is_a?(Integer) && entry[1] >= 0 } && output.is_a?(String)
|
|
37
|
+
raise Invalid, :selector
|
|
38
|
+
end
|
|
39
|
+
output = output.b
|
|
40
|
+
offset = 0
|
|
41
|
+
entries.map do |oid, size|
|
|
42
|
+
ending = output.index("\n", offset)
|
|
43
|
+
unless ending && ending - offset <= 127 && output.byteslice(offset, ending - offset) == "#{oid} blob #{size}"
|
|
44
|
+
raise Invalid, :header
|
|
45
|
+
end
|
|
46
|
+
offset = ending + 1
|
|
47
|
+
content = output.byteslice(offset, size)
|
|
48
|
+
unless content&.bytesize == size && output.byteslice(offset + size, 1) == "\n" &&
|
|
49
|
+
Digest::SHA1.new.update("blob #{size}\0").update(content).hexdigest == oid
|
|
50
|
+
raise Invalid, :bytes
|
|
51
|
+
end
|
|
52
|
+
offset += size + 1
|
|
53
|
+
content
|
|
54
|
+
end.tap { raise Invalid, :trailing unless offset == output.bytesize }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "campaign_execution"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Assign
|
|
9
|
+
module Authority
|
|
10
|
+
# Shared byte/schema validation; each lifecycle separately authenticates
|
|
11
|
+
# the registration's owner, campaign provenance and original selection.
|
|
12
|
+
module AssignmentDefinition
|
|
13
|
+
def self.parse(bytes:, digest:, assignment_id:, project_id:)
|
|
14
|
+
unless bytes.is_a?(String) && bytes.bytesize <= 32_768 && digest.is_a?(String) &&
|
|
15
|
+
Digest::SHA256.hexdigest(bytes) == digest
|
|
16
|
+
raise ArgumentError, "invalid assignment definition digest or size"
|
|
17
|
+
end
|
|
18
|
+
value = JSON.parse(bytes)
|
|
19
|
+
allowed = %w[session_id name description created_at updated_at source_config parent task_id project_id prepared_work review_campaign campaign_execution]
|
|
20
|
+
unless value.is_a?(Hash) && (value.keys - allowed).empty? &&
|
|
21
|
+
%w[session_id name created_at source_config task_id project_id].all? { |key| value[key].is_a?(String) && !value[key].empty? } &&
|
|
22
|
+
value["session_id"] == assignment_id && value["project_id"] == project_id
|
|
23
|
+
raise ArgumentError, "invalid managed assignment definition"
|
|
24
|
+
end
|
|
25
|
+
CampaignExecution.validate_parent!(value.fetch("review_campaign")) if value.key?("review_campaign")
|
|
26
|
+
assignment = Models::Assignment.from_h(value)
|
|
27
|
+
unless assignment.managed? && assignment.task_id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/)
|
|
28
|
+
raise ArgumentError, "definition is not managed or task identity is invalid"
|
|
29
|
+
end
|
|
30
|
+
assignment
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../atoms/git_blob_batch"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Authority
|
|
8
|
+
# Same cat-file batch dialect/codec as the canonical journal. Chunked
|
|
9
|
+
# bodies avoid retaining the entire expanded tree a second time.
|
|
10
|
+
module CandidateBlobBatch
|
|
11
|
+
BATCH_BYTES = 8 * 1024 * 1024
|
|
12
|
+
BATCH_OBJECTS = 512
|
|
13
|
+
MAX_OBJECTS = 20_000
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def candidate_blob_sizes(repository, deadline, oids)
|
|
18
|
+
unless oids.is_a?(Array) && oids.size <= MAX_OBJECTS &&
|
|
19
|
+
oids.all? { |oid| oid.is_a?(String) && self.class::SHA.match?(oid) }
|
|
20
|
+
reject!("Candidate blob inventory is invalid or oversized")
|
|
21
|
+
end
|
|
22
|
+
oids.each_slice(64).flat_map do |slice|
|
|
23
|
+
raw = git(repository, deadline, "cat-file", "--batch-check", stdin_data: slice.join("\n") + "\n", limit: slice.size * 128)
|
|
24
|
+
Atoms::GitBlobBatch.sizes(slice, raw)
|
|
25
|
+
end
|
|
26
|
+
rescue Atoms::GitBlobBatch::Invalid => error
|
|
27
|
+
reject!("Candidate blob batch #{error.reason} differs")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each_candidate_blob(repository, deadline, entries)
|
|
31
|
+
batch = []
|
|
32
|
+
bytes = 0
|
|
33
|
+
entries.each do |entry|
|
|
34
|
+
if !batch.empty? && (batch.size >= BATCH_OBJECTS || bytes + entry.last > BATCH_BYTES)
|
|
35
|
+
yield_candidate_blob_batch(repository, deadline, batch) { |oid, content| yield oid, content }
|
|
36
|
+
batch = []
|
|
37
|
+
bytes = 0
|
|
38
|
+
end
|
|
39
|
+
batch << entry
|
|
40
|
+
bytes += entry.last
|
|
41
|
+
end
|
|
42
|
+
yield_candidate_blob_batch(repository, deadline, batch) { |oid, content| yield oid, content } unless batch.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def yield_candidate_blob_batch(repository, deadline, entries)
|
|
46
|
+
limit = entries.sum { |_oid, size| size + 128 }
|
|
47
|
+
raw = git(repository, deadline, "cat-file", "--batch", stdin_data: entries.map { |oid, _size| "#{oid}\n" }.join, limit: limit)
|
|
48
|
+
Atoms::GitBlobBatch.decode(entries, raw).zip(entries).each { |content, (oid, _size)| yield oid, content }
|
|
49
|
+
rescue Atoms::GitBlobBatch::Invalid => error
|
|
50
|
+
reject!("Candidate blob batch #{error.reason} differs")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
# Compare retained Git objects in a private bare repository. Historical
|
|
7
|
+
# links are object bytes, never paths to materialize or follow. This view
|
|
8
|
+
# supplies evidence; it does not classify a change or grant acceptance.
|
|
9
|
+
# Host: CandidateTransfer supplies bounded Git/admission, byte encoding,
|
|
10
|
+
# private-root storage, monotonic deadlines and rejection helpers.
|
|
11
|
+
module CandidateHeadComparison
|
|
12
|
+
class ReadView
|
|
13
|
+
def initialize(owner:, repository:, reviewed:, candidate:, deadline:)
|
|
14
|
+
@owner, @repository, @reviewed, @candidate, @deadline = owner, repository, reviewed, candidate, deadline
|
|
15
|
+
@thread, @active = Thread.current, true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def close = @active = false
|
|
19
|
+
|
|
20
|
+
def context
|
|
21
|
+
unless @active && Thread.current.equal?(@thread)
|
|
22
|
+
raise AttemptErrors::EvidenceUnavailable, "head comparison escaped its source lifetime"
|
|
23
|
+
end
|
|
24
|
+
@owner.send(:head_comparison_context, @repository, @deadline, @reviewed, @candidate)
|
|
25
|
+
rescue Timeout::Error
|
|
26
|
+
raise AttemptErrors::EvidenceUnavailable, "head comparison deadline expired"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def with_head_comparison(before:, after:, before_base:, after_base:)
|
|
31
|
+
raise ArgumentError, "head comparison requires a block" unless block_given?
|
|
32
|
+
[before, after].each do |input|
|
|
33
|
+
unless input.is_a?(Hash) && input.keys.sort == %i[bytes head sha256 size tree] &&
|
|
34
|
+
input[:tree].is_a?(String) && input[:tree].match?(self.class::SHA)
|
|
35
|
+
reject!("Head comparison candidate binding is invalid")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
[before_base, after_base].each do |base|
|
|
39
|
+
reject!("Head comparison base is invalid") unless base.is_a?(String) && base.match?(self.class::SHA)
|
|
40
|
+
end
|
|
41
|
+
deadline = monotonic + self.class::DEADLINE
|
|
42
|
+
# Each complete bundle is independently verified against an empty
|
|
43
|
+
# repository before either can contribute objects to the comparison.
|
|
44
|
+
admit(**before.reject { |key, _| key == :tree }, deadline: deadline) do |_old_repo, _old_deadline, reviewed|
|
|
45
|
+
reject!("Reviewed tree differs from accepted tree") unless reviewed.fetch("tree") == before[:tree]
|
|
46
|
+
admit(**after.reject { |key, _| key == :tree }, deadline: deadline) do |repository, _new_deadline, candidate|
|
|
47
|
+
reject!("Candidate tree differs from accepted tree") unless candidate.fetch("tree") == after[:tree]
|
|
48
|
+
Tempfile.create(["reviewed-", ".bundle"], @root) do |file|
|
|
49
|
+
file.binmode
|
|
50
|
+
file.write(reviewed.fetch("bundle")); file.flush
|
|
51
|
+
git(repository, deadline, "fetch", "--no-tags", file.path, "#{before[:head]}:refs/heads/reviewed")
|
|
52
|
+
reader = ReadView.new(owner: self, repository: repository,
|
|
53
|
+
reviewed: {"head" => before[:head], "base" => before_base, "tree" => before[:tree]},
|
|
54
|
+
candidate: {"head" => after[:head], "base" => after_base, "tree" => after[:tree]}, deadline: deadline)
|
|
55
|
+
yield reader
|
|
56
|
+
ensure
|
|
57
|
+
reader&.close
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def head_comparison_context(repository, deadline, reviewed, candidate)
|
|
66
|
+
context = {"schema" => "ace.candidate-head-comparison/v1", "reviewed" => reviewed,
|
|
67
|
+
"candidate" => candidate, "inventories" => {}, "objects" => {}, "changes" => {}}
|
|
68
|
+
remaining = self.class::MAX_REVIEW_BYTES
|
|
69
|
+
{"reviewed_head" => reviewed["head"], "reviewed_base" => reviewed["base"],
|
|
70
|
+
"candidate_head" => candidate["head"], "candidate_base" => candidate["base"]}.each do |label, commit|
|
|
71
|
+
git(repository, deadline, "cat-file", "-e", "#{commit}^{commit}")
|
|
72
|
+
tree = git(repository, deadline, "rev-parse", "#{commit}^{tree}").strip
|
|
73
|
+
inventory = head_comparison_inventory(repository, deadline, tree)
|
|
74
|
+
remaining -= JSON.generate(inventory).bytesize
|
|
75
|
+
reject!("Complete head comparison context is oversized") if remaining.negative?
|
|
76
|
+
context.fetch("inventories")[label] = {"tree" => tree, "entries" => inventory}
|
|
77
|
+
end
|
|
78
|
+
oids = context.fetch("inventories").values.flat_map { |inventory| inventory.fetch("entries").map { |entry| entry.fetch("oid") } }.uniq
|
|
79
|
+
sizes = oids.zip(candidate_blob_sizes(repository, deadline, oids)).to_h
|
|
80
|
+
reject!("Complete head comparison object content is oversized") if sizes.values.sum > remaining
|
|
81
|
+
each_candidate_blob(repository, deadline, sizes.to_a) do |oid, content|
|
|
82
|
+
object = {"size" => content.bytesize, "sha256" => Digest::SHA256.hexdigest(content), "content" => review_bytes(content)}
|
|
83
|
+
remaining -= JSON.generate(object).bytesize
|
|
84
|
+
reject!("Complete head comparison context is oversized") if remaining.negative?
|
|
85
|
+
context.fetch("objects")[oid] = object
|
|
86
|
+
end
|
|
87
|
+
{"candidate_delta" => [reviewed["head"], candidate["head"]],
|
|
88
|
+
"base_delta" => [reviewed["base"], candidate["base"]],
|
|
89
|
+
"original_scope" => [reviewed["base"], reviewed["head"]],
|
|
90
|
+
"current_scope" => [candidate["base"], candidate["head"]]}.each do |label, revisions|
|
|
91
|
+
delta = head_comparison_delta(repository, deadline, revisions, remaining)
|
|
92
|
+
remaining -= JSON.generate(delta).bytesize
|
|
93
|
+
reject!("Complete head comparison context is oversized") if remaining.negative?
|
|
94
|
+
context.fetch("changes")[label] = delta
|
|
95
|
+
end
|
|
96
|
+
# Include structural JSON overhead in the final bound as well as all
|
|
97
|
+
# original bytes. No partial inventory or truncated patch is returned.
|
|
98
|
+
reject!("Complete head comparison context is oversized") if JSON.generate(context).bytesize > self.class::MAX_REVIEW_BYTES
|
|
99
|
+
context
|
|
100
|
+
rescue AttemptErrors::ReceiptRejected => error
|
|
101
|
+
reject!("Head comparison read failed: #{error.message}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def head_comparison_inventory(repository, deadline, tree)
|
|
105
|
+
entries = git(repository, deadline, "ls-tree", "-r", "-z", tree, limit: self.class::MAX_BYTES).b.split("\0")
|
|
106
|
+
reject!("Complete head comparison inventory is oversized") if entries.size > self.class::MAX_OBJECTS
|
|
107
|
+
entries.map do |entry|
|
|
108
|
+
metadata, path = entry.split("\t", 2)
|
|
109
|
+
mode, kind, oid = metadata.to_s.split(" ")
|
|
110
|
+
# Inventory entries carry whole paths only; bound their bytes
|
|
111
|
+
# before JSON encoding or delta cross-checks consume them.
|
|
112
|
+
reject!("Complete head comparison path exceeds bounded length") unless path && !path.empty? &&
|
|
113
|
+
path.bytesize <= Atoms::CandidateLimits::PATH_BYTES
|
|
114
|
+
unless kind == "blob" && %w[100644 100755 120000].include?(mode) && oid&.match?(self.class::SHA)
|
|
115
|
+
reject!("Complete head comparison contains an unsupported Git entry")
|
|
116
|
+
end
|
|
117
|
+
{"path" => review_bytes(path), "mode" => mode, "oid" => oid}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def head_comparison_delta(repository, deadline, revisions, remaining)
|
|
122
|
+
options = ["--no-ext-diff", "--no-textconv", "--no-renames", "--ignore-submodules=none"]
|
|
123
|
+
patch = git(repository, deadline, "diff", *options, "--binary", "--full-index", "--src-prefix=a/", "--dst-prefix=b/",
|
|
124
|
+
*revisions, "--", limit: [remaining, 1].max).b
|
|
125
|
+
paths = git(repository, deadline, "diff", *options, "--name-only", "-z", *revisions, "--", limit: [remaining, 1].max).b.split("\0")
|
|
126
|
+
stats = git(repository, deadline, "diff", *options, "--numstat", "-z", *revisions, "--", limit: [remaining, 1].max).b.split("\0")
|
|
127
|
+
binary = stats.filter_map do |entry|
|
|
128
|
+
added, removed, path = entry.split("\t", 3)
|
|
129
|
+
reject!("Complete head comparison diff inventory is invalid") unless path && paths.include?(path)
|
|
130
|
+
path if added == "-" || removed == "-"
|
|
131
|
+
end
|
|
132
|
+
{"from" => revisions.first, "to" => revisions.last, "patch" => review_bytes(patch),
|
|
133
|
+
"patch_sha256" => Digest::SHA256.hexdigest(patch), "paths" => paths.map { |path| review_bytes(path) },
|
|
134
|
+
"binary_paths" => binary.map { |path| review_bytes(path) }}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|