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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
  3. data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
  4. data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
  5. data/CHANGELOG.md +27 -0
  6. data/handbook/skills/as-assign-drive/SKILL.md +5 -2
  7. data/handbook/workflow-instructions/assign/drive.wf.md +77 -0
  8. data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
  9. data/lib/ace/assign/atoms/candidate_limits.rb +20 -0
  10. data/lib/ace/assign/atoms/git_blob_batch.rb +59 -0
  11. data/lib/ace/assign/authority/assignment_definition.rb +35 -0
  12. data/lib/ace/assign/authority/candidate_blob_batch.rb +55 -0
  13. data/lib/ace/assign/authority/candidate_head_comparison.rb +139 -0
  14. data/lib/ace/assign/authority/candidate_transfer.rb +57 -27
  15. data/lib/ace/assign/authority/command_execution.rb +180 -0
  16. data/lib/ace/assign/authority/endcap.rb +4 -2
  17. data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
  18. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +1 -1
  19. data/lib/ace/assign/authority/endcap_publication.rb +0 -166
  20. data/lib/ace/assign/authority/endcap_service_context.rb +38 -0
  21. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +176 -0
  22. data/lib/ace/assign/authority/{endcap_services.rb → fixed_service_lifecycle.rb} +42 -46
  23. data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
  24. data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
  25. data/lib/ace/assign/authority/ordinary_client.rb +70 -0
  26. data/lib/ace/assign/authority/ordinary_completion.rb +104 -0
  27. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
  28. data/lib/ace/assign/authority/ordinary_lifecycle.rb +227 -0
  29. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +81 -0
  30. data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
  31. data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
  32. data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
  33. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +143 -0
  34. data/lib/ace/assign/authority/ordinary_results.rb +206 -0
  35. data/lib/ace/assign/authority/ordinary_review_evidence.rb +180 -0
  36. data/lib/ace/assign/authority/ordinary_reviews.rb +214 -0
  37. data/lib/ace/assign/authority/ordinary_server.rb +75 -0
  38. data/lib/ace/assign/authority/ordinary_supervisor.rb +99 -0
  39. data/lib/ace/assign/authority/prepared_work.rb +9 -4
  40. data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
  41. data/lib/ace/assign/authority/release_artifact_manifest.rb +91 -0
  42. data/lib/ace/assign/authority/retained_input.rb +53 -0
  43. data/lib/ace/assign/authority/router.rb +11 -6
  44. data/lib/ace/assign/authority/server.rb +11 -180
  45. data/lib/ace/assign/authority/socket_server.rb +255 -0
  46. data/lib/ace/assign/authority/transfer_codec.rb +5 -3
  47. data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
  48. data/lib/ace/assign/cli/commands/submit_candidate.rb +1 -1
  49. data/lib/ace/assign/molecules/canonical_evidence.rb +25 -8
  50. data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
  51. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
  52. data/lib/ace/assign/molecules/evidence_journal.rb +157 -63
  53. data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
  54. data/lib/ace/assign/molecules/journal_mutation.rb +7 -4
  55. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
  56. data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
  57. data/lib/ace/assign/molecules/receipt_verifier.rb +8 -2
  58. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  59. data/lib/ace/assign/organisms/attempt_coordinator.rb +88 -36
  60. data/lib/ace/assign/organisms/ordinary_driver.rb +310 -0
  61. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
  62. data/lib/ace/assign/version.rb +1 -1
  63. metadata +39 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 407eb167c22a0bac683f3ac5db391b535f6200f420a38d458cc13bf36590d930
4
- data.tar.gz: ef23045c1b5c2915768a21110675121dd390a896347dba0f6be90f134eb5fe12
3
+ metadata.gz: 5330cebc5f70e683451c7a69082c04f7fb6d8a5d92b24b92c73b37d30c56c4a9
4
+ data.tar.gz: d1687b8ba44c21a2a0618e61199c3043a67a8c6f0ecf7be38635635696c57b71
5
5
  SHA512:
6
- metadata.gz: 217a25d646c1499d23196d0d43e1eed171914b01fa91532cf2d996f728eb1ef2b60a7126e3cc46aa8b58d0e7015c844637ee67e18c8fb3cfb36b900c81ee4342
7
- data.tar.gz: 4de219ae9bdf137b84d813c599643a459961fd0337b7de987b69ac6edd8cbc4c483bd63def33a30f7bbbb075df66030843dbe71cab847f863edeb2f483ad2c26
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: Prepare and bind a stable release cut without exercising publication authority
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: publication-authority-placeholder
40
+ - name: request-authorized-publication
41
41
  number: "040"
42
42
  instructions: |
43
- Publication placeholder only. Do not run a publish command in this preset. Route
44
- the exact manifest to the separate publication authority named by project policy.
45
- Missing credentials, publisher identity, or admission is
46
- BLOCKED(provision, owner). If that authority accepts a request and returns a
47
- durable operation reference plus reconciliation path, record
48
- WAIT(reference, next-check); otherwise preserve uncertainty and do not retry.
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
- After any candidate movement, delegate renewed evidence for the new exact head:
36
- prove replay content identity with range-diff or a stronger project-approved
37
- comparison, run fresh relevant tests, and obtain an independent reviewer verdict.
38
- The reviewer must be independent of the executor. Content beyond the declared
39
- replay delta requires the full project review gate, not a verdict-stands shortcut.
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. No second queue, journal, daemon, or cache may
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
- test and independent-review evidence bound to that head, required authority and
46
- transport, and grouping constraints. Re-record the decision after dependency,
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
- - If unrelated dirty files are generated side effects outside task scope, clean/reset them instead of auto-committing them.
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