Establish public verified release boundary

This commit is contained in:
tmk241
2026-08-10 23:31:23 +02:00
parent 39f1966536
commit 2c957b94f8
5 changed files with 351 additions and 2 deletions
+32
View File
@@ -0,0 +1,32 @@
# AGENTS.md
`ink-releases` is the public, source-free distribution boundary for private Ink repositories.
## Authority
- Read `REQUIREMENTS.md` before durable changes.
- `REQUIREMENTS.md` is authoritative; generated artifacts and private source repositories are evidence.
- Update requirements before changing durable installer, publication, integrity, or platform behavior.
- Keep strict Redgate proof current. `req:` is traceability; accepted local `test` edges are proof.
## Boundaries
- Never copy private source, credentials, deploy keys, policy, profiles, or configuration here.
- `ink` publication owns only `install.sh`, `channels/*/ink`, and `releases/*/ink`.
- `ink-toolset` publication owns only `channels/*/toolset` and `releases/*/toolset`.
- Published executables are individual static x86_64 Linux-musl assets; do not add bundles.
- Installation writes executable files only. Never edit PATH, shell configuration, policy, or credentials.
## Work tracking
Work is tracked in Gitea Issues for `tmk241/ink-releases`. Do not mirror issues into local files. Do not create, close, or relabel issues without explicit user authority.
## Verification
```sh
bash -n install.sh
python3 test/installer_e2e.py
redgate lint < requirements.tsv
```
Final reports must include `REQUIREMENT IMPACT: IDs` or `REQUIREMENT IMPACT: none`.
+36 -2
View File
@@ -1,3 +1,37 @@
# ink-releases
# Ink Releases
Public verified static release assets for Ink and Ink Toolset
Public, source-free distribution for private `ink` and `ink-toolset` repositories.
## Install
Inspect the installer, then run it. A noninteractive invocation installs Ink
only. With a terminal it offers a compact native set menu.
```sh
curl -fsSL https://git.tmk241.com/tmk241/ink-releases/raw/branch/main/install.sh | less
curl -fsSL https://git.tmk241.com/tmk241/ink-releases/raw/branch/main/install.sh | sh
curl -fsSL https://git.tmk241.com/tmk241/ink-releases/raw/branch/main/install.sh | sh -s -- --set text --set web -y
```
The installer verifies SHA-256 before installing. It writes executable files
under `~/.ink/bin` only and never edits PATH, shell configuration, Ink policy,
or credentials.
## Layout
```text
install.sh
channels/main/ink/
channels/main/toolset/
releases/VERSION/ink/
releases/VERSION/toolset/
```
Each executable is an individual static x86_64 Linux-musl asset. There are no
bundles: selecting one tool downloads one tool.
## Provenance
The private source workflows build, test, inspect, run on Void Linux musl, and
then publish only artifacts, checksums, and manifests. Each source owns its own
subtree. This repository contains no private source or publisher credential.
+28
View File
@@ -0,0 +1,28 @@
# Requirements
## distribution
001 The repository must remain public and contain only installers, manifests, checksums, and built release artifacts derived from private source repositories.
002 The stable `channels/main/ink` path must contain the latest verified Ink core asset and checksum published from the private Ink main branch.
003 The stable toolset channel must contain verified individual assets, a set manifest, and checksums from the private main branch.
004 A source release tag must publish the same source-owned files under `releases/TAG/ink` or `releases/TAG/toolset` without modifying the other source subtree.
005 Every executable artifact must be a static x86_64 Linux-musl executable and must pass `--help` inside Void Linux musl before publication.
006 Every published manifest must identify its source repository, source commit, target, executable, set, byte count, digest, and asset path.
007 Publication must fail closed before changing this repository when build, test, static-artifact inspection, checksum, or Void Linux runtime proof fails.
008 This repository must never contain source-repository credentials, deploy keys, private source, Ink policy, shell configuration, or user credentials.
## installer
001 The root `install.sh` must install Ink and only explicitly selected tool assets from public repository paths after SHA-256 verification.
002 Without a terminal or selection flags the installer must install Ink only; with a terminal it may present a compact native text menu.
003 The installer must support exact tool and named set selection without downloading unselected executable assets.
004 The installer must write only selected-prefix executables and must not edit PATH, shell configuration, policy, or credentials.
005 Installer help must completely document options, environment, effects, defaults, examples, and platform support.
006 The installer must support the stable main channel and immutable matching source-release tags.
## governance
001 `REQUIREMENTS.md` must remain authoritative; code, workflows, tests, runtime, and source repositories are evidence.
002 Durable behavior changes must update requirements before implementation and proof.
003 `req:` references must provide traceability; only accepted local executable `test` edges satisfy proof coverage.
004 Work is tracked in Gitea Issues for `tmk241/ink-releases`; agents must not mirror issues locally or mutate tracker state without explicit authority.
Executable
+175
View File
@@ -0,0 +1,175 @@
#!/bin/sh
set -eu
INK_VERSION=${INK_VERSION:-main}
TOOLSET_VERSION=${TOOLSET_VERSION:-main}
PREFIX=${INK_PREFIX:-$HOME/.ink}
RELEASE_BASE=${INK_RELEASE_BASE:-https://git.tmk241.com/tmk241/ink-releases/raw/branch/main}
TARGET=x86_64-linux-musl
SETS=
TOOLS=
ASSUME_YES=0
LIST_ONLY=0
usage() {
cat <<'HELP'
Usage: install.sh [OPTIONS]
Install the static Ink core and optionally selected permission-sized tools.
With a terminal and no selection flags, a small set menu is shown. Without a
terminal, the safe default installs only Ink.
Options:
--set NAME Add text, filesystem, web, git, or all (repeatable)
--tool NAME Add one exact executable (repeatable)
--version VERSION Use immutable VERSION for Ink and Ink Toolset
--ink-version VERSION Select main or an immutable Ink release
--toolset-version VER Select main or an immutable Toolset release
--prefix DIR Install executable files under DIR/bin
--list List available tool assets; do not install
-y, --yes Do not ask for confirmation
-h, --help Show this help
Environment:
INK_VERSION, TOOLSET_VERSION, INK_PREFIX (versions default to main)
INK_RELEASE_BASE (public ink-releases raw root)
Examples:
curl -fsSL URL/install.sh | sh
curl -fsSL URL/install.sh | sh -s -- --set text --set web -y
curl -fsSL URL/install.sh | sh -s -- --tool lines --tool httpget -y
Effects:
Writes executable files only under PREFIX/bin. It never edits PATH, shell
configuration, Ink policy, credentials, or unrelated state.
HELP
}
append_word() {
if [ -z "$1" ]; then printf '%s' "$2"; else printf '%s %s' "$1" "$2"; fi
}
while [ $# -gt 0 ]; do
case $1 in
--set) [ $# -ge 2 ] || { echo 'install: --set needs a value' >&2; exit 2; }; SETS=$(append_word "$SETS" "$2"); shift 2 ;;
--tool) [ $# -ge 2 ] || { echo 'install: --tool needs a value' >&2; exit 2; }; TOOLS=$(append_word "$TOOLS" "$2"); shift 2 ;;
--version) [ $# -ge 2 ] || { echo 'install: --version needs a value' >&2; exit 2; }; INK_VERSION=$2; TOOLSET_VERSION=$2; shift 2 ;;
--ink-version) [ $# -ge 2 ] || { echo 'install: --ink-version needs a value' >&2; exit 2; }; INK_VERSION=$2; shift 2 ;;
--toolset-version) [ $# -ge 2 ] || { echo 'install: --toolset-version needs a value' >&2; exit 2; }; TOOLSET_VERSION=$2; shift 2 ;;
--prefix) [ $# -ge 2 ] || { echo 'install: --prefix needs a value' >&2; exit 2; }; PREFIX=$2; shift 2 ;;
--list) LIST_ONLY=1; shift ;;
-y|--yes) ASSUME_YES=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
*) echo "install: unknown option: $1" >&2; exit 2 ;;
esac
done
[ $# -eq 0 ] || { echo 'install: trailing arguments' >&2; exit 2; }
case $(uname -m) in x86_64|amd64) ;; *) echo 'install: this release supports x86_64 Linux only' >&2; exit 1 ;; esac
case $PREFIX in ''|/) echo 'install: unsafe empty/root prefix' >&2; exit 2 ;; esac
TTY=0
if [ -r /dev/tty ] && [ -w /dev/tty ]; then TTY=1; fi
if [ "$TTY" -eq 1 ]; then
cyan='\033[36m'; bold='\033[1m'; dim='\033[2m'; reset='\033[0m'
else
cyan= bold= dim= reset=
fi
say() { printf '%s\n' "$*" >&2; }
fetch() {
url=$1 destination=$2
if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o "$destination"
elif command -v wget >/dev/null 2>&1; then wget -qO "$destination" "$url"
else echo 'install: curl or wget is required' >&2; exit 1
fi
}
digest() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'
else echo 'install: sha256sum or shasum is required' >&2; exit 1
fi
}
if [ -z "$SETS$TOOLS" ] && [ "$TTY" -eq 1 ] && [ "$LIST_ONLY" -eq 0 ]; then
printf '%bInk%b %bsmall tools, explicit authority%b\n\n' "$bold$cyan" "$reset" "$dim" "$reset" >/dev/tty
printf ' 0 Ink only\n 1 text\n 2 filesystem\n 3 web\n 4 git\n 5 all tools\n\nSelect sets [0]: ' >/dev/tty
IFS= read -r answer </dev/tty || answer=0
for choice in $answer; do
case $choice in 0|'') ;; 1) SETS=$(append_word "$SETS" text) ;; 2) SETS=$(append_word "$SETS" filesystem) ;; 3) SETS=$(append_word "$SETS" web) ;; 4) SETS=$(append_word "$SETS" git) ;; 5) SETS=all ;; *) echo "install: invalid selection: $choice" >&2; exit 2 ;; esac
done
fi
work=${TMPDIR:-/tmp}/ink-install.$$
trap 'rm -rf "$work"' EXIT HUP INT TERM
mkdir -p "$work"
release_path() {
version=$1 kind=$2
if [ "$version" = main ]; then printf 'channels/main/%s' "$kind"
else printf 'releases/%s/%s' "$version" "$kind"
fi
}
toolset_url=$RELEASE_BASE/$(release_path "$TOOLSET_VERSION" toolset)
manifest_name=ink-toolset-$TOOLSET_VERSION-$TARGET.manifest.tsv
if [ -n "$SETS$TOOLS" ] || [ "$LIST_ONLY" -eq 1 ]; then
fetch "$toolset_url/$manifest_name" "$work/$manifest_name"
fetch "$toolset_url/ink-toolset-SHA256SUMS" "$work/toolset-SHA256SUMS"
expected=$(awk -v file="$manifest_name" '$2 == file || $2 == "*" file {print $1; exit}' "$work/toolset-SHA256SUMS")
[ -n "$expected" ] && [ "$(digest "$work/$manifest_name")" = "$expected" ] || { echo 'install: toolset manifest checksum mismatch' >&2; exit 1; }
fi
if [ "$LIST_ONLY" -eq 1 ]; then
cat "$work/$manifest_name"
exit 0
fi
selected=$work/selected.tsv
: > "$selected"
if [ -n "$SETS" ]; then
awk -F '\t' -v sets="$SETS" 'NR > 1 { n=split(sets,a," "); for(i=1;i<=n;i++) if(a[i]=="all" || $4==a[i]) {print; break} }' "$work/$manifest_name" >> "$selected"
fi
for tool in $TOOLS; do
row=$(awk -F '\t' -v tool="$tool" 'NR > 1 && $3==tool {print; exit}' "$work/$manifest_name")
[ -n "$row" ] || { echo "install: unknown tool: $tool" >&2; exit 2; }
printf '%s\n' "$row" >> "$selected"
done
sort -u "$selected" -o "$selected"
say "${cyan}Ink installer${reset}"
say " core $INK_VERSION"
say " tools $TOOLSET_VERSION ($(wc -l < "$selected" | tr -d ' ') selected)"
say " target $TARGET"
say " location $PREFIX/bin"
if [ "$ASSUME_YES" -eq 0 ] && [ "$TTY" -eq 1 ]; then
printf 'Install? [y/N] ' >/dev/tty
IFS= read -r answer </dev/tty || answer=n
case $answer in y|Y|yes|YES) ;; *) say 'install: cancelled'; exit 1 ;; esac
fi
ink_asset=ink-$TARGET
ink_url=$RELEASE_BASE/$(release_path "$INK_VERSION" ink)
fetch "$ink_url/$ink_asset" "$work/$ink_asset"
fetch "$ink_url/ink-SHA256SUMS" "$work/ink-SHA256SUMS"
expected=$(awk -v file="$ink_asset" '$2 == file || $2 == "*" file {print $1; exit}' "$work/ink-SHA256SUMS")
[ -n "$expected" ] && [ "$(digest "$work/$ink_asset")" = "$expected" ] || { echo 'install: Ink checksum mismatch' >&2; exit 1; }
while IFS="$(printf '\t')" read -r source_repo source_commit name set target bytes expected asset; do
[ -n "$name" ] || continue
fetch "$toolset_url/$asset" "$work/$asset"
[ "$(digest "$work/$asset")" = "$expected" ] || { echo "install: checksum mismatch: $asset" >&2; exit 1; }
done < "$selected"
mkdir -p "$PREFIX/bin"
chmod 755 "$work/$ink_asset"
mv "$work/$ink_asset" "$PREFIX/bin/ink"
while IFS="$(printf '\t')" read -r source_repo source_commit name set target bytes expected asset; do
[ -n "$name" ] || continue
chmod 755 "$work/$asset"
mv "$work/$asset" "$PREFIX/bin/$name"
done < "$selected"
say "${bold}Installed.${reset} Executables only; policy and PATH were not changed."
say "Run: $PREFIX/bin/ink --help"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# req: installer/001, installer/002, installer/003, installer/004, installer/006 test
import hashlib
import http.server
import os
from pathlib import Path
import socketserver
import subprocess
import tempfile
import threading
ROOT = Path(__file__).resolve().parents[1]
TARGET = "x86_64-linux-musl"
def sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
with tempfile.TemporaryDirectory() as raw:
tmp = Path(raw)
server_root = tmp / "server"
ink_release = server_root / "releases" / "v-test" / "ink"
tools_release = server_root / "releases" / "v-tools" / "toolset"
ink_release.mkdir(parents=True)
tools_release.mkdir(parents=True)
ink_asset = ink_release / f"ink-{TARGET}"
ink_asset.write_text("#!/bin/sh\necho ink\n")
os.chmod(ink_asset, 0o755)
(ink_release / "ink-SHA256SUMS").write_text(f"{sha(ink_asset)} {ink_asset.name}\n")
rows = []
for name, group in (("facts", "text"), ("lines", "text"), ("httpget", "web"), ("gitstatus", "git")):
asset = tools_release / f"{name}-{TARGET}"
asset.write_text(f"#!/bin/sh\necho {name}\n")
os.chmod(asset, 0o755)
rows.append(("tmk241/ink-toolset", "fixture-commit", name, group, TARGET, asset.stat().st_size, sha(asset), asset.name))
manifest_name = f"ink-toolset-v-tools-{TARGET}.manifest.tsv"
manifest = tools_release / manifest_name
manifest.write_text("source_repository\tsource_commit\texecutable\tset\ttarget\tbytes\tsha256\tasset\n" + "".join("\t".join(map(str, row)) + "\n" for row in rows))
sums = [f"{sha(tools_release / row[7])} {row[7]}\n" for row in rows]
sums.append(f"{sha(manifest)} {manifest.name}\n")
(tools_release / "ink-toolset-SHA256SUMS").write_text("".join(sums))
class Quiet(http.server.SimpleHTTPRequestHandler):
def log_message(self, *_args):
pass
handler = lambda *args, **kwargs: Quiet(*args, directory=server_root, **kwargs)
with socketserver.TCPServer(("127.0.0.1", 0), handler) as server:
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
port = server.server_address[1]
prefix = tmp / "prefix"
home = tmp / "home"
home.mkdir()
policy = home / "policy.tsv"
policy.write_text("sentinel\n")
env = {
**os.environ,
"HOME": str(home),
"INK_RELEASE_BASE": f"http://127.0.0.1:{port}",
}
result = subprocess.run(
["/bin/sh", str(ROOT / "install.sh"), "--ink-version", "v-test", "--toolset-version", "v-tools", "--set", "text", "--tool", "httpget", "--prefix", str(prefix), "--yes"],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
server.shutdown()
assert result.returncode == 0, result.stderr
assert sorted(path.name for path in (prefix / "bin").iterdir()) == ["facts", "httpget", "ink", "lines"]
assert subprocess.check_output([prefix / "bin" / "ink"], text=True).strip() == "ink"
assert not (prefix / "bin" / "gitstatus").exists()
assert policy.read_text() == "sentinel\n"
assert not (home / ".profile").exists()