diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af7598..39ac8ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and versions are tracked in the repo-root `VERSION` file. documented context instead of silently accepting and discarding them. - Isolated application run status per model while retaining the process-global last-status value as a compatibility view of the most recently active model. +- Added a manifest-bound lock to standalone applications' embedded vendor copy + so it passes the same offline vendor verification as ordinary installs. - Prevented list, CLI, and application call paths from creating or overwriting caller-visible variables through undeclared internal scratch assignments. - Eliminated an intermittent macOS Bash process-group race in supervised diff --git a/docs/vendor-workflow.md b/docs/vendor-workflow.md index d367cff..1fae75d 100644 --- a/docs/vendor-workflow.md +++ b/docs/vendor-workflow.md @@ -28,8 +28,18 @@ scripts/vendor standalone . /tmp/base-bash-libs-v2 dist/app PATH="$PWD/dist/app/bin:$PATH" dist/app/bin/app --help ``` -The standalone payload contains the verified launcher and framework under its -own root plus an auditable vendor copy and `BASE_BASH_STANDALONE.release`. The -launcher resolves its colocated `lib/bash` tree, so runtime network access and -ambient `BASE_BASH_LIBS_DIR` are unnecessary. No command downloads, executes, -or evaluates remote content. +The standalone payload contains two deterministic copies of the same verified +framework bundle. The root copy is the authoritative runtime layout and is +bound by `BASE_BASH_STANDALONE.release`; the launcher resolves its colocated +`lib/bash` tree without ambient `BASE_BASH_LIBS_DIR`. The +`vendor/base-bash-libs` copy is the authoritative audit/vendor layout and has +its own `base-bash-libs.lock`, so consumers can verify it independently: + +```bash +scripts/vendor verify dist/app/vendor/base-bash-libs +``` + +Both copies carry the same `MANIFEST.sha256`, version, and source commit from +the input bundle. Standalone creation stages the complete payload and its lock +before one atomic move. No command downloads, executes, or evaluates remote +content. diff --git a/scripts/vendor b/scripts/vendor index 5db5abb..01105f8 100755 --- a/scripts/vendor +++ b/scripts/vendor @@ -156,8 +156,10 @@ rollback_bundle() { } verify_destination() { - local destination="$1" expected path actual lock_hash - [[ -f "$destination/base-bash-libs.lock" && -f "$destination/MANIFEST.sha256" ]] || { + local destination="$1" expected path actual lock_hash lock_version lock_commit + local bundle_version bundle_commit + [[ -f "$destination/base-bash-libs.lock" && -f "$destination/MANIFEST.sha256" && + -f "$destination/BUNDLE.release" ]] || { error "vendor destination lacks lock or hash metadata: $destination" return 1 } @@ -178,6 +180,18 @@ verify_destination() { error 'vendor lock does not match MANIFEST.sha256' return 1 } + lock_version="$(sed -n 's/^version=//p' "$destination/base-bash-libs.lock" | sed -n '1p')" + lock_commit="$(sed -n 's/^source_commit=//p' "$destination/base-bash-libs.lock" | sed -n '1p')" + bundle_version="$(sed -n 's/^source_version=//p' "$destination/BUNDLE.release" | sed -n '1p')" + bundle_commit="$(sed -n 's/^source_commit=//p' "$destination/BUNDLE.release" | sed -n '1p')" + [[ -n "$lock_version" && "$lock_version" == "$bundle_version" ]] || { + error 'vendor lock version does not match BUNDLE.release' + return 1 + } + [[ -n "$lock_commit" && "$lock_commit" == "$bundle_commit" ]] || { + error 'vendor lock source commit does not match BUNDLE.release' + return 1 + } printf 'Vendor lock and hashes are valid: %s\n' "$destination" } @@ -202,10 +216,11 @@ standalone_bundle() { fi # Put the verified framework at the standalone root so the copied # launcher resolves lib/bash without ambient environment variables, and - # retain an explicit vendor copy for provenance/audit consumers. + # retain an independently locked vendor copy for provenance/audit consumers. if ! copy_tree "$framework_bundle" "$temporary" || ! mkdir -p "$temporary/vendor/base-bash-libs" "$temporary/bin" || ! copy_tree "$framework_bundle" "$temporary/vendor/base-bash-libs" || + ! write_lock "$temporary/vendor/base-bash-libs" "$framework_bundle" standalone || ! cp -- "$framework_bundle/bin/base-bash" "$temporary/bin/base-bash" || ! chmod +x "$temporary/bin/base-bash" "$temporary/bin/app"; then rm -rf -- "$temporary" diff --git a/tests/vendor.bats b/tests/vendor.bats index d86b36b..37d0e6b 100644 --- a/tests/vendor.bats +++ b/tests/vendor.bats @@ -48,7 +48,14 @@ setup() { [ "$status" -eq 0 ] [ -x "$standalone/bin/base-bash" ] [ -x "$standalone/bin/app" ] - [ -f "$standalone/vendor/base-bash-libs/base-bash-libs.lock" ] || true + [ -f "$standalone/vendor/base-bash-libs/base-bash-libs.lock" ] + bats_run "$BASE_REPO_ROOT/scripts/vendor" verify "$standalone/vendor/base-bash-libs" + [ "$status" -eq 0 ] + [[ "$output" == *"Vendor lock and hashes are valid"* ]] + [ "$(sed -n 's/^version=//p' "$standalone/vendor/base-bash-libs/base-bash-libs.lock")" = \ + "$(sed -n 's/^source_version=//p' "$standalone/vendor/base-bash-libs/BUNDLE.release")" ] + [ "$(sed -n 's/^source_commit=//p' "$standalone/vendor/base-bash-libs/base-bash-libs.lock")" = \ + "$(sed -n 's/^source_commit=//p' "$standalone/vendor/base-bash-libs/BUNDLE.release")" ] bats_run env PATH="$standalone/bin:$PATH" "$standalone/bin/app" run [ "$status" -eq 0 ] [[ "$output" == *"hello=world"* ]]