devrites 6.0.1 → 6.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 +6 -0
- package/README.md +15 -64
- package/SECURITY.md +7 -11
- package/bin/devrites.mjs +0 -8
- package/docs/architecture.md +2 -5
- package/docs/cli.md +4 -11
- package/docs/command-map.md +1 -2
- package/docs/release.md +5 -6
- package/engine/internal/release/release.go +3 -13
- package/engine/internal/release/release_test.go +6 -197
- package/install.sh +1 -116
- package/package.json +1 -1
- package/scripts/install-lib.sh +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [6.0.2](https://github.com/ViktorsBaikers/DevRites/compare/v6.0.1...v6.0.2) (2026-10-14)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **installer:** drop gh attestation requirement from installers ([ff27b58](https://github.com/ViktorsBaikers/DevRites/commit/ff27b58f569dffdc956ce4a4c759cb6427abc9bd))
|
|
10
|
+
|
|
5
11
|
## [6.0.1](https://github.com/ViktorsBaikers/DevRites/compare/v6.0.0...v6.0.1) (2026-10-14)
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
|
|
|
31
31
|
the feature to another engineer, the next session reads the same plan,
|
|
32
32
|
decisions, and evidence before it continues.
|
|
33
33
|
|
|
34
|
-
**Status:** [`v6.0.
|
|
34
|
+
**Status:** [`v6.0.2`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.0.2): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
35
35
|
|
|
36
36
|
This is the latest published release; `main` may contain unreleased work.
|
|
37
37
|
|
|
@@ -51,11 +51,6 @@ This is the latest published release; `main` may contain unreleased work.
|
|
|
51
51
|
install and run DevRites. Development in this repository (`npm ci`, which
|
|
52
52
|
`.npmrc` runs with `engine-strict=true`) needs Node.js 22.22.2 or later within
|
|
53
53
|
22.x, or 24.15 or later; Node 23.x and 24.0 through 24.14 fail with `EBADENGINE`.
|
|
54
|
-
`gh` (GitHub CLI) must also be installed and authenticated, not merely
|
|
55
|
-
installed, because DevRites verifies the build-provenance attestation of every
|
|
56
|
-
release asset it downloads. Run `gh auth login` once, then `gh auth status` to
|
|
57
|
-
confirm. See [Install, update, and remove](#install-update-and-remove) for
|
|
58
|
-
which routes fail without it and which fall back to a local build.
|
|
59
54
|
|
|
60
55
|
```bash
|
|
61
56
|
npx devrites@latest
|
|
@@ -315,8 +310,7 @@ Run `npx devrites@latest --help` for common flags and
|
|
|
315
310
|
<br>
|
|
316
311
|
|
|
317
312
|
Download the release-owned installer and its checksum before executing it. It
|
|
318
|
-
needs `curl`, `gzip`, and `tar
|
|
319
|
-
authenticated (`gh auth login`).
|
|
313
|
+
needs `curl`, `gzip`, and `tar`.
|
|
320
314
|
|
|
321
315
|
```bash
|
|
322
316
|
bootstrap_dir="$(mktemp -d)"
|
|
@@ -349,8 +343,6 @@ project_dir="$PWD"
|
|
|
349
343
|
exit 1
|
|
350
344
|
fi
|
|
351
345
|
[ "$got" = "$want" ] || { echo 'error: install.sh checksum mismatch' >&2; exit 1; }
|
|
352
|
-
command -v gh >/dev/null 2>&1 || { echo 'error: gh (GitHub CLI) is required and was not found' >&2; exit 1; }
|
|
353
|
-
gh attestation verify install.sh --repo ViktorsBaikers/DevRites --signer-workflow ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main || { if gh auth status >/dev/null 2>&1; then echo 'error: install.sh attestation verification failed: no build provenance from ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main' >&2; else echo 'error: gh is not authenticated; run gh auth login' >&2; fi; exit 1; }
|
|
354
346
|
|
|
355
347
|
# Choose one Node-free operation:
|
|
356
348
|
bash ./install.sh --target "$project_dir" # install into the directory you ran this from
|
|
@@ -403,59 +395,19 @@ proof. `devrites-engine migrate` owns deterministic v5 schema normalization.
|
|
|
403
395
|
See the [CLI contract](docs/cli.md) and
|
|
404
396
|
[ADR-0029](docs/adr/0029-v5-workspace-schema-and-native-migration.md).
|
|
405
397
|
|
|
406
|
-
Every release asset is
|
|
407
|
-
decompressed, extracted, or executed
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`
|
|
412
|
-
|
|
413
|
-
bundle) has its own fallback of the same kind.
|
|
414
|
-
|
|
415
|
-
Release binaries and installers ship with SHA-256 sidecars and
|
|
416
|
-
build-provenance attestations. Verify a download with both
|
|
417
|
-
`shasum -a 256 -c <file>.sha256` and
|
|
398
|
+
Every release asset is checked against its exact-filename SHA-256 sidecar
|
|
399
|
+
before it is decompressed, extracted, or executed. The installers do not need
|
|
400
|
+
GitHub CLI and do not run `gh attestation verify`.
|
|
401
|
+
|
|
402
|
+
Release binaries and installers also ship with build-provenance attestations.
|
|
403
|
+
The `.sha256` sidecar is fetched from the same origin as the artifact, so it is
|
|
404
|
+
a corruption check, not a provenance one. To check provenance yourself, run
|
|
418
405
|
`gh attestation verify <file> -R ViktorsBaikers/DevRites --signer-workflow ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`
|
|
419
|
-
before it runs
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
but does not show that the intended publishing step produced it. See
|
|
406
|
+
on the downloaded file before it runs. This step is optional. Pin the signer
|
|
407
|
+
workflow, because valid provenance shows where a build ran but does not show
|
|
408
|
+
that the intended publishing step produced it. See
|
|
423
409
|
[docs/release.md](docs/release.md).
|
|
424
410
|
|
|
425
|
-
Read a failure as "prove provenance or stop", not as a verdict about your
|
|
426
|
-
build. The bootstrap checks `gh` itself, exits 1 before `install.sh` runs, and
|
|
427
|
-
prints the cause. It says `error: gh (GitHub CLI) is required and was not
|
|
428
|
-
found` when `gh` is absent. When `gh` is present but logged out (`gh auth
|
|
429
|
-
status` fails) it says `error: gh is not authenticated; run gh auth login`.
|
|
430
|
-
Only when `gh` is logged in and still rejects the attestation does it say
|
|
431
|
-
`error: install.sh attestation verification failed: no build provenance from
|
|
432
|
-
ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`. After that,
|
|
433
|
-
`install.sh` verifies the release bundle `devrites-<tag>.tar.gz` and reports the
|
|
434
|
-
same three causes as `attestation verification failed: gh (GitHub CLI) is
|
|
435
|
-
required and was not found`, `attestation verification failed: gh is not
|
|
436
|
-
authenticated ...; run 'gh auth login' and retry`, and `attestation verification
|
|
437
|
-
failed: no build provenance from <signer>`. The engine binary is acquired
|
|
438
|
-
separately: the bundled installer (`scripts/install-lib.sh`) and the npm route
|
|
439
|
-
report `attestation verification failed (gh must be installed and
|
|
440
|
-
authenticated)` for any of these causes, discard the downloaded binary, and
|
|
441
|
-
fall back to other sources. The bundled installer builds from the bundle's
|
|
442
|
-
`engine/` source when `go` is available, then tries an already-installed
|
|
443
|
-
`devrites-engine`, and only when none works does `install.sh` exit 1 with
|
|
444
|
-
`could not acquire devrites-engine` followed by that cause. `npx devrites@latest` repeats the combined message
|
|
445
|
-
before exiting 127 when no other engine is available. `devrites-engine update` reports `gh (GitHub CLI) is
|
|
446
|
-
required and must be authenticated for attestation verification` when `gh` is
|
|
447
|
-
absent, and `attestation verification failed for <asset>` whenever a present
|
|
448
|
-
`gh` rejects the asset, logged out or not.
|
|
449
|
-
|
|
450
|
-
There is no flag to skip, disable, or bypass attestation verification. The
|
|
451
|
-
`.sha256` sidecar is fetched from the same origin as the artifact, so a
|
|
452
|
-
substituted origin supplies a digest that matches its own payload, which is why
|
|
453
|
-
the sidecar is a corruption check and not a provenance one. If you cannot run
|
|
454
|
-
`gh auth login`, install Go and use `npx devrites@latest`, or clone this
|
|
455
|
-
repository and run `bash install.sh`: both compile `engine/` locally when the
|
|
456
|
-
download is refused, with `cd engine && go build -o devrites-engine .` as the
|
|
457
|
-
manual equivalent.
|
|
458
|
-
|
|
459
411
|
</details>
|
|
460
412
|
|
|
461
413
|
## Hosts
|
|
@@ -527,10 +479,9 @@ traces. DevRites detects these tools but does not install them.
|
|
|
527
479
|
|
|
528
480
|
- Installed host artifacts stay in the project. Use `--no-binary` or
|
|
529
481
|
`DEVRITES_NO_BINARY=1` to avoid keeping a shared binary outside it.
|
|
530
|
-
- npm, Bash, and `devrites-engine update` all require
|
|
531
|
-
|
|
532
|
-
`
|
|
533
|
-
`ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main` (see
|
|
482
|
+
- npm, Bash, and `devrites-engine update` all require release assets that
|
|
483
|
+
match their SHA-256 sidecars. Build-provenance attestations can be checked
|
|
484
|
+
manually with `gh attestation verify` (see
|
|
534
485
|
[Install, update, and remove](#install-update-and-remove)). Remote fetches use HTTPS at every redirect hop, are bounded in size,
|
|
535
486
|
and never fall back to an unchecked raw file, source archive, tag, or default
|
|
536
487
|
branch.
|
package/SECURITY.md
CHANGED
|
@@ -119,23 +119,20 @@ and `.codex/config.toml` without replacing unrelated user settings. The shared
|
|
|
119
119
|
`devrites-engine` executable is the only allowed artifact outside the project.
|
|
120
120
|
It is installed to `DEVRITES_BIN_DIR`, a writable `~/.local/bin`, or a writable
|
|
121
121
|
`/usr/local/bin`; `--no-binary` / `DEVRITES_NO_BINARY=1` skips it. The bootstrap
|
|
122
|
-
and direct updater may fetch the release bundle and checksummed engine assets
|
|
123
|
-
each of which must also pass `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`).
|
|
122
|
+
and direct updater may fetch the release bundle and checksummed engine assets.
|
|
124
123
|
They never invoke `sudo` or edit shell startup files.
|
|
125
124
|
|
|
126
125
|
The documented Node-free boundary downloads the release-owned `install.sh` and
|
|
127
|
-
its exact-name sidecar, then requires
|
|
128
|
-
|
|
129
|
-
signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`)
|
|
130
|
-
to pass before execution, as in the README "verified Bash bootstrap"; mutable
|
|
126
|
+
its exact-name sidecar, then requires the checksum to pass before execution, as
|
|
127
|
+
in the README "verified Bash bootstrap"; mutable
|
|
131
128
|
default-branch scripts are not recommended. Legacy local and extracted shim invocations remain compatible, but
|
|
132
129
|
their exact-release guarantee begins at that verified asset. A piped shim never
|
|
133
130
|
treats current-directory siblings as its bundle.
|
|
134
131
|
|
|
135
132
|
Network acquisition accepts only an exact SemVer release asset and its mandatory
|
|
136
|
-
exact-filename SHA-256 sidecar
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
exact-filename SHA-256 sidecar. Installers do not run `gh attestation verify`;
|
|
134
|
+
release assets carry build-provenance attestations that users may verify
|
|
135
|
+
manually (see [docs/release.md](docs/release.md)). Every redirect hop must remain HTTPS, private
|
|
139
136
|
temporary-directory creation must succeed, and unchecked raw/source/default-
|
|
140
137
|
branch fallbacks are absent. Representative in-stream ceilings are 1 MiB for
|
|
141
138
|
release metadata, 4 KiB for a sidecar, and 64 MiB for an archive or binary, with
|
|
@@ -154,8 +151,7 @@ and [ADR-0028](docs/adr/0028-self-contained-engine-update.md) own the rationale.
|
|
|
154
151
|
With `npx devrites@latest`, the CLI (`bin/devrites.mjs`) calls
|
|
155
152
|
`devrites-engine` directly instead of running `install.sh`. The bundled host
|
|
156
153
|
payload is pinned to the requested npm package version. The shim first tries the
|
|
157
|
-
matching exact-release binary, which must pass its mandatory SHA-256 sidecar
|
|
158
|
-
`gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`),
|
|
154
|
+
matching exact-release binary, which must pass its mandatory SHA-256 sidecar,
|
|
159
155
|
then a package-local Go build, and then an existing engine. Remote responses are
|
|
160
156
|
byte-bounded and every redirect is checked before following it. It has no
|
|
161
157
|
runtime npm dependencies. The same project and shared-binary boundaries apply
|
package/bin/devrites.mjs
CHANGED
|
@@ -190,14 +190,6 @@ async function downloadEngine() {
|
|
|
190
190
|
rmSync(`${out}.sha256`, { force: true });
|
|
191
191
|
return null;
|
|
192
192
|
}
|
|
193
|
-
const signer = `${repo}/.github/workflows/ci.yml@refs/heads/main`;
|
|
194
|
-
const attestation = spawnSync('gh', ['attestation', 'verify', out, '--repo', repo, '--signer-workflow', signer], { stdio: 'ignore', timeout: 120_000 });
|
|
195
|
-
if (attestation.error || attestation.status !== 0) {
|
|
196
|
-
acquisitionFailure = `release ${tag} asset ${name}: attestation verification failed (gh must be installed and authenticated)`;
|
|
197
|
-
rmSync(out, { force: true });
|
|
198
|
-
rmSync(`${out}.sha256`, { force: true });
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
193
|
try {
|
|
202
194
|
chmodSync(out, 0o755);
|
|
203
195
|
} catch {
|
package/docs/architecture.md
CHANGED
|
@@ -154,8 +154,7 @@ consume only local source, payload, and binary candidates.
|
|
|
154
154
|
|
|
155
155
|
Direct `devrites-engine update` uses the separate `engine/internal/release`
|
|
156
156
|
boundary to resolve the latest stable release and acquire its checksummed bundle
|
|
157
|
-
and platform engine
|
|
158
|
-
`gh` must be installed and authenticated). It then invokes the downloaded engine with local paths, so
|
|
157
|
+
and platform engine. It then invokes the downloaded engine with local paths, so
|
|
159
158
|
the candidate validates its own payload schema. `update --check` resolves only
|
|
160
159
|
release metadata and downloads no assets. Engine `--to` and `--pre` selectors do
|
|
161
160
|
not exist.
|
|
@@ -164,9 +163,7 @@ The shell entrypoints
|
|
|
164
163
|
(`bin/devrites.mjs`) remain equivalent acquisition adapters. They acquire and
|
|
165
164
|
verify the exact-release bundle or binary, then pass local paths and user flags
|
|
166
165
|
through to the deterministic engine operation. All network paths require exact SemVer,
|
|
167
|
-
HTTPS-only redirects, exact-filename SHA-256 sidecars,
|
|
168
|
-
pass for every release asset (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
|
|
169
|
-
see [Install, update, and remove](../README.md#install-update-and-remove)), private temporary
|
|
166
|
+
HTTPS-only redirects, exact-filename SHA-256 sidecars, private temporary
|
|
170
167
|
directories, bounded transfers, and full archive preflight. They provide no
|
|
171
168
|
unchecked raw, source-archive, tag, or default-branch fallback.
|
|
172
169
|
|
package/docs/cli.md
CHANGED
|
@@ -76,11 +76,7 @@ Run `devrites-engine update` in an installed project to resolve the latest
|
|
|
76
76
|
stable release and update the project pack plus shared engine binary. The
|
|
77
77
|
command downloads the release bundle and platform engine, verifies both exact
|
|
78
78
|
filenames against their SHA-256 sidecars, then hands the extracted candidate to
|
|
79
|
-
the downloaded engine.
|
|
80
|
-
pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`,
|
|
81
|
-
so `gh` must be installed and authenticated, with no bypass (see
|
|
82
|
-
[Install, update, and remove](../README.md#install-update-and-remove)). This
|
|
83
|
-
avoids asking an old engine to validate a newer payload schema.
|
|
79
|
+
the downloaded engine. This avoids asking an old engine to validate a newer payload schema.
|
|
84
80
|
|
|
85
81
|
`devrites-engine update --check` resolves and compares release metadata without
|
|
86
82
|
downloading assets. `--source-dir` and `--payload-dir` remain the advanced local
|
|
@@ -100,9 +96,8 @@ comparison without changing the installation. Use
|
|
|
100
96
|
retaining the shared `devrites-engine` binary.
|
|
101
97
|
|
|
102
98
|
If an older direct engine reports `missing codex/hooks.json` or asks for
|
|
103
|
-
`--source-dir`, run `npx devrites@latest update`, or use a release `install.sh` that is
|
|
104
|
-
checksum-verified
|
|
105
|
-
the verified Bash bootstrap under
|
|
99
|
+
`--source-dir`, run `npx devrites@latest update`, or use a release `install.sh` that is
|
|
100
|
+
checksum-verified (see the verified Bash bootstrap under
|
|
106
101
|
[README Install, update, and remove](../README.md#install-update-and-remove))
|
|
107
102
|
and run `bash ./install.sh update` once to install a release with the
|
|
108
103
|
self-contained updater.
|
|
@@ -271,9 +266,7 @@ commands refuse ambiguous, escaped, symlinked, or otherwise unsafe roots.
|
|
|
271
266
|
Install, update application, and uninstall remain manifest-owned local
|
|
272
267
|
operations. Direct engine update, shell, and npm may acquire a release candidate;
|
|
273
268
|
all remote acquisition uses exact SemVer, HTTPS-only redirect hops, mandatory
|
|
274
|
-
exact-filename SHA-256 sidecars,
|
|
275
|
-
asset (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
|
|
276
|
-
`gh` must be installed and authenticated, with no bypass), private temporary
|
|
269
|
+
exact-filename SHA-256 sidecars, private temporary
|
|
277
270
|
directories, and byte/archive bounds, with no unchecked raw/source/default-branch
|
|
278
271
|
fallback. Update accepts
|
|
279
272
|
`--check` to compare the installed manifest version with the latest stable
|
package/docs/command-map.md
CHANGED
|
@@ -74,8 +74,7 @@ Scans cap captured input at 64 MiB and entries/findings at 4,096; findings expos
|
|
|
74
74
|
metadata only and source or limit errors fail closed.
|
|
75
75
|
|
|
76
76
|
Direct `devrites-engine update` acquires the latest exact-release pack and
|
|
77
|
-
platform binary, each verified by SHA-256 sidecar
|
|
78
|
-
(`gh` installed and authenticated), then hands local paths to the downloaded engine. Shell/npm may
|
|
77
|
+
platform binary, each verified by SHA-256 sidecar, then hands local paths to the downloaded engine. Shell/npm may
|
|
79
78
|
perform the same acquisition before invoking the local path. `--check` compares
|
|
80
79
|
the installed version with latest release metadata without downloading assets;
|
|
81
80
|
engine release selectors are unsupported.
|
package/docs/release.md
CHANGED
|
@@ -31,9 +31,8 @@ the missing host payload before handing that local candidate to the engine. The
|
|
|
31
31
|
engine itself only validates and copies host payloads; it never generates them.
|
|
32
32
|
The npm entrypoint, verified release installer, and direct engine updater acquire
|
|
33
33
|
only an exact-SemVer release bundle or platform binary with its mandatory
|
|
34
|
-
exact-filename SHA-256 sidecar
|
|
35
|
-
|
|
36
|
-
`gh` must be installed and authenticated, with no bypass).
|
|
34
|
+
exact-filename SHA-256 sidecar. They do not run `gh attestation verify`; that
|
|
35
|
+
check is an optional manual step (below).
|
|
37
36
|
|
|
38
37
|
Verify a downloaded engine binary from a GitHub Release:
|
|
39
38
|
|
|
@@ -45,7 +44,8 @@ curl -q -LO "https://github.com/ViktorsBaikers/DevRites/releases/download/v<vers
|
|
|
45
44
|
|
|
46
45
|
Every release artifact also carries a GitHub artifact attestation (build
|
|
47
46
|
provenance generated inside the release job, bound to the artifact digest).
|
|
48
|
-
|
|
47
|
+
The installers do not check it, and GitHub CLI is not needed to install.
|
|
48
|
+
To verify it manually, pin the signer workflow so an attestation minted from any other
|
|
49
49
|
workflow path in this repository is rejected — provenance proves where a build
|
|
50
50
|
ran, not that the publishing step was the intended one:
|
|
51
51
|
|
|
@@ -80,8 +80,7 @@ or path breach before extraction (at most 10,000 members, 4,096-byte paths, and
|
|
|
80
80
|
256 MiB expanded files). Metadata is capped at 1 MiB, sidecars at 4 KiB,
|
|
81
81
|
archives/binaries at 64 MiB, and the Node adapter follows at most five redirects.
|
|
82
82
|
There is no raw, source-archive, tag, or default-branch acquisition fallback;
|
|
83
|
-
exact-release guarantees begin at the checksummed release `install.sh` asset
|
|
84
|
-
also passes `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`).
|
|
83
|
+
exact-release guarantees begin at the checksummed release `install.sh` asset.
|
|
85
84
|
The Go release boundary may acquire the latest stable candidate; the downloaded
|
|
86
85
|
engine then supplies local paths to the manifest-owned update core. Engine
|
|
87
86
|
`update --check` resolves release metadata but downloads no assets. `--to` and
|
|
@@ -16,7 +16,6 @@ import (
|
|
|
16
16
|
"net/netip"
|
|
17
17
|
"net/url"
|
|
18
18
|
"os"
|
|
19
|
-
"os/exec"
|
|
20
19
|
"path"
|
|
21
20
|
"path/filepath"
|
|
22
21
|
"regexp"
|
|
@@ -98,7 +97,7 @@ func Acquire(ctx context.Context, repository, tag string) (Candidate, func(), er
|
|
|
98
97
|
bundleName := "devrites-" + tag + ".tar.gz"
|
|
99
98
|
bundleURL := base + "/" + bundleName
|
|
100
99
|
bundlePath := filepath.Join(tmp, bundleName)
|
|
101
|
-
if err := downloadVerified(ctx, bundleURL, bundlePath, bundleName, 0o644
|
|
100
|
+
if err := downloadVerified(ctx, bundleURL, bundlePath, bundleName, 0o644); err != nil {
|
|
102
101
|
return fail(fmt.Errorf("download release bundle: %w", err))
|
|
103
102
|
}
|
|
104
103
|
source, err := extractBundle(bundlePath, tmp, tag)
|
|
@@ -108,7 +107,7 @@ func Acquire(ctx context.Context, repository, tag string) (Candidate, func(), er
|
|
|
108
107
|
|
|
109
108
|
engineURL := base + "/" + binaryName
|
|
110
109
|
enginePath := filepath.Join(tmp, binaryName)
|
|
111
|
-
if err := downloadVerified(ctx, engineURL, enginePath, binaryName, 0o755
|
|
110
|
+
if err := downloadVerified(ctx, engineURL, enginePath, binaryName, 0o755); err != nil {
|
|
112
111
|
return fail(fmt.Errorf("download engine binary: %w", err))
|
|
113
112
|
}
|
|
114
113
|
return Candidate{
|
|
@@ -185,7 +184,7 @@ func fetchJSON(ctx context.Context, rawURL string, out any) error {
|
|
|
185
184
|
return nil
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
func downloadVerified(ctx context.Context, rawURL, destination, filename string, mode fs.FileMode
|
|
187
|
+
func downloadVerified(ctx context.Context, rawURL, destination, filename string, mode fs.FileMode) error {
|
|
189
188
|
if err := download(ctx, rawURL, destination, maxAssetBytes, mode); err != nil {
|
|
190
189
|
return err
|
|
191
190
|
}
|
|
@@ -223,15 +222,6 @@ func downloadVerified(ctx context.Context, rawURL, destination, filename string,
|
|
|
223
222
|
if !strings.EqualFold(hex.EncodeToString(hash.Sum(nil)), hex.EncodeToString(want)) {
|
|
224
223
|
return fmt.Errorf("checksum mismatch for %s", filename)
|
|
225
224
|
}
|
|
226
|
-
signer := repository + "/.github/workflows/ci.yml@refs/heads/main"
|
|
227
|
-
// #nosec G204 -- fixed gh argv; repository is matched by repositoryPattern (leading alphanumeric, so never a flag) in Acquire and destination is an absolute path under the engine-created temp dir
|
|
228
|
-
cmd := exec.CommandContext(ctx, "gh", "attestation", "verify", destination, "--repo", repository, "--signer-workflow", signer)
|
|
229
|
-
if _, err := cmd.CombinedOutput(); err != nil {
|
|
230
|
-
if errors.Is(err, exec.ErrNotFound) {
|
|
231
|
-
return errors.New("gh (GitHub CLI) is required and must be authenticated for attestation verification")
|
|
232
|
-
}
|
|
233
|
-
return fmt.Errorf("attestation verification failed for %s: %w", filename, err)
|
|
234
|
-
}
|
|
235
225
|
return nil
|
|
236
226
|
}
|
|
237
227
|
|
|
@@ -33,7 +33,9 @@ func TestLatestAndAcquireVerifiedRelease(t *testing.T) {
|
|
|
33
33
|
bundleName: bundle,
|
|
34
34
|
binaryName: engine,
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
if runtime.GOOS == "windows" {
|
|
37
|
+
t.Skip("positive-control engine is a shell script")
|
|
38
|
+
}
|
|
37
39
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
38
40
|
if r.URL.Path == "/repos/owner/repo/releases/latest" {
|
|
39
41
|
_, _ = fmt.Fprintf(w, `{"tag_name":%q}`, tag)
|
|
@@ -81,12 +83,12 @@ func TestLatestAndAcquireVerifiedRelease(t *testing.T) {
|
|
|
81
83
|
cmd := exec.Command(candidate.EnginePath)
|
|
82
84
|
cmd.Env = append(os.Environ(), "MARKER="+marker)
|
|
83
85
|
if err := cmd.Run(); err == nil {
|
|
84
|
-
t.Fatal("
|
|
86
|
+
t.Fatal("acquired engine exited successfully; want positive-control exit 23")
|
|
85
87
|
} else if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 23 {
|
|
86
|
-
t.Fatalf("
|
|
88
|
+
t.Fatalf("acquired engine exit = %v, want 23", err)
|
|
87
89
|
}
|
|
88
90
|
if got, err := os.ReadFile(marker); err != nil || string(got) != "success" {
|
|
89
|
-
t.Fatalf("
|
|
91
|
+
t.Fatalf("acquired engine marker = %q, %v", got, err)
|
|
90
92
|
}
|
|
91
93
|
cleanup()
|
|
92
94
|
if _, err := os.Stat(root); !os.IsNotExist(err) {
|
|
@@ -111,199 +113,6 @@ func TestValidTag(t *testing.T) {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
func TestAcquireRejectsOriginSubstitution(t *testing.T) {
|
|
115
|
-
const tag = "v4.1.0"
|
|
116
|
-
trusted := testBundle(t, tag, map[string]string{
|
|
117
|
-
"package.json": `{"version":"4.1.0"}`,
|
|
118
|
-
"pack/generated/claude/skills/rite/SKILL.md": "rite\n",
|
|
119
|
-
})
|
|
120
|
-
attacker := testBundle(t, tag, map[string]string{
|
|
121
|
-
"package.json": `{"version":"4.1.0"}`,
|
|
122
|
-
"pack/generated/claude/skills/rite/SKILL.md": "attacker\n",
|
|
123
|
-
})
|
|
124
|
-
binaryName, err := platformBinaryName()
|
|
125
|
-
if err != nil {
|
|
126
|
-
t.Skip(err)
|
|
127
|
-
}
|
|
128
|
-
installAttestationStandIn(t, trusted, []byte("trusted engine"))
|
|
129
|
-
assets := map[string][]byte{"devrites-" + tag + ".tar.gz": attacker, binaryName: []byte("attacker engine")}
|
|
130
|
-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
131
|
-
name := filepath.Base(r.URL.Path)
|
|
132
|
-
assetName := strings.TrimSuffix(name, ".sha256")
|
|
133
|
-
asset, ok := assets[assetName]
|
|
134
|
-
if !ok {
|
|
135
|
-
http.NotFound(w, r)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
if strings.HasSuffix(name, ".sha256") {
|
|
139
|
-
_, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
_, _ = w.Write(asset)
|
|
143
|
-
}))
|
|
144
|
-
defer server.Close()
|
|
145
|
-
oldWeb := webBaseURL
|
|
146
|
-
webBaseURL = server.URL
|
|
147
|
-
t.Cleanup(func() { webBaseURL = oldWeb })
|
|
148
|
-
|
|
149
|
-
_, _, err = Acquire(context.Background(), "owner/repo", tag)
|
|
150
|
-
if err == nil || !strings.Contains(err.Error(), "attestation verification failed") {
|
|
151
|
-
t.Fatalf("Acquire error = %v, want attestation refusal despite a matching origin checksum", err)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
func TestAcquireRejectsSingleAssetSubstitution(t *testing.T) {
|
|
156
|
-
const tag = "v4.1.0"
|
|
157
|
-
files := map[string]string{
|
|
158
|
-
"package.json": `{"version":"4.1.0"}`,
|
|
159
|
-
"pack/generated/claude/skills/rite/SKILL.md": "rite\n",
|
|
160
|
-
}
|
|
161
|
-
trustedBundle := testBundle(t, tag, files)
|
|
162
|
-
files["pack/generated/claude/skills/rite/SKILL.md"] = "attacker\n"
|
|
163
|
-
attackerBundle := testBundle(t, tag, files)
|
|
164
|
-
trustedEngine, attackerEngine := []byte("trusted engine"), []byte("attacker engine")
|
|
165
|
-
binaryName, err := platformBinaryName()
|
|
166
|
-
if err != nil {
|
|
167
|
-
t.Skip(err)
|
|
168
|
-
}
|
|
169
|
-
for _, tc := range []struct {
|
|
170
|
-
name, refusedAsset string
|
|
171
|
-
bundle, engine []byte
|
|
172
|
-
}{
|
|
173
|
-
{"engine only", binaryName, trustedBundle, attackerEngine},
|
|
174
|
-
{"bundle only", "devrites-" + tag + ".tar.gz", attackerBundle, trustedEngine},
|
|
175
|
-
} {
|
|
176
|
-
t.Run(tc.name, func(t *testing.T) {
|
|
177
|
-
installAttestationStandIn(t, trustedBundle, trustedEngine)
|
|
178
|
-
assets := map[string][]byte{"devrites-" + tag + ".tar.gz": tc.bundle, binaryName: tc.engine}
|
|
179
|
-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
180
|
-
name := filepath.Base(r.URL.Path)
|
|
181
|
-
assetName := strings.TrimSuffix(name, ".sha256")
|
|
182
|
-
asset, ok := assets[assetName]
|
|
183
|
-
if !ok {
|
|
184
|
-
http.NotFound(w, r)
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
if strings.HasSuffix(name, ".sha256") {
|
|
188
|
-
_, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
_, _ = w.Write(asset)
|
|
192
|
-
}))
|
|
193
|
-
defer server.Close()
|
|
194
|
-
oldWeb := webBaseURL
|
|
195
|
-
webBaseURL = server.URL
|
|
196
|
-
t.Cleanup(func() { webBaseURL = oldWeb })
|
|
197
|
-
|
|
198
|
-
_, cleanup, err := Acquire(context.Background(), "owner/repo", tag)
|
|
199
|
-
if err == nil {
|
|
200
|
-
cleanup()
|
|
201
|
-
t.Fatal("Acquire accepted a release with one unattested asset")
|
|
202
|
-
}
|
|
203
|
-
want := "attestation verification failed for " + tc.refusedAsset
|
|
204
|
-
if !strings.Contains(err.Error(), want) {
|
|
205
|
-
t.Fatalf("Acquire error = %v, want %q", err, want)
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
func installAttestationStandIn(t *testing.T, trusted ...[]byte) {
|
|
212
|
-
t.Helper()
|
|
213
|
-
if runtime.GOOS == "windows" {
|
|
214
|
-
t.Skip("attestation stand-in and positive-control engine are shell scripts")
|
|
215
|
-
}
|
|
216
|
-
dir := t.TempDir()
|
|
217
|
-
var hashes strings.Builder
|
|
218
|
-
for _, data := range trusted {
|
|
219
|
-
fmt.Fprintf(&hashes, "%x\n", sha256.Sum256(data))
|
|
220
|
-
}
|
|
221
|
-
allowed := filepath.Join(dir, "allowed-digests")
|
|
222
|
-
if err := os.WriteFile(allowed, []byte(hashes.String()), 0o600); err != nil {
|
|
223
|
-
t.Fatal(err)
|
|
224
|
-
}
|
|
225
|
-
gh := filepath.Join(dir, "gh")
|
|
226
|
-
script := `#!/bin/sh
|
|
227
|
-
[ "$1" = attestation ] && [ "$2" = verify ] && [ "$4" = --repo ] && [ "$5" = owner/repo ] && [ "$6" = --signer-workflow ] && [ "$7" = owner/repo/.github/workflows/ci.yml@refs/heads/main ] || exit 64
|
|
228
|
-
got=$(shasum -a 256 "$3" | awk '{print $1}') || exit 65
|
|
229
|
-
grep -Fxq "$got" "$ATTESTED_DIGESTS"
|
|
230
|
-
`
|
|
231
|
-
if err := os.WriteFile(gh, []byte(script), 0o700); err != nil {
|
|
232
|
-
t.Fatal(err)
|
|
233
|
-
}
|
|
234
|
-
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
235
|
-
t.Setenv("ATTESTED_DIGESTS", allowed)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
func TestAttestationStandInIsStrict(t *testing.T) {
|
|
239
|
-
trusted := []byte("trusted engine")
|
|
240
|
-
installAttestationStandIn(t, trusted, []byte("other"))
|
|
241
|
-
dir := t.TempDir()
|
|
242
|
-
good := filepath.Join(dir, "good")
|
|
243
|
-
bad := filepath.Join(dir, "bad")
|
|
244
|
-
if err := os.WriteFile(good, trusted, 0o600); err != nil {
|
|
245
|
-
t.Fatal(err)
|
|
246
|
-
}
|
|
247
|
-
if err := os.WriteFile(bad, []byte("attacker engine"), 0o600); err != nil {
|
|
248
|
-
t.Fatal(err)
|
|
249
|
-
}
|
|
250
|
-
const signer = "owner/repo/.github/workflows/ci.yml@refs/heads/main"
|
|
251
|
-
run := func(file, repo, workflow string) error {
|
|
252
|
-
return exec.Command("gh", "attestation", "verify", file, "--repo", repo, "--signer-workflow", workflow).Run()
|
|
253
|
-
}
|
|
254
|
-
if err := run(good, "owner/repo", signer); err != nil {
|
|
255
|
-
t.Fatalf("stand-in rejected the pinned request: %v", err)
|
|
256
|
-
}
|
|
257
|
-
if run(good, "other/repo", signer) == nil {
|
|
258
|
-
t.Error("stand-in accepted a wrong repo")
|
|
259
|
-
}
|
|
260
|
-
if run(good, "owner/repo", "owner/repo/.github/workflows/other.yml@refs/heads/main") == nil {
|
|
261
|
-
t.Error("stand-in accepted a wrong signer workflow")
|
|
262
|
-
}
|
|
263
|
-
if run(bad, "owner/repo", signer) == nil {
|
|
264
|
-
t.Error("stand-in accepted an unattested digest")
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
func TestAcquireRejectsMissingAttestationVerifier(t *testing.T) {
|
|
269
|
-
const tag = "v4.1.0"
|
|
270
|
-
binaryName, err := platformBinaryName()
|
|
271
|
-
if err != nil {
|
|
272
|
-
t.Skip(err)
|
|
273
|
-
}
|
|
274
|
-
assets := map[string][]byte{
|
|
275
|
-
"devrites-" + tag + ".tar.gz": testBundle(t, tag, map[string]string{
|
|
276
|
-
"package.json": `{"version":"4.1.0"}`,
|
|
277
|
-
"pack/generated/claude/skills/rite/SKILL.md": "rite\n",
|
|
278
|
-
}),
|
|
279
|
-
binaryName: []byte("engine"),
|
|
280
|
-
}
|
|
281
|
-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
282
|
-
name := filepath.Base(r.URL.Path)
|
|
283
|
-
assetName := strings.TrimSuffix(name, ".sha256")
|
|
284
|
-
asset, ok := assets[assetName]
|
|
285
|
-
if !ok {
|
|
286
|
-
http.NotFound(w, r)
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
if strings.HasSuffix(name, ".sha256") {
|
|
290
|
-
_, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
_, _ = w.Write(asset)
|
|
294
|
-
}))
|
|
295
|
-
defer server.Close()
|
|
296
|
-
oldWeb := webBaseURL
|
|
297
|
-
webBaseURL = server.URL
|
|
298
|
-
t.Cleanup(func() { webBaseURL = oldWeb })
|
|
299
|
-
t.Setenv("PATH", t.TempDir())
|
|
300
|
-
|
|
301
|
-
_, _, err = Acquire(context.Background(), "owner/repo", tag)
|
|
302
|
-
if err == nil || !strings.Contains(err.Error(), "gh (GitHub CLI) is required") {
|
|
303
|
-
t.Fatalf("Acquire error = %v, want refusal when gh is not installed", err)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
116
|
func TestAcquireRejectsChecksumMismatch(t *testing.T) {
|
|
308
117
|
const tag = "v4.1.0"
|
|
309
118
|
bundleName := "devrites-" + tag + ".tar.gz"
|
package/install.sh
CHANGED
|
@@ -13,8 +13,6 @@ DEVRITES_REPO="${DEVRITES_REPO:-ViktorsBaikers/DevRites}"
|
|
|
13
13
|
DEVRITES_REF="${DEVRITES_REF:-}"
|
|
14
14
|
|
|
15
15
|
BOOTSTRAP_DIR=""
|
|
16
|
-
RUN_JOB=""
|
|
17
|
-
RUN_WATCH=""
|
|
18
16
|
BOOTSTRAP_MAX_METADATA=1048576
|
|
19
17
|
BOOTSTRAP_MAX_SIDECAR=4096
|
|
20
18
|
BOOTSTRAP_MAX_ARCHIVE=67108864
|
|
@@ -140,114 +138,6 @@ verify_sha256() {
|
|
|
140
138
|
[ -n "$want" ] && [ -n "$got" ] && [ "$got" = "$want" ]
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
# Runs a command for at most $1 seconds with output discarded. Past the limit it
|
|
144
|
-
# is sent SIGTERM and, if it is still alive two seconds later, SIGKILL. A command
|
|
145
|
-
# stopped this way sets RUN_TIMED_OUT=1 and returns 124; any other failure keeps
|
|
146
|
-
# the command's own status, so a crash is never mistaken for a timeout. Same
|
|
147
|
-
# watchdog idea as scripts/install-lib.sh dr_run_bounded, which is not available
|
|
148
|
-
# until the bundle is verified and extracted. The timeout marker lives in
|
|
149
|
-
# $BOOTSTRAP_DIR, which the EXIT trap removes, so a run without that directory
|
|
150
|
-
# fails closed; stop_bounded ends a command and watchdog still in flight when the
|
|
151
|
-
# bootstrap is interrupted.
|
|
152
|
-
run_bounded() {
|
|
153
|
-
limit="$1"; shift
|
|
154
|
-
RUN_TIMED_OUT=0
|
|
155
|
-
[ -n "$BOOTSTRAP_DIR" ] || return 125
|
|
156
|
-
fired="$(mktemp "$BOOTSTRAP_DIR/bound.XXXXXX" 2>/dev/null)" || return 125
|
|
157
|
-
"$@" >/dev/null 2>&1 </dev/null &
|
|
158
|
-
job=$!
|
|
159
|
-
RUN_JOB=$job
|
|
160
|
-
{
|
|
161
|
-
trap 'kill "$nap"; exit 0' TERM
|
|
162
|
-
sleep "$limit" & nap=$!; wait "$nap"
|
|
163
|
-
kill -0 "$job" || exit 0
|
|
164
|
-
echo 1 >"$fired"
|
|
165
|
-
kill "$job"
|
|
166
|
-
sleep 2 & nap=$!; wait "$nap"
|
|
167
|
-
kill -9 "$job"
|
|
168
|
-
} >/dev/null 2>&1 </dev/null &
|
|
169
|
-
watch=$!
|
|
170
|
-
RUN_WATCH=$watch
|
|
171
|
-
wait "$job" 2>/dev/null
|
|
172
|
-
rc=$?
|
|
173
|
-
kill "$watch" >/dev/null 2>&1
|
|
174
|
-
wait "$watch" 2>/dev/null
|
|
175
|
-
RUN_JOB=""; RUN_WATCH=""
|
|
176
|
-
if [ "$rc" -ne 0 ] && [ -s "$fired" ]; then
|
|
177
|
-
RUN_TIMED_OUT=1
|
|
178
|
-
rc=124
|
|
179
|
-
fi
|
|
180
|
-
rm -f "$fired"
|
|
181
|
-
return "$rc"
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
stop_bounded() {
|
|
185
|
-
# A signal can land after a job is forked but before its PID is recorded, so
|
|
186
|
-
# every background job is ended, not only the recorded ones. run_bounded is the
|
|
187
|
-
# only place this script backgrounds anything.
|
|
188
|
-
for pid in $(jobs -p); do
|
|
189
|
-
[ "$pid" = "$RUN_WATCH" ] || kill -9 "$pid" >/dev/null 2>&1
|
|
190
|
-
done
|
|
191
|
-
[ -z "$RUN_WATCH" ] || kill "$RUN_WATCH" >/dev/null 2>&1
|
|
192
|
-
RUN_JOB=""; RUN_WATCH=""
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
# The checksum sidecar above travels with the tarball it vouches for: it is
|
|
196
|
-
# fetched from "$url.sha256", one suffix away from the artifact's own origin. An
|
|
197
|
-
# attacker who substitutes the release origin therefore supplies both the
|
|
198
|
-
# payload and a digest that matches it, and the digest check passes. The
|
|
199
|
-
# release job already mints the proof that does not travel with the artifact --
|
|
200
|
-
# a build-provenance attestation signed by that job's OIDC identity and bound to
|
|
201
|
-
# the artifact digest (.github/workflows/ci.yml, 'Attest release artifacts').
|
|
202
|
-
# Verify it here, pinned to the repository and to the signer workflow, so an
|
|
203
|
-
# attestation minted by any other workflow in this repository is rejected too.
|
|
204
|
-
#
|
|
205
|
-
# There is deliberately no bypass for a missing verifier: without gh there is no
|
|
206
|
-
# proof of origin, and the sidecar is not a substitute for one.
|
|
207
|
-
#
|
|
208
|
-
# The lookup is bounded: DEVRITES_ATTEST_TIMEOUT is a whole number of seconds
|
|
209
|
-
# (1 to 99999, default 120). Anything else is ignored with a warning and the
|
|
210
|
-
# default applies, so a bad value can never remove the bound. The follow-up
|
|
211
|
-
# `gh auth status` is bounded by 30 seconds or that limit, whichever is shorter.
|
|
212
|
-
# A timeout, a crash and a plain rejection each fail closed and are reported as
|
|
213
|
-
# what they are.
|
|
214
|
-
verify_attestation() {
|
|
215
|
-
file="$1"
|
|
216
|
-
signer="$DEVRITES_REPO/.github/workflows/ci.yml@refs/heads/main"
|
|
217
|
-
ATTESTATION_FAILURE=""
|
|
218
|
-
if ! command -v gh >/dev/null 2>&1; then
|
|
219
|
-
ATTESTATION_FAILURE="attestation verification failed: gh (GitHub CLI) is required and was not found"
|
|
220
|
-
return 1
|
|
221
|
-
fi
|
|
222
|
-
attest_limit=120
|
|
223
|
-
case "${DEVRITES_ATTEST_TIMEOUT:-}" in
|
|
224
|
-
"") ;;
|
|
225
|
-
*[!0-9]* | 0* | [0-9][0-9][0-9][0-9][0-9][0-9]*)
|
|
226
|
-
echo "warning: ignoring invalid DEVRITES_ATTEST_TIMEOUT '$DEVRITES_ATTEST_TIMEOUT' (expected whole seconds, 1 to 99999); using $attest_limit" >&2
|
|
227
|
-
;;
|
|
228
|
-
*) attest_limit="$DEVRITES_ATTEST_TIMEOUT" ;;
|
|
229
|
-
esac
|
|
230
|
-
auth_limit=30
|
|
231
|
-
[ "$attest_limit" -lt "$auth_limit" ] && auth_limit="$attest_limit"
|
|
232
|
-
verify_rc=0
|
|
233
|
-
run_bounded "$attest_limit" gh attestation verify "$file" --repo "$DEVRITES_REPO" \
|
|
234
|
-
--signer-workflow "$signer" || verify_rc=$?
|
|
235
|
-
if [ "$verify_rc" -ne 0 ]; then
|
|
236
|
-
if [ "$RUN_TIMED_OUT" -eq 1 ]; then
|
|
237
|
-
ATTESTATION_FAILURE="attestation verification timed out after ${attest_limit}s; check connectivity or raise DEVRITES_ATTEST_TIMEOUT and retry"
|
|
238
|
-
elif [ "$verify_rc" -ge 128 ]; then
|
|
239
|
-
ATTESTATION_FAILURE="attestation verification failed: gh exited with status $verify_rc"
|
|
240
|
-
elif run_bounded "$auth_limit" gh auth status; then
|
|
241
|
-
ATTESTATION_FAILURE="attestation verification failed: no build provenance from $signer"
|
|
242
|
-
elif [ "$RUN_TIMED_OUT" -eq 1 ]; then
|
|
243
|
-
ATTESTATION_FAILURE="attestation verification timed out: gh auth status did not answer within ${auth_limit}s; check connectivity or raise DEVRITES_ATTEST_TIMEOUT and retry"
|
|
244
|
-
else
|
|
245
|
-
ATTESTATION_FAILURE="attestation verification failed: gh is not authenticated (attestation lookup requires a GitHub login); run 'gh auth login' and retry"
|
|
246
|
-
fi
|
|
247
|
-
return 1
|
|
248
|
-
fi
|
|
249
|
-
}
|
|
250
|
-
|
|
251
141
|
preflight_archive() {
|
|
252
142
|
archive_path="$1"
|
|
253
143
|
tag="$2"
|
|
@@ -324,7 +214,7 @@ bootstrap_bundle() {
|
|
|
324
214
|
exit 1
|
|
325
215
|
}
|
|
326
216
|
umask "$old_umask"
|
|
327
|
-
trap '
|
|
217
|
+
trap 'exit 1' HUP INT TERM
|
|
328
218
|
trap 'rm -rf "$BOOTSTRAP_DIR"' EXIT
|
|
329
219
|
if [ -n "$DEVRITES_REF" ]; then
|
|
330
220
|
tag="$(normalize_release_tag "$DEVRITES_REF")" || {
|
|
@@ -361,11 +251,6 @@ bootstrap_bundle() {
|
|
|
361
251
|
echo "error: release $tag asset $asset: checksum failed." >&2
|
|
362
252
|
exit 1
|
|
363
253
|
}
|
|
364
|
-
verify_attestation "$archive" || {
|
|
365
|
-
rm -f "$archive" "$sidecar"
|
|
366
|
-
echo "error: release $tag asset $asset: ${ATTESTATION_FAILURE:-attestation verification failed}." >&2
|
|
367
|
-
exit 1
|
|
368
|
-
}
|
|
369
254
|
uncompressed="$BOOTSTRAP_DIR/devrites-$tag.tar"
|
|
370
255
|
bounded_decompress "$archive" "$uncompressed" "$BOOTSTRAP_MAX_UNCOMPRESSED" || {
|
|
371
256
|
rm -f "$archive" "$sidecar"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code, Codex, omp, pi, and Devin CLI",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|
package/scripts/install-lib.sh
CHANGED
|
@@ -100,20 +100,6 @@ dr_verify_checksum() {
|
|
|
100
100
|
[ "$_dr_got" = "$_dr_want" ]
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
# Runs a command for at most $1 seconds with output discarded; a timeout is a failure.
|
|
104
|
-
dr_run_bounded() {
|
|
105
|
-
_dr_limit="$1"; shift
|
|
106
|
-
"$@" >/dev/null 2>&1 </dev/null &
|
|
107
|
-
_dr_job=$!
|
|
108
|
-
{ trap 'kill "$_dr_nap"; exit 0' TERM; sleep "$_dr_limit" & _dr_nap=$!; wait "$_dr_nap" && kill "$_dr_job"; } >/dev/null 2>&1 </dev/null &
|
|
109
|
-
_dr_watch=$!
|
|
110
|
-
wait "$_dr_job" 2>/dev/null
|
|
111
|
-
_dr_rc=$?
|
|
112
|
-
kill "$_dr_watch" >/dev/null 2>&1
|
|
113
|
-
wait "$_dr_watch" 2>/dev/null
|
|
114
|
-
return "$_dr_rc"
|
|
115
|
-
}
|
|
116
|
-
|
|
117
103
|
dr_download_engine() {
|
|
118
104
|
_dr_source_dir="$1"
|
|
119
105
|
_dr_repo="$2"
|
|
@@ -138,8 +124,6 @@ dr_download_engine() {
|
|
|
138
124
|
dr_bounded_download "$_dr_url" "$_dr_engine_out" 67108864 || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: ${DR_DOWNLOAD_FAILURE:-download} failed"; return 1; }
|
|
139
125
|
dr_bounded_download "$_dr_url.sha256" "$_dr_engine_out.sha256" 4096 || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset.sha256: ${DR_DOWNLOAD_FAILURE:-download} failed"; rm -f "$_dr_engine_out"; return 1; }
|
|
140
126
|
dr_verify_checksum "$_dr_engine_out" "$_dr_engine_out.sha256" "$_dr_asset" || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: checksum failed"; rm -f "$_dr_engine_out" "$_dr_engine_out.sha256"; return 1; }
|
|
141
|
-
_dr_signer="$_dr_repo/.github/workflows/ci.yml@refs/heads/main"
|
|
142
|
-
command -v gh >/dev/null 2>&1 && dr_run_bounded "${DR_ATTEST_TIMEOUT:-120}" gh attestation verify "$_dr_engine_out" --repo "$_dr_repo" --signer-workflow "$_dr_signer" || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: attestation verification failed (gh must be installed and authenticated)"; rm -f "$_dr_engine_out" "$_dr_engine_out.sha256"; return 1; }
|
|
143
127
|
chmod +x "$_dr_engine_out" || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: executable preparation failed"; rm -f "$_dr_engine_out" "$_dr_engine_out.sha256"; return 1; }
|
|
144
128
|
}
|
|
145
129
|
|