devrites 7.3.0 → 7.3.2
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.
- package/CHANGELOG.md +32 -0
- package/README.md +19 -3
- package/bin/devrites.mjs +31 -5
- package/docs/engine/commands.md +41 -3
- package/docs/release.md +5 -2
- package/engine/Makefile +2 -2
- package/engine/internal/acceptance/newfinal_origins.go +39 -0
- package/engine/internal/acceptance/preconditions.go +17 -11
- package/engine/internal/acceptance/transcript.go +23 -4
- package/engine/internal/gate/gate.go +8 -6
- package/engine/internal/gate/readiness_binding.go +54 -24
- package/engine/internal/install/apply.go +36 -21
- package/engine/internal/install/binary.go +67 -9
- package/engine/internal/install/install.go +63 -21
- package/engine/internal/install/manifest.go +4 -5
- package/engine/internal/install/merge.go +89 -33
- package/engine/internal/install/preflight.go +60 -19
- package/engine/internal/install/uninstall.go +19 -9
- package/engine/internal/install/update.go +0 -4
- package/engine/internal/lib/claims.go +170 -4
- package/engine/internal/lib/claims_holder.go +13 -16
- package/engine/internal/lib/closeout.go +52 -28
- package/engine/internal/lib/context_io.go +88 -4
- package/engine/internal/lib/continuation.go +111 -24
- package/engine/internal/lib/dispatch.go +157 -24
- package/engine/internal/lib/final_prerequisites.go +137 -0
- package/engine/internal/lib/manifest_union.go +62 -25
- package/engine/internal/lib/next.go +10 -10
- package/engine/internal/lib/packet.go +32 -8
- package/engine/internal/lib/packet_integrity.go +194 -2
- package/engine/internal/lib/resolve.go +128 -142
- package/engine/internal/lib/treebaseline.go +0 -1
- package/engine/internal/markdowntext/markdowntext.go +110 -31
- package/engine/internal/overhaul/records/check.go +45 -5
- package/engine/internal/overhaul/records/consistency.go +34 -8
- package/engine/internal/overhaul/records/run.go +55 -11
- package/engine/internal/parallel/landing.go +17 -0
- package/engine/internal/state/acceptance.go +35 -5
- package/engine/internal/state/feature.go +3 -2
- package/engine/internal/state/observation.go +50 -3
- package/engine/internal/state/write.go +20 -44
- package/engine/internal/workflow/commands.go +25 -3
- package/engine/main.go +19 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/.claude/skills/overhaul/SKILL.md +8 -4
- package/pack/.claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/.claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/.claude/skills/overhaul/references/records.md +23 -8
- package/pack/.claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/.claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/.claude/skills/rite-prove/SKILL.md +15 -7
- package/pack/.claude/skills/rite-review/SKILL.md +7 -0
- package/pack/.claude/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/claude/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/claude/skills/overhaul/references/records.md +23 -8
- package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/claude/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/claude/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/claude/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/codex/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/codex/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/codex/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/codex/skills/overhaul/references/records.md +23 -8
- package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/codex/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/codex/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/codex/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/codex/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/devin/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/devin/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/devin/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/devin/skills/overhaul/references/records.md +23 -8
- package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/devin/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/devin/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/devin/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/devin/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/dsh/runtime/cordis.patch.yml +28 -14
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/dsh/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/dsh/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/dsh/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/dsh/skills/overhaul/references/records.md +23 -8
- package/pack/generated/dsh/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/dsh/skills/overhaul/references/scoring.md +6 -0
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/dsh/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/dsh/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/dsh/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/omp/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/omp/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/omp/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/omp/skills/overhaul/references/records.md +23 -8
- package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/omp/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/omp/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/omp/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/omp/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/pi/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/pi/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/pi/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/pi/skills/overhaul/references/records.md +23 -8
- package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/pi/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/pi/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/pi/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/pi/skills/rite-vet/SKILL.md +10 -1
- package/package.json +1 -1
- package/scripts/dsh-generate.py +1 -1
- package/scripts/install-lib.sh +29 -0
- package/uninstall.sh +1 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
package gate
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"bytes"
|
|
4
5
|
"crypto/sha256"
|
|
5
6
|
"encoding/binary"
|
|
6
7
|
"encoding/hex"
|
|
7
8
|
"errors"
|
|
8
9
|
"fmt"
|
|
9
|
-
"
|
|
10
|
+
"io"
|
|
10
11
|
"regexp"
|
|
12
|
+
"slices"
|
|
11
13
|
"strings"
|
|
12
14
|
|
|
15
|
+
"github.com/devrites/devrites/internal/acceptance"
|
|
13
16
|
"github.com/devrites/devrites/internal/markdowntext"
|
|
14
17
|
"github.com/devrites/devrites/internal/state"
|
|
15
18
|
)
|
|
@@ -17,7 +20,7 @@ import (
|
|
|
17
20
|
const (
|
|
18
21
|
readinessBindingLabel = "Readiness inputs SHA-256: "
|
|
19
22
|
readinessBindingDomain = "devrites-readiness-inputs"
|
|
20
|
-
readinessBindingVersion = "
|
|
23
|
+
readinessBindingVersion = "2"
|
|
21
24
|
)
|
|
22
25
|
|
|
23
26
|
type readinessInput struct {
|
|
@@ -37,11 +40,15 @@ var readinessInputs = [...]readinessInput{
|
|
|
37
40
|
{logical: "design-brief.md"},
|
|
38
41
|
{logical: "ai-spec.md"},
|
|
39
42
|
{logical: ".devrites/principles.md"},
|
|
43
|
+
{logical: acceptance.GatesFile},
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// ReadinessBinding returns the exact line Vet records in eng-review.md.
|
|
47
|
+
// Explicit sources resolve live first, then safely archived planning bytes.
|
|
48
|
+
// Gate definitions are bound independently of proof progress. Older bindings
|
|
49
|
+
// omitted those definitions and require normal re-Vet, never automatic refresh.
|
|
43
50
|
func ReadinessBinding(root, slug string) (string, error) {
|
|
44
|
-
observation, err := state.
|
|
51
|
+
observation, err := state.ObserveReferencedWorkspace(root, slug)
|
|
45
52
|
if err != nil {
|
|
46
53
|
return "", err
|
|
47
54
|
}
|
|
@@ -49,10 +56,6 @@ func ReadinessBinding(root, slug string) (string, error) {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
func readinessBindingFromObservation(observation *state.WorkspaceObservation) (string, error) {
|
|
52
|
-
return bindReadinessInputs(observation, true)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func bindReadinessInputs(observation *state.WorkspaceObservation, blankProgress bool) (string, error) {
|
|
56
59
|
digest := sha256.New()
|
|
57
60
|
writeReadinessField(digest, readinessBindingDomain)
|
|
58
61
|
writeReadinessField(digest, readinessBindingVersion)
|
|
@@ -62,7 +65,7 @@ func bindReadinessInputs(observation *state.WorkspaceObservation, blankProgress
|
|
|
62
65
|
if err != nil {
|
|
63
66
|
return "", err
|
|
64
67
|
}
|
|
65
|
-
if
|
|
68
|
+
if input.logical == "traceability.md" {
|
|
66
69
|
content = withoutProgressCells(content)
|
|
67
70
|
}
|
|
68
71
|
writeReadinessField(digest, string(input.logical))
|
|
@@ -84,12 +87,26 @@ func retainedReadinessInput(observation *state.WorkspaceObservation, input readi
|
|
|
84
87
|
}
|
|
85
88
|
switch fact.State() {
|
|
86
89
|
case state.ArtifactAbsent:
|
|
87
|
-
|
|
90
|
+
required := input.required
|
|
91
|
+
if input.logical == acceptance.GatesFile {
|
|
92
|
+
phase, err := observation.DeclaredPhase()
|
|
93
|
+
if err != nil {
|
|
94
|
+
return nil, false, err
|
|
95
|
+
}
|
|
96
|
+
policy, _ := state.PolicyFor(phase)
|
|
97
|
+
required = policyRequires(policy, input.logical)
|
|
98
|
+
}
|
|
99
|
+
if required {
|
|
88
100
|
return nil, false, fmt.Errorf("readiness input %s is missing", input.logical)
|
|
89
101
|
}
|
|
90
102
|
return nil, false, nil
|
|
91
103
|
case state.ArtifactEmpty, state.ArtifactPresent:
|
|
92
|
-
|
|
104
|
+
content := fact.Bytes()
|
|
105
|
+
if input.logical == acceptance.GatesFile {
|
|
106
|
+
content, err := normalizedReadinessGates(content)
|
|
107
|
+
return content, true, err
|
|
108
|
+
}
|
|
109
|
+
return content, true, nil
|
|
93
110
|
case state.ArtifactMalformed, state.ArtifactUnsafe, state.ArtifactUnreadable:
|
|
94
111
|
diagnostic, _ := fact.Diagnostic()
|
|
95
112
|
return nil, false, readinessDiagnosticError(diagnostic)
|
|
@@ -98,6 +115,25 @@ func retainedReadinessInput(observation *state.WorkspaceObservation, input readi
|
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
// Vet approves obligations, not their checkbox, evidence, or progress prose.
|
|
119
|
+
func normalizedReadinessGates(content []byte) ([]byte, error) {
|
|
120
|
+
doc := acceptance.ParseLedger(string(content))
|
|
121
|
+
if len(doc.Errors) > 0 {
|
|
122
|
+
return nil, fmt.Errorf("readiness input gates.md is malformed: %s", strings.Join(doc.Errors, "; "))
|
|
123
|
+
}
|
|
124
|
+
slices.SortFunc(doc.Gates, func(a, b *acceptance.Gate) int {
|
|
125
|
+
return strings.Compare(a.ID, b.ID)
|
|
126
|
+
})
|
|
127
|
+
var normalized bytes.Buffer
|
|
128
|
+
writeReadinessLength(&normalized, uint64(len(doc.Gates)))
|
|
129
|
+
for _, gate := range doc.Gates {
|
|
130
|
+
for _, field := range [...]string{gate.ID, gate.Title, gate.Check, gate.Expect, gate.Cwd, gate.Inputs, gate.Run} {
|
|
131
|
+
writeReadinessField(&normalized, field)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return normalized.Bytes(), nil
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
func readinessDiagnostics(observation *state.WorkspaceObservation) []state.ArtifactDiagnostic {
|
|
102
138
|
var diagnostics []state.ArtifactDiagnostic
|
|
103
139
|
for _, input := range readinessInputs {
|
|
@@ -117,12 +153,6 @@ func verifyReadinessBinding(observation *state.WorkspaceObservation) (string, er
|
|
|
117
153
|
if err != nil {
|
|
118
154
|
return "", err
|
|
119
155
|
}
|
|
120
|
-
// A line recorded over the raw bytes, before progress cells were blanked,
|
|
121
|
-
// still verifies while those bytes are unchanged.
|
|
122
|
-
legacy, err := bindReadinessInputs(observation, false)
|
|
123
|
-
if err != nil {
|
|
124
|
-
return "", err
|
|
125
|
-
}
|
|
126
156
|
fact, ok := observation.Fact("eng-review.md")
|
|
127
157
|
if !ok || fact.State() == state.ArtifactAbsent {
|
|
128
158
|
return expected, errors.New("readiness input eng-review.md is missing")
|
|
@@ -143,7 +173,7 @@ func verifyReadinessBinding(observation *state.WorkspaceObservation) (string, er
|
|
|
143
173
|
continue
|
|
144
174
|
}
|
|
145
175
|
attempts++
|
|
146
|
-
if line == expected
|
|
176
|
+
if line == expected {
|
|
147
177
|
matches++
|
|
148
178
|
}
|
|
149
179
|
}
|
|
@@ -154,9 +184,9 @@ func verifyReadinessBinding(observation *state.WorkspaceObservation) (string, er
|
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
// ReadinessInputDigest is one readiness input's content identity at a moment
|
|
157
|
-
// in time
|
|
158
|
-
//
|
|
159
|
-
// a re-vet route need.
|
|
187
|
+
// in time (canonical definitions only for gates.md). The aggregate binding
|
|
188
|
+
// answers "did anything change"; the per-input digest answers "which artifact
|
|
189
|
+
// changed", which is what a drift diagnosis and a re-vet route need.
|
|
160
190
|
type ReadinessInputDigest struct {
|
|
161
191
|
Path string `json:"path"`
|
|
162
192
|
Present bool `json:"present"`
|
|
@@ -180,9 +210,9 @@ func withoutReviewedIdentity(content []byte) []byte {
|
|
|
180
210
|
// ReadinessInputDigests returns each readiness input's content digest plus the
|
|
181
211
|
// aggregate binding line Vet records in eng-review.md. A required input that
|
|
182
212
|
// is missing, or any unreadable input, is an error — the same refusal the
|
|
183
|
-
// readiness gate applies.
|
|
213
|
+
// readiness gate applies. Source selection matches ReadinessBinding.
|
|
184
214
|
func ReadinessInputDigests(root, slug string) ([]ReadinessInputDigest, string, error) {
|
|
185
|
-
observation, err := state.
|
|
215
|
+
observation, err := state.ObserveReferencedWorkspace(root, slug)
|
|
186
216
|
if err != nil {
|
|
187
217
|
return nil, "", err
|
|
188
218
|
}
|
|
@@ -321,12 +351,12 @@ func policyRequires(policy state.PhasePolicy, name state.ArtifactPath) bool {
|
|
|
321
351
|
return false
|
|
322
352
|
}
|
|
323
353
|
|
|
324
|
-
func writeReadinessField(digest
|
|
354
|
+
func writeReadinessField(digest io.Writer, value string) {
|
|
325
355
|
writeReadinessLength(digest, uint64(len(value)))
|
|
326
356
|
_, _ = digest.Write([]byte(value))
|
|
327
357
|
}
|
|
328
358
|
|
|
329
|
-
func writeReadinessLength(digest
|
|
359
|
+
func writeReadinessLength(digest io.Writer, value uint64) {
|
|
330
360
|
var encoded [8]byte
|
|
331
361
|
binary.BigEndian.PutUint64(encoded[:], value)
|
|
332
362
|
_, _ = digest.Write(encoded[:])
|
|
@@ -16,6 +16,10 @@ import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
func (r *runner) install() error {
|
|
19
|
+
if err := r.openTargetRoot(); err != nil {
|
|
20
|
+
return err
|
|
21
|
+
}
|
|
22
|
+
defer r.closeTargetRoot()
|
|
19
23
|
fmt.Fprintln(r.opts.Stdout, "DevRites installer")
|
|
20
24
|
fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
|
|
21
25
|
fmt.Fprintf(r.opts.Stdout, " payload: %s\n", r.payload)
|
|
@@ -43,6 +47,10 @@ func (r *runner) install() error {
|
|
|
43
47
|
if err := r.preflightInstall(); err != nil {
|
|
44
48
|
return err
|
|
45
49
|
}
|
|
50
|
+
// Complete binary installation before changing payload ownership or version.
|
|
51
|
+
if err := r.installBinary(); err != nil {
|
|
52
|
+
return fmt.Errorf("install engine binary: %w", err)
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex, r.opts.WithOmp, r.opts.WithPi, r.opts.WithDevin, r.opts.WithDsh) {
|
|
48
56
|
if err := r.installTree(tree.PayloadPrefix, tree.TargetPrefix); err != nil {
|
|
@@ -106,9 +114,6 @@ func (r *runner) install() error {
|
|
|
106
114
|
return fmt.Errorf("write manifest: %w", err)
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
|
-
if err := r.installBinary(); err != nil {
|
|
110
|
-
return fmt.Errorf("install engine binary: %w", err)
|
|
111
|
-
}
|
|
112
117
|
|
|
113
118
|
fmt.Fprintln(r.opts.Stdout)
|
|
114
119
|
if r.opts.DryRun {
|
|
@@ -218,19 +223,32 @@ func (r *runner) installData(data []byte, rel string) error {
|
|
|
218
223
|
return nil
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// after the path recheck cannot redirect the write.
|
|
224
|
-
func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error {
|
|
225
|
-
rel, err := filepath.Rel(r.target, dest)
|
|
226
|
+
func (r *runner) readConfined(path string) ([]byte, error) {
|
|
227
|
+
rel, err := filepath.Rel(r.target, path)
|
|
226
228
|
if err != nil {
|
|
227
|
-
return err
|
|
229
|
+
return nil, err
|
|
230
|
+
}
|
|
231
|
+
if r.targetRoot != nil {
|
|
232
|
+
return r.targetRoot.ReadFile(rel)
|
|
228
233
|
}
|
|
229
234
|
root, err := os.OpenRoot(r.target)
|
|
230
235
|
if err != nil {
|
|
231
|
-
return err
|
|
236
|
+
return nil, err
|
|
232
237
|
}
|
|
233
238
|
defer func() { _ = root.Close() }()
|
|
239
|
+
return root.ReadFile(rel)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// writeConfined uses the operation's validated root, not its mutable pathname.
|
|
243
|
+
func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error {
|
|
244
|
+
rel, err := filepath.Rel(r.target, dest)
|
|
245
|
+
if err != nil {
|
|
246
|
+
return err
|
|
247
|
+
}
|
|
248
|
+
root := r.targetRoot
|
|
249
|
+
if root == nil {
|
|
250
|
+
return fmt.Errorf("install target is not open")
|
|
251
|
+
}
|
|
234
252
|
if info, err := root.Stat(rel); err == nil {
|
|
235
253
|
perm = info.Mode().Perm()
|
|
236
254
|
} else if !os.IsNotExist(err) {
|
|
@@ -239,18 +257,16 @@ func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error
|
|
|
239
257
|
return fsutil.WriteFileAtomicIn(root, rel, data, perm)
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
// removeConfined
|
|
243
|
-
// through a root opened on the target.
|
|
260
|
+
// removeConfined uses the same retained root as writes and managed reads.
|
|
244
261
|
func (r *runner) removeConfined(dest string) error {
|
|
245
262
|
rel, err := filepath.Rel(r.target, dest)
|
|
246
263
|
if err != nil {
|
|
247
264
|
return err
|
|
248
265
|
}
|
|
249
|
-
root
|
|
250
|
-
if
|
|
251
|
-
return
|
|
266
|
+
root := r.targetRoot
|
|
267
|
+
if root == nil {
|
|
268
|
+
return fmt.Errorf("install target is not open")
|
|
252
269
|
}
|
|
253
|
-
defer func() { _ = root.Close() }()
|
|
254
270
|
return root.Remove(rel)
|
|
255
271
|
}
|
|
256
272
|
|
|
@@ -285,11 +301,10 @@ func (r *runner) seedDevrites() error {
|
|
|
285
301
|
fmt.Fprintln(r.opts.Stdout, " [seed] .devrites/ACTIVE (runtime state - preserved on uninstall)")
|
|
286
302
|
return nil
|
|
287
303
|
}
|
|
288
|
-
if err :=
|
|
304
|
+
if err := r.targetRoot.MkdirAll(".devrites", 0o755); err != nil {
|
|
289
305
|
return fmt.Errorf("create %s: %w", filepath.Dir(active), err)
|
|
290
306
|
}
|
|
291
|
-
|
|
292
|
-
f, err := os.OpenFile(active, os.O_CREATE|os.O_EXCL, 0o644)
|
|
307
|
+
f, err := r.targetRoot.OpenFile(filepath.Join(".devrites", "ACTIVE"), os.O_CREATE|os.O_EXCL, 0o644)
|
|
293
308
|
if err != nil {
|
|
294
309
|
if os.IsExist(err) {
|
|
295
310
|
return nil
|
|
@@ -342,7 +357,7 @@ func (r *runner) relinquishEditedHostConfigs() (map[string]bool, error) {
|
|
|
342
357
|
func (r *runner) seedHostConfig(config hostpack.HostConfig) error {
|
|
343
358
|
if _, managed := r.prev[config.Rel]; !managed {
|
|
344
359
|
// #nosec G304 -- fixed host config path under the operator target
|
|
345
|
-
data, err :=
|
|
360
|
+
data, err := r.readConfined(filepath.Join(r.target, filepath.FromSlash(config.Rel)))
|
|
346
361
|
if err == nil && string(data) != config.Content {
|
|
347
362
|
if !strings.Contains(string(data), config.Applied) {
|
|
348
363
|
fmt.Fprintf(r.opts.Stderr, "warning: %s exists and was not changed; to hide duplicate DevRites skills in %s, add: %s\n", config.Rel, config.Host, config.Advice)
|
|
@@ -408,7 +423,7 @@ func (r *runner) pruneDropped() error {
|
|
|
408
423
|
if err := r.removeConfined(dead); err != nil && !os.IsNotExist(err) {
|
|
409
424
|
return fmt.Errorf("remove %s: %w", rel, err)
|
|
410
425
|
}
|
|
411
|
-
pruneEmptyDirs(filepath.Dir(dead)
|
|
426
|
+
r.pruneEmptyDirs(filepath.Dir(dead))
|
|
412
427
|
fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
|
|
413
428
|
}
|
|
414
429
|
r.stats.pruned++
|
|
@@ -63,11 +63,20 @@ func (r *runner) installBinary() error {
|
|
|
63
63
|
if err != nil {
|
|
64
64
|
return err
|
|
65
65
|
}
|
|
66
|
+
removeBackup := true
|
|
66
67
|
if backup != "" {
|
|
67
|
-
defer func() {
|
|
68
|
+
defer func() {
|
|
69
|
+
if removeBackup {
|
|
70
|
+
_ = os.Remove(backup)
|
|
71
|
+
}
|
|
72
|
+
}()
|
|
68
73
|
}
|
|
69
74
|
if err := fsutil.WriteFileAtomic(dest, data, 0o755); err != nil {
|
|
70
75
|
if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
|
|
76
|
+
removeBackup = false
|
|
77
|
+
if backup != "" {
|
|
78
|
+
restoreErr = fmt.Errorf("%w (previous binary retained at %s)", restoreErr, backup)
|
|
79
|
+
}
|
|
71
80
|
return fmt.Errorf("install binary: %v; restore previous binary: %w", err, restoreErr)
|
|
72
81
|
}
|
|
73
82
|
return err
|
|
@@ -75,6 +84,10 @@ func (r *runner) installBinary() error {
|
|
|
75
84
|
_ = os.Chmod(dest, 0o755)
|
|
76
85
|
if _, err := verifyEngineBinary(dest, tag, 30*time.Second); err != nil {
|
|
77
86
|
if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
|
|
87
|
+
removeBackup = false
|
|
88
|
+
if backup != "" {
|
|
89
|
+
restoreErr = fmt.Errorf("%w (previous binary retained at %s)", restoreErr, backup)
|
|
90
|
+
}
|
|
78
91
|
return fmt.Errorf("verify installed engine binary: %v; restore previous binary: %w", err, restoreErr)
|
|
79
92
|
}
|
|
80
93
|
if hadOld {
|
|
@@ -173,6 +186,44 @@ func (r *runner) acquireBinary(tag string) (string, func(), error) {
|
|
|
173
186
|
return path, func() {}, nil
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
func (r *runner) preflightBinaryRemoval() error {
|
|
190
|
+
if runtime.GOOS != "windows" || r.opts.DryRun || r.opts.KeepBinary || !r.opts.WithBinary || os.Getenv("DEVRITES_NO_BINARY") == "1" {
|
|
191
|
+
return nil
|
|
192
|
+
}
|
|
193
|
+
for _, path := range binaryCandidates() {
|
|
194
|
+
info, err := os.Lstat(path)
|
|
195
|
+
if os.IsNotExist(err) {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if err != nil {
|
|
199
|
+
return fmt.Errorf("inspect engine binary %s: %w", path, err)
|
|
200
|
+
}
|
|
201
|
+
if info.Mode()&os.ModeSymlink != 0 {
|
|
202
|
+
info, err = os.Stat(path)
|
|
203
|
+
if os.IsNotExist(err) {
|
|
204
|
+
return nil
|
|
205
|
+
}
|
|
206
|
+
if err != nil {
|
|
207
|
+
return fmt.Errorf("inspect engine binary %s: %w", path, err)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
executable, err := os.Executable()
|
|
211
|
+
if err != nil {
|
|
212
|
+
return fmt.Errorf("locate running engine binary: %w", err)
|
|
213
|
+
}
|
|
214
|
+
running, err := os.Stat(executable)
|
|
215
|
+
if err != nil {
|
|
216
|
+
return fmt.Errorf("inspect running engine binary %s: %w", executable, err)
|
|
217
|
+
}
|
|
218
|
+
if os.SameFile(info, running) {
|
|
219
|
+
return fmt.Errorf("cannot remove running engine binary %s on Windows; use npx devrites@latest uninstall --target %q or --keep-binary", path, r.target)
|
|
220
|
+
}
|
|
221
|
+
// Removal stops at the first existing candidate, even if another is running.
|
|
222
|
+
return nil
|
|
223
|
+
}
|
|
224
|
+
return nil
|
|
225
|
+
}
|
|
226
|
+
|
|
176
227
|
func (r *runner) removeBinary() error {
|
|
177
228
|
if r.opts.KeepBinary {
|
|
178
229
|
fmt.Fprintln(r.opts.Stdout, " kept the global devrites-engine binary (--keep-binary).")
|
|
@@ -187,17 +238,24 @@ func (r *runner) removeBinary() error {
|
|
|
187
238
|
return nil
|
|
188
239
|
}
|
|
189
240
|
for _, p := range binaryCandidates() {
|
|
190
|
-
if p == "" || !exists(p) {
|
|
191
|
-
continue
|
|
192
|
-
}
|
|
193
241
|
if r.opts.DryRun {
|
|
242
|
+
if _, err := os.Lstat(p); err != nil {
|
|
243
|
+
if os.IsNotExist(err) {
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
return fmt.Errorf("inspect engine binary %s: %w", p, err)
|
|
247
|
+
}
|
|
194
248
|
fmt.Fprintf(r.opts.Stdout, " [remove] %s (global engine binary)\n", p)
|
|
195
249
|
continue
|
|
196
250
|
}
|
|
197
|
-
if err := os.Remove(p); err
|
|
198
|
-
|
|
199
|
-
|
|
251
|
+
if err := os.Remove(p); err != nil {
|
|
252
|
+
if os.IsNotExist(err) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
return fmt.Errorf("remove %s: %w", p, err)
|
|
200
256
|
}
|
|
257
|
+
fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", p)
|
|
258
|
+
return nil
|
|
201
259
|
}
|
|
202
260
|
return nil
|
|
203
261
|
}
|
|
@@ -220,10 +278,10 @@ func engineBinaryName() string {
|
|
|
220
278
|
}
|
|
221
279
|
|
|
222
280
|
func binaryCandidates() []string {
|
|
223
|
-
candidates := []string{}
|
|
224
281
|
if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
|
|
225
|
-
|
|
282
|
+
return []string{filepath.Join(dir, engineBinaryName())}
|
|
226
283
|
}
|
|
284
|
+
candidates := []string{}
|
|
227
285
|
home, _ := os.UserHomeDir()
|
|
228
286
|
if home != "" {
|
|
229
287
|
candidates = append(candidates, filepath.Join(home, ".local", "bin", engineBinaryName()))
|
|
@@ -61,6 +61,8 @@ type stats struct {
|
|
|
61
61
|
type runner struct {
|
|
62
62
|
opts Options
|
|
63
63
|
target string
|
|
64
|
+
targetInfo fs.FileInfo
|
|
65
|
+
targetRoot *os.Root
|
|
64
66
|
payload string
|
|
65
67
|
payloadFS fs.FS
|
|
66
68
|
source string
|
|
@@ -246,6 +248,10 @@ func Apply(opts Options) error {
|
|
|
246
248
|
return fmt.Errorf("prepare installer: %w", err)
|
|
247
249
|
}
|
|
248
250
|
if opts.Mode == ModeUninstall {
|
|
251
|
+
if err := r.openTargetRoot(); err != nil {
|
|
252
|
+
return err
|
|
253
|
+
}
|
|
254
|
+
defer r.closeTargetRoot()
|
|
249
255
|
return r.uninstall()
|
|
250
256
|
}
|
|
251
257
|
if err := r.install(); err != nil {
|
|
@@ -297,27 +303,75 @@ func newRunner(opts Options) (*runner, error) {
|
|
|
297
303
|
}
|
|
298
304
|
}
|
|
299
305
|
r := &runner{
|
|
300
|
-
opts:
|
|
301
|
-
target:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
opts: opts,
|
|
307
|
+
target: target,
|
|
308
|
+
targetInfo: info,
|
|
309
|
+
payload: payload,
|
|
310
|
+
payloadFS: os.DirFS(payload),
|
|
311
|
+
source: source,
|
|
312
|
+
prev: map[string]managedRecord{},
|
|
313
|
+
records: map[string]string{},
|
|
314
|
+
preflight: map[string]pathSnapshot{},
|
|
308
315
|
}
|
|
309
316
|
if opts.Mode != ModeUninstall {
|
|
310
317
|
if err := r.validatePayload(); err != nil {
|
|
311
318
|
return nil, fmt.Errorf("validate payload: %w", err)
|
|
312
319
|
}
|
|
313
320
|
}
|
|
314
|
-
|
|
321
|
+
if err := r.openTargetRoot(); err != nil {
|
|
322
|
+
return nil, err
|
|
323
|
+
}
|
|
324
|
+
defer r.closeTargetRoot()
|
|
325
|
+
data, err := r.readConfined(filepath.Join(target, ManifestName))
|
|
326
|
+
if err != nil && !os.IsNotExist(err) {
|
|
327
|
+
return nil, fmt.Errorf("read manifest %s: %w", ManifestName, err)
|
|
328
|
+
}
|
|
329
|
+
r.prev, _, err = parseManifestData(data)
|
|
315
330
|
if err != nil {
|
|
316
331
|
return nil, err
|
|
317
332
|
}
|
|
318
333
|
return r, nil
|
|
319
334
|
}
|
|
320
335
|
|
|
336
|
+
func (r *runner) openTargetRoot() error {
|
|
337
|
+
root, err := os.OpenRoot(r.target)
|
|
338
|
+
if err != nil {
|
|
339
|
+
return err
|
|
340
|
+
}
|
|
341
|
+
info, err := root.Stat(".")
|
|
342
|
+
if err != nil || r.targetInfo == nil || !os.SameFile(r.targetInfo, info) {
|
|
343
|
+
_ = root.Close()
|
|
344
|
+
return fmt.Errorf("install target changed before operation: %s", r.target)
|
|
345
|
+
}
|
|
346
|
+
r.targetRoot = root
|
|
347
|
+
return nil
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func (r *runner) closeTargetRoot() {
|
|
351
|
+
if r.targetRoot != nil {
|
|
352
|
+
_ = r.targetRoot.Close()
|
|
353
|
+
r.targetRoot = nil
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
func (r *runner) checkTargetIdentity() error {
|
|
358
|
+
info, err := os.Stat(r.target)
|
|
359
|
+
if err != nil || r.targetInfo == nil || !os.SameFile(r.targetInfo, info) {
|
|
360
|
+
return fmt.Errorf("install target changed during operation: %s", r.target)
|
|
361
|
+
}
|
|
362
|
+
return nil
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func (r *runner) pruneEmptyDirs(start string) {
|
|
366
|
+
for start != r.target && start != filepath.Dir(start) {
|
|
367
|
+
rel, err := filepath.Rel(r.target, start)
|
|
368
|
+
if err != nil || r.targetRoot == nil || r.targetRoot.Remove(rel) != nil {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
start = filepath.Dir(start)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
321
375
|
func inferSourceDir() string {
|
|
322
376
|
if v := os.Getenv("DEVRITES_SOURCE_DIR"); v != "" {
|
|
323
377
|
return v
|
|
@@ -390,18 +444,6 @@ func isGlobalTarget(target string) bool {
|
|
|
390
444
|
return false
|
|
391
445
|
}
|
|
392
446
|
|
|
393
|
-
func pruneEmptyDirs(start, stop string) {
|
|
394
|
-
for {
|
|
395
|
-
if start == stop || start == filepath.Dir(start) || start == string(os.PathSeparator) {
|
|
396
|
-
return
|
|
397
|
-
}
|
|
398
|
-
if err := os.Remove(start); err != nil {
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
start = filepath.Dir(start)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
447
|
func exists(path string) bool {
|
|
406
448
|
// #nosec G703 -- existence probe on managed install paths
|
|
407
449
|
_, err := os.Lstat(path)
|
|
@@ -19,11 +19,6 @@ func readManifest(path string) (map[string]managedRecord, error) {
|
|
|
19
19
|
return records, err
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
func readManifestList(path string) ([]string, error) {
|
|
23
|
-
_, entries, err := parseManifest(path)
|
|
24
|
-
return entries, err
|
|
25
|
-
}
|
|
26
|
-
|
|
27
22
|
func parseManifest(path string) (map[string]managedRecord, []string, error) {
|
|
28
23
|
// #nosec G304 -- install manifest under the operator-selected target
|
|
29
24
|
data, err := os.ReadFile(path)
|
|
@@ -33,6 +28,10 @@ func parseManifest(path string) (map[string]managedRecord, []string, error) {
|
|
|
33
28
|
}
|
|
34
29
|
return nil, nil, fmt.Errorf("read manifest %s: %w", path, err)
|
|
35
30
|
}
|
|
31
|
+
return parseManifestData(data)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func parseManifestData(data []byte) (map[string]managedRecord, []string, error) {
|
|
36
35
|
hashes := map[string]string{}
|
|
37
36
|
var out []string
|
|
38
37
|
for _, line := range strings.Split(string(data), "\n") {
|