From 2c957b94f878d358203f0e5c27f965d207f06830 Mon Sep 17 00:00:00 2001 From: tmk241 Date: Mon, 10 Aug 2026 23:31:23 +0200 Subject: [PATCH] Establish public verified release boundary --- AGENTS.md | 32 ++++++++ README.md | 38 ++++++++- REQUIREMENTS.md | 28 +++++++ install.sh | 175 ++++++++++++++++++++++++++++++++++++++++++ test/installer_e2e.py | 80 +++++++++++++++++++ 5 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 100644 REQUIREMENTS.md create mode 100755 install.sh create mode 100755 test/installer_e2e.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bcd3b51 --- /dev/null +++ b/AGENTS.md @@ -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`. diff --git a/README.md b/README.md index f54beaf..170e01a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ -# ink-releases +# Ink Releases -Public verified static release assets for Ink and Ink Toolset \ No newline at end of file +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. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..0667cbc --- /dev/null +++ b/REQUIREMENTS.md @@ -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. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..2f4cf4e --- /dev/null +++ b/install.sh @@ -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 &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 &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" diff --git a/test/installer_e2e.py b/test/installer_e2e.py new file mode 100755 index 0000000..1cde449 --- /dev/null +++ b/test/installer_e2e.py @@ -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()