devrites 4.0.1 → 4.0.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 +9 -0
- package/README.md +23 -18
- package/SECURITY.md +9 -8
- package/docs/adr/0024-native-policy-offline-installer-boundary.md +1 -1
- package/docs/adr/0028-self-contained-engine-update.md +66 -0
- package/docs/adr/README.md +2 -1
- package/docs/architecture.md +17 -12
- package/docs/cli.md +30 -17
- package/docs/command-map.md +6 -4
- package/docs/engine/commands.md +14 -12
- package/docs/release.md +7 -7
- package/engine/internal/install/install.go +95 -6
- package/engine/internal/install/install_test.go +111 -0
- package/engine/internal/release/release.go +411 -0
- package/engine/internal/release/release_test.go +167 -0
- package/engine/tests/meta_test.go +6 -6
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
package release
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/tar"
|
|
5
|
+
"bytes"
|
|
6
|
+
"compress/gzip"
|
|
7
|
+
"context"
|
|
8
|
+
"crypto/sha256"
|
|
9
|
+
"fmt"
|
|
10
|
+
"net/http"
|
|
11
|
+
"net/http/httptest"
|
|
12
|
+
"os"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"strings"
|
|
15
|
+
"testing"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func TestLatestAndAcquireVerifiedRelease(t *testing.T) {
|
|
19
|
+
const tag = "v4.1.0"
|
|
20
|
+
bundleName := "devrites-" + tag + ".tar.gz"
|
|
21
|
+
bundle := testBundle(t, tag, map[string]string{
|
|
22
|
+
"package.json": `{"version":"4.1.0"}`,
|
|
23
|
+
"pack/generated/claude/skills/rite/SKILL.md": "rite\n",
|
|
24
|
+
})
|
|
25
|
+
binaryName, err := platformBinaryName()
|
|
26
|
+
if err != nil {
|
|
27
|
+
t.Skip(err)
|
|
28
|
+
}
|
|
29
|
+
assets := map[string][]byte{
|
|
30
|
+
bundleName: bundle,
|
|
31
|
+
binaryName: []byte("test engine"),
|
|
32
|
+
}
|
|
33
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
34
|
+
if r.URL.Path == "/repos/owner/repo/releases/latest" {
|
|
35
|
+
_, _ = fmt.Fprintf(w, `{"tag_name":%q}`, tag)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
name := filepath.Base(r.URL.Path)
|
|
39
|
+
if strings.HasSuffix(name, ".sha256") {
|
|
40
|
+
assetName := strings.TrimSuffix(name, ".sha256")
|
|
41
|
+
asset, ok := assets[assetName]
|
|
42
|
+
if !ok {
|
|
43
|
+
http.NotFound(w, r)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
_, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
asset, ok := assets[name]
|
|
50
|
+
if !ok {
|
|
51
|
+
http.NotFound(w, r)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
_, _ = w.Write(asset)
|
|
55
|
+
}))
|
|
56
|
+
defer server.Close()
|
|
57
|
+
oldAPI, oldWeb := apiBaseURL, webBaseURL
|
|
58
|
+
apiBaseURL, webBaseURL = server.URL, server.URL
|
|
59
|
+
t.Cleanup(func() { apiBaseURL, webBaseURL = oldAPI, oldWeb })
|
|
60
|
+
|
|
61
|
+
gotTag, err := Latest(context.Background(), "owner/repo")
|
|
62
|
+
if err != nil {
|
|
63
|
+
t.Fatal(err)
|
|
64
|
+
}
|
|
65
|
+
if gotTag != tag {
|
|
66
|
+
t.Fatalf("latest tag = %q, want %q", gotTag, tag)
|
|
67
|
+
}
|
|
68
|
+
candidate, cleanup, err := Acquire(context.Background(), "owner/repo", tag)
|
|
69
|
+
if err != nil {
|
|
70
|
+
t.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
root := filepath.Dir(candidate.SourceDir)
|
|
73
|
+
if _, err := os.Stat(filepath.Join(candidate.PayloadDir, "claude", "skills", "rite", "SKILL.md")); err != nil {
|
|
74
|
+
t.Fatalf("extracted payload: %v", err)
|
|
75
|
+
}
|
|
76
|
+
if got, err := os.ReadFile(candidate.EnginePath); err != nil || string(got) != "test engine" {
|
|
77
|
+
t.Fatalf("engine = %q, %v", got, err)
|
|
78
|
+
}
|
|
79
|
+
cleanup()
|
|
80
|
+
if _, err := os.Stat(root); !os.IsNotExist(err) {
|
|
81
|
+
t.Fatalf("cleanup kept %s: %v", root, err)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func TestValidTag(t *testing.T) {
|
|
86
|
+
for _, tc := range []struct {
|
|
87
|
+
tag string
|
|
88
|
+
want bool
|
|
89
|
+
}{
|
|
90
|
+
{tag: "v4.1.0", want: true},
|
|
91
|
+
{tag: "v4.1.0-rc.1+build.2", want: true},
|
|
92
|
+
{tag: "4.1.0", want: false},
|
|
93
|
+
{tag: "v4.1.0-01", want: false},
|
|
94
|
+
{tag: "v4.1", want: false},
|
|
95
|
+
} {
|
|
96
|
+
if got := validTag(tc.tag); got != tc.want {
|
|
97
|
+
t.Errorf("validTag(%q) = %t, want %t", tc.tag, got, tc.want)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func TestAcquireRejectsChecksumMismatch(t *testing.T) {
|
|
103
|
+
const tag = "v4.1.0"
|
|
104
|
+
bundleName := "devrites-" + tag + ".tar.gz"
|
|
105
|
+
bundle := testBundle(t, tag, map[string]string{
|
|
106
|
+
"package.json": `{"version":"4.1.0"}`,
|
|
107
|
+
"pack/generated/claude/skills/rite/SKILL.md": "rite\n",
|
|
108
|
+
})
|
|
109
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
110
|
+
if strings.HasSuffix(r.URL.Path, ".sha256") {
|
|
111
|
+
_, _ = fmt.Fprintf(w, "%064d %s\n", 0, bundleName)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
_, _ = w.Write(bundle)
|
|
115
|
+
}))
|
|
116
|
+
defer server.Close()
|
|
117
|
+
oldWeb := webBaseURL
|
|
118
|
+
webBaseURL = server.URL
|
|
119
|
+
t.Cleanup(func() { webBaseURL = oldWeb })
|
|
120
|
+
|
|
121
|
+
_, _, err := Acquire(context.Background(), "owner/repo", tag)
|
|
122
|
+
if err == nil || !strings.Contains(err.Error(), "checksum mismatch") {
|
|
123
|
+
t.Fatalf("Acquire error = %v, want checksum mismatch", err)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func TestExtractBundleRejectsPathTraversal(t *testing.T) {
|
|
128
|
+
const tag = "v4.1.0"
|
|
129
|
+
archive := filepath.Join(t.TempDir(), "unsafe.tar.gz")
|
|
130
|
+
data := testBundle(t, tag, map[string]string{"../escape": "bad"})
|
|
131
|
+
if err := os.WriteFile(archive, data, 0o600); err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
_, err := extractBundle(archive, t.TempDir(), tag)
|
|
135
|
+
if err == nil || !strings.Contains(err.Error(), "unsafe path") {
|
|
136
|
+
t.Fatalf("extractBundle error = %v, want unsafe path", err)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func testBundle(t *testing.T, tag string, files map[string]string) []byte {
|
|
141
|
+
t.Helper()
|
|
142
|
+
var raw bytes.Buffer
|
|
143
|
+
gz := gzip.NewWriter(&raw)
|
|
144
|
+
tw := tar.NewWriter(gz)
|
|
145
|
+
for name, body := range files {
|
|
146
|
+
data := []byte(body)
|
|
147
|
+
header := &tar.Header{
|
|
148
|
+
Name: "devrites-" + tag + "/" + name,
|
|
149
|
+
Mode: 0o644,
|
|
150
|
+
Size: int64(len(data)),
|
|
151
|
+
Typeflag: tar.TypeReg,
|
|
152
|
+
}
|
|
153
|
+
if err := tw.WriteHeader(header); err != nil {
|
|
154
|
+
t.Fatal(err)
|
|
155
|
+
}
|
|
156
|
+
if _, err := tw.Write(data); err != nil {
|
|
157
|
+
t.Fatal(err)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if err := tw.Close(); err != nil {
|
|
161
|
+
t.Fatal(err)
|
|
162
|
+
}
|
|
163
|
+
if err := gz.Close(); err != nil {
|
|
164
|
+
t.Fatal(err)
|
|
165
|
+
}
|
|
166
|
+
return raw.Bytes()
|
|
167
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
package main_test
|
|
2
2
|
|
|
3
|
-
// Cross-cutting assertion:
|
|
3
|
+
// Cross-cutting assertion: release acquisition is the only engine network boundary.
|
|
4
4
|
|
|
5
5
|
import (
|
|
6
6
|
"os/exec"
|
|
@@ -8,10 +8,9 @@ import (
|
|
|
8
8
|
"testing"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
func TestFirstPartyMakesNoNetworkCalls(t *testing.T) {
|
|
11
|
+
// TestNetworkImportsStayInReleaseBoundary keeps update networking isolated from
|
|
12
|
+
// workspace state, policy, proof, and installation logic.
|
|
13
|
+
func TestNetworkImportsStayInReleaseBoundary(t *testing.T) {
|
|
15
14
|
if _, err := exec.LookPath("go"); err != nil {
|
|
16
15
|
t.Skip("go toolchain unavailable")
|
|
17
16
|
}
|
|
@@ -22,6 +21,7 @@ func TestFirstPartyMakesNoNetworkCalls(t *testing.T) {
|
|
|
22
21
|
t.Fatalf("go list: %v", err)
|
|
23
22
|
}
|
|
24
23
|
forbidden := map[string]bool{"net": true, "net/http": true, "net/rpc": true, "net/smtp": true}
|
|
24
|
+
allowed := map[string]bool{"github.com/devrites/devrites/internal/release": true}
|
|
25
25
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
26
26
|
fields := strings.Fields(line)
|
|
27
27
|
if len(fields) == 0 {
|
|
@@ -29,7 +29,7 @@ func TestFirstPartyMakesNoNetworkCalls(t *testing.T) {
|
|
|
29
29
|
}
|
|
30
30
|
pkg, imports := fields[0], fields[1:]
|
|
31
31
|
for _, imp := range imports {
|
|
32
|
-
if forbidden[imp] {
|
|
32
|
+
if forbidden[imp] && !allowed[pkg] {
|
|
33
33
|
t.Errorf("first-party package %s imports forbidden network package %q", pkg, imp)
|
|
34
34
|
}
|
|
35
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code and Codex",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|