From b11f4a67be11094a73a35157dcccf8d12c48d693 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 20 Oct 2026 19:22:12 -0400 Subject: [PATCH 1/4] feat: add verified one-line install and concise stack overview --- README.md | 233 ++++-------------------- docs/book/src/SUMMARY.md | 1 + docs/book/src/installation-reference.md | 169 +++++++++++++++++ docs/stack-table.md | 8 +- install.sh | 126 +++++++++++++ scripts/check-stack-table.py | 4 +- scripts/install.sh | 51 +++++- scripts/test-install-bootstrap.py | 31 ++++ scripts/test-readme.sh | 2 + scripts/test-stack-table.py | 4 +- 10 files changed, 419 insertions(+), 210 deletions(-) create mode 100644 docs/book/src/installation-reference.md create mode 100755 install.sh diff --git a/README.md b/README.md index 0716433..1062488 100644 --- a/README.md +++ b/README.md @@ -1,217 +1,68 @@ -> Caboodle is becoming **Tambo**. The transition release includes both `tambo` -> and `caboodle`, backed by the same CLI. Existing plan and state paths remain -> compatible. New release archives use `tambo-vVERSION-TARGET.tar.gz` with a -> sibling SHA-256 checksum; supported archives contain both commands. -> -> Repository and registry publication, package-manager migration, and installed -> host verification are separate release steps. The compatibility command stays -> available for at least one release. +

Tambo toolkit

-

- Animated banner β€” a teal kit box with a tipping lid and three flowing, knotted cords -

+# Tambo -

- Tambo logo β€” a light-teal tackle box open with pink fold-out trays, each compartment holding a tool of the stack -

- -

tambo

- -

- 🧰 Agent Platform Toolkit - installs and configures the quipu stack -

- -

- License: MIT - CI - Part of the quipu stack -

- -**Caboodle installs and configures a stack built around Quipu, a governed, -bitemporal knowledge store for people and AI coding agents.** Shared facts, -decisions and work history stay available for your agents to query. Its CLI -installs the tools from a reviewable plan and proves each one works; an agent -skill guides you through the same workflow. - -## Why you would want it - -- Give your agent memory and code context that survive a session. -- Review one plan, then resume installation if it is interrupted. -- Know each tool passed a functional check before you rely on it. - -[What the quipu stack is, and three ways to start](https://scbrown.github.io/caboodle/introduction.html). +Install the Quipu stack from a plan you can review, then prove the selected tools +work. Give an agent durable memory and code context, without assembling every tool +by hand. Tambo is a CLI and [agent skill](skills/tambo/SKILL.md). +The compatibility command `caboodle` remains available. ## Install -### Checksummed archives - -Checksummed releases: Linux x86_64, macOS arm64 and macOS x86_64. -The installer verifies SHA256 before placing the binary in `~/.cargo/bin`. - -```bash -curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | bash -``` - -To pin a release, pass the version to the receiving shell: - ```bash -curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | CABOODLE_VERSION=vX.Y.Z bash +curl -fsSL https://raw.githubusercontent.com/scbrown/tambo/main/install.sh | sh ``` -Add the install directory to this shell's PATH: - -```bash -export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH" -``` - -The `export` lasts for this shell only. To keep it, add the same line once to -your shell's startup file: `~/.bashrc` for bash, `~/.zshrc` for zsh (the macOS -default). For bash: - -```bash -echo 'export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"' >> ~/.bashrc -``` - -Apps started outside a terminal, such as the Claude desktop app or an IDE, may -not read that file, which is why the agent step below registers servers by -absolute path. - -Or build the latest release from source with Rust (`cargo install --git -https://github.com/scbrown/caboodle --tag vX.Y.Z --locked`). Either way, -`caboodle --version` prints `caboodle X.Y.Z` for the release you installed. - -### macOS with Homebrew - -```bash -brew install scbrown/tap/caboodle -caboodle --version -``` - -Homebrew owns the installed command; Caboodle still composes and verifies the -stack. Update it with `brew upgrade scbrown/tap/caboodle`. To hold the installed -version, use `brew pin caboodle`; resume updates with `brew unpin caboodle`. -The [tap channel guide](https://github.com/scbrown/homebrew-tap#caboodle-preview-channel) -explains the opt-in nightly channel and switching back to stable. +Downloads the latest release, verifies its SHA-256 and version, and installs +`tambo` and `caboodle` in `${CARGO_HOME:-$HOME/.cargo}/bin`. Requires `curl`, +`tar`, Python 3 and `sha256sum` or `shasum`. Add that directory to `PATH`. +Published archives support Linux x86_64 and macOS arm64/x86_64; **Linux arm64 +has no published archive**. Source builds, pinned versions, removal and the +Homebrew alternative are in the [installation reference](https://scbrown.github.io/caboodle/installation-reference.html). ## First success: local, single-user -After installing Tambo, run this in an empty directory you will keep: +Run in an empty directory you intend to keep: ```bash tambo quickstart ``` -Tambo saves a retrieval plan, installs Quipu, Camayoc and Bobbin, and verifies -all three. There are no interview questions, issued tokens, accounts or running -server prerequisites. Your knowledge store is local at `.caboodle/quipu.db`. -Keep `caboodle-plan.toml` and `.caboodle/` to resume or update the same install. -An existing plan is reused and validated; quickstart never replaces it. - -**Success check:** exit 0 and `quipu: verified`, `camayoc: verified`, and -`bobbin: verified`. Each tool passes an isolated functional round trip, including -a negative control and a read after writing. Recheck with one command: +This saves a reviewed retrieval plan, installs Quipu, Camayoc and Bobbin, and +checks each tool. Success is exit 0 and `quipu: verified`, `camayoc: verified`, +`bobbin: verified`. Keep `caboodle-plan.toml` and `.caboodle/` to resume. +Recheck the same install: ```bash tambo verify ``` -Quipu's verification uses a temporary embedded database. Camayoc's check starts -and stops its own temporary loopback server; you do not supply or manage one. -This proves the installed tools, not a connection to a remote knowledge service. -Optional Shantytown hook messages do not require a crew installation. +### Advanced: shared services and agents -### Advanced: connect to a shared, authenticated Quipu server +For issued credentials, remote stores and registering Bobbin with your agent, +use the [agent and installation reference](https://scbrown.github.io/caboodle/installation-reference.html). +The default quickstart is local and needs no account or remote server. -Remote mode is opt-in: set `QUIPU_SERVER` to the server you intend to use, or -create a plan with `--quipu-mcp-url`. Obtain an administrator-issued token and -provision it before verification: +From the code repository you want searchable, initialize and index it before +registering the server by absolute path: ```bash -tambo provision-quipu-token --from /secure/issued-token --server https://your-quipu-server.example -export QUIPU_SERVER=https://your-quipu-server.example -tambo verify -``` - -Remote verification requires the canonical credential with `0400/0700` -permissions, proves that unauthenticated access is refused and the selected -credential is accepted, and refuses token rotation. Missing or rejected -credentials stop verification. Never put a token in the plan or a server URL. -Unset `QUIPU_SERVER` to use the local default; an explicit `quipu_mcp` plan -remains remote until you remove that configuration deliberately. - -To customize the tools and intended-use contract instead, use -`tambo init --guided` or `tambo plan --profile retrieval --intent caboodle-intent.toml`, -review the resulting plan, then run `tambo install`. See the -[walkthrough](https://scbrown.github.io/caboodle/getting-started.html). - -## On your own code - -| I want to… | Command | -|---|---| -| Choose tools and describe my own use | `caboodle init --guided` | -| Find install blockers before changing anything | `caboodle doctor` | -| Install and prove the reviewed selection | `caboodle install` | -| Recheck the selected tools | `caboodle verify` | -| Compare against this build's reviewed versions | `caboodle check-updates` | - -[Full command and configuration reference](https://scbrown.github.io/caboodle/reference.html). - -## Wire it into your agent - -Tambo is a CLI, with an [agent skill](skills/tambo/SKILL.md), not an MCP -server. Bobbin, which the `retrieval` profile installed above, is one. It -searches one repository: the code you want your agent to work on, such as your -own project's git checkout. Do not use `~/caboodle`, which holds only the plan. -Index that repository, then register bobbin from inside it: - -```bash -cd ~/path/to/your/repo bobbin init bobbin index claude mcp add bobbin -- "$(command -v bobbin)" serve "$PWD" ``` -`$(command -v bobbin)` and `$PWD` are expanded once, now, so Claude Code -starts the server by absolute path and serves this repository whatever its own -`PATH` or working directory. Registering a bare `bobbin` fails with `ENOENT` when -Claude Code was not started from a shell that has `~/.cargo/bin` on `PATH`. - -`claude mcp add` uses the default `local` scope: the server is registered for -this project only, so start Claude Code in this repository to see it (`claude -mcp list` there shows `bobbin: ... βœ“ Connected`). Repeat the four lines in each -repository you want searchable. `bobbin init` creates `.bobbin/` -in the repository and, if the repository has a `.gitignore`, appends `.bobbin/` -to it; review that change before you commit. Without `bobbin init`, `bobbin -serve` exits with `Bobbin not initialized` and Claude Code reports the server as -failed to connect. - -MCP (Model Context Protocol) lets your agent call the installed server. -[Agent setup](https://scbrown.github.io/caboodle/agents.html) covers indexing, Yupana, other clients -and managed crew registration through `caboodle project-settings`. - -## Before you start - -| Platform | Caboodle | Quipu | Yupana | -|---|---|---|---| -| Linux x86_64 | Release | Release | Release | -| macOS arm64 / x86_64 | Release | Release | Release | -| Linux arm64 | Build with Rust | Release | Build with Rust | - -Have `curl`, `tar`, `git`, `bash`, `python3` and `sha256sum` on PATH -(`brew install coreutils` supplies `sha256sum` on macOS). -The `everything` profile also needs Go. No account or remote server is needed. -[Installation details](https://scbrown.github.io/caboodle/installing.html) cover source builds, -installed files and removal; `caboodle doctor` names missing prerequisites. +## Where to go next -## What's next +- [Choose a profile](https://scbrown.github.io/caboodle/profiles.html) or use `tambo init --guided`. +- [Camayoc](https://github.com/scbrown/camayoc): vocabulary, shapes and governed agent memory. +- [Getting started](https://scbrown.github.io/caboodle/getting-started.html) and [command reference](https://scbrown.github.io/caboodle/reference.html). +- **Load this repository's graph:** repository-pendant publication is pending. + When a release provides the archive, checksum and provenance, follow Quipu's + [verified import workflow](https://scbrown.github.io/quipu/sharing/index.html). + Import stages knowledge; it does not promote it into ROOT automatically. -- [Read the book](https://scbrown.github.io/caboodle/introduction.html) -- [Find a topic in the docs map](https://scbrown.github.io/caboodle/docs-map.html) -- [Choose your profile](https://scbrown.github.io/caboodle/profiles.html) - -## 🧺 The stack - -Caboodle installs these together and proves each one works; every tool also stands alone. +## The stack @@ -222,9 +73,11 @@ when. | tool | what it gives your agents | |---|---| -| [caboodle](https://github.com/scbrown/caboodle) | 🧰 Agent Platform Toolkit - installs and configures the quipu stack | +| [tambo](https://github.com/scbrown/tambo) | 🧰 Agent Platform Toolkit - installs and configures the quipu stack | | [quipu](https://github.com/scbrown/quipu) | stores shared facts with provenance and history; can check writes against your rules | | [camayoc](https://github.com/scbrown/camayoc) | prepares knowledge and vocabulary for Quipu, with checks before it is admitted | +| [quechua](https://github.com/scbrown/quechua) | shared vocabulary and shapes for interoperable knowledge | +| [chaski](https://github.com/scbrown/chaski) | reacts to graph changes and delivers governed notifications | | [bobbin](https://github.com/scbrown/bobbin) | searches your code and can include related knowledge from Quipu | | [yupana](https://github.com/scbrown/yupana) | shows code impact and can read policies from Quipu to check changes | | [desire-path](https://github.com/scbrown/desire-path) | turns agent mistakes into feature requests; works on its own without Quipu | @@ -237,16 +90,6 @@ when. ## Contributing -```bash -just book -just check -just docs-check -``` - -## πŸ“œ License - -[MIT](LICENSE) +Run `just check`, `just book` and `just docs-check`. -

- Animated footer β€” a woven band of teal, pink, and ochre cords with sliding beads -

+[MIT license](LICENSE). diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 27f8b77..4592478 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -4,6 +4,7 @@ - [Getting started](getting-started.md) - [Installing and removing](installing.md) + - [Installation and agent reference](installation-reference.md) - [Using it with agents](agents.md) - [Using Caboodle](using.md) - [Reference](reference.md) diff --git a/docs/book/src/installation-reference.md b/docs/book/src/installation-reference.md new file mode 100644 index 0000000..5204adc --- /dev/null +++ b/docs/book/src/installation-reference.md @@ -0,0 +1,169 @@ +# Installation and agent reference + + +### Checksummed archives + +Checksummed releases: Linux x86_64, macOS arm64 and macOS x86_64. +The installer verifies SHA256 before placing the binary in `~/.cargo/bin`. + +```bash +curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | bash +``` + +To pin a release, pass the version to the receiving shell: + +```bash +curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | CABOODLE_VERSION=vX.Y.Z bash +``` + +Add the install directory to this shell's PATH: + +```bash +export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH" +``` + +The `export` lasts for this shell only. To keep it, add the same line once to +your shell's startup file: `~/.bashrc` for bash, `~/.zshrc` for zsh (the macOS +default). For bash: + +```bash +echo 'export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"' >> ~/.bashrc +``` + +Apps started outside a terminal, such as the Claude desktop app or an IDE, may +not read that file, which is why the agent step below registers servers by +absolute path. + +Or build the latest release from source with Rust (`cargo install --git +https://github.com/scbrown/caboodle --tag vX.Y.Z --locked`). Either way, +`caboodle --version` prints `caboodle X.Y.Z` for the release you installed. + +### macOS with Homebrew + +```bash +brew install scbrown/tap/caboodle +caboodle --version +``` + +Homebrew owns the installed command; Caboodle still composes and verifies the +stack. Update it with `brew upgrade scbrown/tap/caboodle`. To hold the installed +version, use `brew pin caboodle`; resume updates with `brew unpin caboodle`. +The [tap channel guide](https://github.com/scbrown/homebrew-tap#caboodle-preview-channel) +explains the opt-in nightly channel and switching back to stable. + +## First success: local, single-user + +After installing Tambo, run this in an empty directory you will keep: + +```bash +tambo quickstart +``` + +Tambo saves a retrieval plan, installs Quipu, Camayoc and Bobbin, and verifies +all three. There are no interview questions, issued tokens, accounts or running +server prerequisites. Your knowledge store is local at `.caboodle/quipu.db`. +Keep `caboodle-plan.toml` and `.caboodle/` to resume or update the same install. +An existing plan is reused and validated; quickstart never replaces it. + +**Success check:** exit 0 and `quipu: verified`, `camayoc: verified`, and +`bobbin: verified`. Each tool passes an isolated functional round trip, including +a negative control and a read after writing. Recheck with one command: + +```bash +tambo verify +``` + +Quipu's verification uses a temporary embedded database. Camayoc's check starts +and stops its own temporary loopback server; you do not supply or manage one. +This proves the installed tools, not a connection to a remote knowledge service. +Optional Shantytown hook messages do not require a crew installation. + +### Advanced: connect to a shared, authenticated Quipu server + +Remote mode is opt-in: set `QUIPU_SERVER` to the server you intend to use, or +create a plan with `--quipu-mcp-url`. Obtain an administrator-issued token and +provision it before verification: + +```bash +tambo provision-quipu-token --from /secure/issued-token --server https://your-quipu-server.example +export QUIPU_SERVER=https://your-quipu-server.example +tambo verify +``` + +Remote verification requires the canonical credential with `0400/0700` +permissions, proves that unauthenticated access is refused and the selected +credential is accepted, and refuses token rotation. Missing or rejected +credentials stop verification. Never put a token in the plan or a server URL. +Unset `QUIPU_SERVER` to use the local default; an explicit `quipu_mcp` plan +remains remote until you remove that configuration deliberately. + +To customize the tools and intended-use contract instead, use +`tambo init --guided` or `tambo plan --profile retrieval --intent caboodle-intent.toml`, +review the resulting plan, then run `tambo install`. See the +[walkthrough](https://scbrown.github.io/caboodle/getting-started.html). + +## On your own code + +| I want to… | Command | +|---|---| +| Choose tools and describe my own use | `caboodle init --guided` | +| Find install blockers before changing anything | `caboodle doctor` | +| Install and prove the reviewed selection | `caboodle install` | +| Recheck the selected tools | `caboodle verify` | +| Compare against this build's reviewed versions | `caboodle check-updates` | + +[Full command and configuration reference](https://scbrown.github.io/caboodle/reference.html). + +## Wire it into your agent + +Tambo is a CLI, with an [agent skill](https://github.com/scbrown/tambo/blob/main/skills/tambo/SKILL.md), not an MCP +server. Bobbin, which the `retrieval` profile installed above, is one. It +searches one repository: the code you want your agent to work on, such as your +own project's git checkout. Do not use `~/caboodle`, which holds only the plan. +Index that repository, then register bobbin from inside it: + +```bash +cd ~/path/to/your/repo +bobbin init +bobbin index +claude mcp add bobbin -- "$(command -v bobbin)" serve "$PWD" +``` + +`$(command -v bobbin)` and `$PWD` are expanded once, now, so Claude Code +starts the server by absolute path and serves this repository whatever its own +`PATH` or working directory. Registering a bare `bobbin` fails with `ENOENT` when +Claude Code was not started from a shell that has `~/.cargo/bin` on `PATH`. + +`claude mcp add` uses the default `local` scope: the server is registered for +this project only, so start Claude Code in this repository to see it (`claude +mcp list` there shows `bobbin: ... βœ“ Connected`). Repeat the four lines in each +repository you want searchable. `bobbin init` creates `.bobbin/` +in the repository and, if the repository has a `.gitignore`, appends `.bobbin/` +to it; review that change before you commit. Without `bobbin init`, `bobbin +serve` exits with `Bobbin not initialized` and Claude Code reports the server as +failed to connect. + +MCP (Model Context Protocol) lets your agent call the installed server. +[Agent setup](https://scbrown.github.io/caboodle/agents.html) covers indexing, Yupana, other clients +and managed crew registration through `caboodle project-settings`. + +## Before you start + +| Platform | Caboodle | Quipu | Yupana | +|---|---|---|---| +| Linux x86_64 | Release | Release | Release | +| macOS arm64 / x86_64 | Release | Release | Release | +| Linux arm64 | Build with Rust | Release | Build with Rust | + +Have `curl`, `tar`, `git`, `bash`, `python3` and `sha256sum` on PATH +(`brew install coreutils` supplies `sha256sum` on macOS). +The `everything` profile also needs Go. No account or remote server is needed. +[Installation details](https://scbrown.github.io/caboodle/installing.html) cover source builds, +installed files and removal; `caboodle doctor` names missing prerequisites. + +## What's next + +- [Read the book](https://scbrown.github.io/caboodle/introduction.html) +- [Find a topic in the docs map](https://scbrown.github.io/caboodle/docs-map.html) +- [Choose your profile](https://scbrown.github.io/caboodle/profiles.html) + diff --git a/docs/stack-table.md b/docs/stack-table.md index 373f99a..3a36d77 100644 --- a/docs/stack-table.md +++ b/docs/stack-table.md @@ -1,6 +1,6 @@ # Canonical stack table -This is the source for the marked stack overview in the nine tool READMEs. +This is the source for the marked stack overview in the twelve tool READMEs. @@ -12,9 +12,11 @@ when. | tool | what it gives your agents | |---|---| -| [caboodle](https://github.com/scbrown/caboodle) | 🧰 Agent Platform Toolkit - installs and configures the quipu stack | +| [tambo](https://github.com/scbrown/tambo) | 🧰 Agent Platform Toolkit - installs and configures the quipu stack | | [quipu](https://github.com/scbrown/quipu) | stores shared facts with provenance and history; can check writes against your rules | | [camayoc](https://github.com/scbrown/camayoc) | prepares knowledge and vocabulary for Quipu, with checks before it is admitted | +| [quechua](https://github.com/scbrown/quechua) | shared vocabulary and shapes for interoperable knowledge | +| [chaski](https://github.com/scbrown/chaski) | reacts to graph changes and delivers governed notifications | | [bobbin](https://github.com/scbrown/bobbin) | searches your code and can include related knowledge from Quipu | | [yupana](https://github.com/scbrown/yupana) | shows code impact and can read policies from Quipu to check changes | | [desire-path](https://github.com/scbrown/desire-path) | turns agent mistakes into feature requests; works on its own without Quipu | @@ -27,5 +29,5 @@ when. Run `python3 scripts/check-stack-table.py` to check this repository, or add -`--remote` to compare all nine published READMEs. Update the marked blocks +`--remote` to compare all twelve published READMEs. Update the marked blocks together when changing this table. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9b46f7c --- /dev/null +++ b/install.sh @@ -0,0 +1,126 @@ +#!/bin/sh +set -eu + +# This compatibility URL also redirects after the repository rename. +repo=${TAMBO_REPOSITORY:-${CABOODLE_REPOSITORY:-scbrown/caboodle}} +version=${TAMBO_VERSION:-${CABOODLE_VERSION:-}} +install_dir=${TAMBO_INSTALL_DIR:-${CABOODLE_INSTALL_DIR:-${CARGO_HOME:-$HOME/.cargo}/bin}} + +if [ -z "$version" ]; then + version=$(curl -fsSL --connect-timeout 10 --max-time 120 "https://api.github.com/repos/$repo/releases/latest" | + sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1) +fi +case "$version" in + v[0-9]*[!A-Za-z0-9._-]*) printf '%s\n' "invalid release tag" >&2; exit 1 ;; + v[0-9]*) ;; + *) printf '%s\n' "invalid or unavailable Tambo release version: $version" >&2; exit 1 ;; +esac + +if [ -n "${TAMBO_TARGET:-${CABOODLE_TARGET:-}}" ]; then + target=${TAMBO_TARGET:-$CABOODLE_TARGET} +else + machine=$(uname -m) + system=$(uname -s) + case "$system:$machine" in + Linux:x86_64) target=x86_64-unknown-linux-gnu ;; + Darwin:x86_64) target=x86_64-apple-darwin ;; + Darwin:arm64) target=aarch64-apple-darwin ;; + *) printf '%s\n' "unsupported Tambo release target: $system $machine" >&2; exit 1 ;; + esac +fi + +case "$target" in + x86_64-unknown-linux-gnu|x86_64-apple-darwin|aarch64-apple-darwin) ;; + *) printf '%s\n' "unsupported Tambo release target: $target" >&2; exit 1 ;; +esac + +archive="tambo-$version-$target.tar.gz" +binaries='tambo caboodle' +base=${TAMBO_RELEASE_BASE_URL:-${CABOODLE_RELEASE_BASE_URL:-https://github.com/$repo/releases/download/$version}} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT HUP INT TERM + +if ! status=$(curl -sSL --connect-timeout 10 --max-time 120 \ + -w '%{http_code}' "$base/$archive" -o "$work/$archive"); then + printf '%s\n' 'release archive transfer failed' >&2 + exit 1 +fi +case "$status" in + 200) ;; + 000) + # Local release fixtures use file://, which has no HTTP status. + case "$base" in file://*) ;; *) exit 1 ;; esac + ;; + 404) + archive="caboodle-$version-$target.tar.gz" + binaries=caboodle + curl -fsSL --connect-timeout 10 --max-time 120 "$base/$archive" -o "$work/$archive" + ;; + *) printf '%s\n' "release archive unavailable (HTTP $status)" >&2; exit 1 ;; +esac +curl -fsSL --connect-timeout 10 --max-time 120 "$base/$archive.sha256" -o "$work/$archive.sha256" +python3 - "$work/$archive.sha256" "$archive" <<'PYCHECKSUM' +import pathlib, re, sys +fields = pathlib.Path(sys.argv[1]).read_text().split() +if (len(fields) != 2 or not re.fullmatch(r"[0-9a-fA-F]{64}", fields[0]) + or fields[1].lstrip("*") != sys.argv[2]): + raise SystemExit("invalid release checksum manifest") +PYCHECKSUM +expected=$(awk '{print $1}' "$work/$archive.sha256" | tr 'A-F' 'a-f') +if command -v sha256sum >/dev/null; then + actual=$(sha256sum "$work/$archive" | awk '{print $1}') +else + actual=$(shasum -a 256 "$work/$archive" | awk '{print $1}') +fi +test "$actual" = "$expected" || { + printf '%s\n' "$archive: checksum mismatch" >&2 + exit 1 +} +# Only the declared regular executables may leave the verified archive. +python3 - "$work" "$archive" $binaries <<'PYARCHIVE' +import pathlib, sys, tarfile +root, name = pathlib.Path(sys.argv[1]), sys.argv[2] +expected = set(sys.argv[3:]) +with tarfile.open(root / name, "r:gz") as bundle: + members = bundle.getmembers() + if len(members) != len(expected) or {m.name for m in members} != expected: + raise SystemExit("unexpected release archive members") + for member in members: + if not member.isfile() or member.size > 256 * 1024 * 1024: + raise SystemExit("invalid release executable") + source = bundle.extractfile(member) + if source is None: + raise SystemExit("missing release executable") + (root / member.name).write_bytes(source.read()) + (root / member.name).chmod(0o755) +PYARCHIVE +for binary in $binaries; do + test -f "$work/$binary" && test ! -L "$work/$binary" && test -x "$work/$binary" + reported=$("$work/$binary" --version) + case "$reported" in + "$binary ${version#v}"|"$binary ${version#v} "*) ;; + *) printf '%s\n' "release version mismatch: $binary" >&2; exit 1 ;; + esac +done +mkdir -p "$install_dir" +for binary in $binaries; do + staged="$install_dir/.$binary.$$.tmp" + cp "$work/$binary" "$staged" + chmod 0755 "$staged" + mv "$staged" "$install_dir/$binary" +done +for binary in $binaries; do + "$install_dir/$binary" --version + printf '%s\n' "installed $install_dir/$binary" +done + +# Name a stale executable for either command; version equality cannot prove +# that PATH resolves to the binary just installed. +for binary in $binaries; do + resolved=$(command -v "$binary" 2>/dev/null || true) + if [ -n "$resolved" ] && ! [ "$resolved" -ef "$install_dir/$binary" ]; then + printf '%s\n' "WARNING: \`$binary\` on PATH is $resolved ($("$resolved" --version 2>&1 | head -n 1)), not the copy just installed at $install_dir/$binary. Remove the stale copy or put $install_dir earlier on PATH." >&2 + elif [ -z "$resolved" ]; then + printf '%s\n' "note: $install_dir is not on PATH; add it to run \`$binary\`" >&2 + fi +done diff --git a/scripts/check-stack-table.py b/scripts/check-stack-table.py index 4559e23..2260af0 100644 --- a/scripts/check-stack-table.py +++ b/scripts/check-stack-table.py @@ -10,7 +10,7 @@ from urllib.request import Request, urlopen ROOT = Path(__file__).resolve().parents[1] -REPOS = ("quipu", "bobbin", "camayoc", "yupana", "desire-path", "seeds", "caboodle", "shantytown", "rikuy") +REPOS = ("tambo", "quipu", "camayoc", "quechua", "chaski", "bobbin", "yupana", "seeds", "shantytown", "shuttle", "rikuy", "desire-path") START = b"" END = b"" MAX_BYTES = 256 * 1024 @@ -56,7 +56,7 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("readmes", nargs="*", type=Path, help="local README copies") parser.add_argument("--canonical", type=Path, default=ROOT / "docs/stack-table.md") - parser.add_argument("--remote", action="store_true", help="also audit all nine public main branches") + parser.add_argument("--remote", action="store_true", help="also audit all twelve public main branches") args = parser.parse_args() try: canonical = marked_table(args.canonical.read_bytes(), str(args.canonical)) diff --git a/scripts/install.sh b/scripts/install.sh index 93e64b6..9b46f7c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,6 +11,7 @@ if [ -z "$version" ]; then sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1) fi case "$version" in + v[0-9]*[!A-Za-z0-9._-]*) printf '%s\n' "invalid release tag" >&2; exit 1 ;; v[0-9]*) ;; *) printf '%s\n' "invalid or unavailable Tambo release version: $version" >&2; exit 1 ;; esac @@ -28,6 +29,11 @@ else esac fi +case "$target" in + x86_64-unknown-linux-gnu|x86_64-apple-darwin|aarch64-apple-darwin) ;; + *) printf '%s\n' "unsupported Tambo release target: $target" >&2; exit 1 ;; +esac + archive="tambo-$version-$target.tar.gz" binaries='tambo caboodle' base=${TAMBO_RELEASE_BASE_URL:-${CABOODLE_RELEASE_BASE_URL:-https://github.com/$repo/releases/download/$version}} @@ -53,19 +59,48 @@ case "$status" in *) printf '%s\n' "release archive unavailable (HTTP $status)" >&2; exit 1 ;; esac curl -fsSL --connect-timeout 10 --max-time 120 "$base/$archive.sha256" -o "$work/$archive.sha256" +python3 - "$work/$archive.sha256" "$archive" <<'PYCHECKSUM' +import pathlib, re, sys +fields = pathlib.Path(sys.argv[1]).read_text().split() +if (len(fields) != 2 or not re.fullmatch(r"[0-9a-fA-F]{64}", fields[0]) + or fields[1].lstrip("*") != sys.argv[2]): + raise SystemExit("invalid release checksum manifest") +PYCHECKSUM +expected=$(awk '{print $1}' "$work/$archive.sha256" | tr 'A-F' 'a-f') if command -v sha256sum >/dev/null; then - (cd "$work" && sha256sum -c "$archive.sha256") + actual=$(sha256sum "$work/$archive" | awk '{print $1}') else - expected=$(awk '{print $1}' "$work/$archive.sha256") actual=$(shasum -a 256 "$work/$archive" | awk '{print $1}') - test "$actual" = "$expected" || { - printf '%s\n' "$archive: checksum mismatch" >&2 - exit 1 - } fi -tar -xzf "$work/$archive" -C "$work" +test "$actual" = "$expected" || { + printf '%s\n' "$archive: checksum mismatch" >&2 + exit 1 +} +# Only the declared regular executables may leave the verified archive. +python3 - "$work" "$archive" $binaries <<'PYARCHIVE' +import pathlib, sys, tarfile +root, name = pathlib.Path(sys.argv[1]), sys.argv[2] +expected = set(sys.argv[3:]) +with tarfile.open(root / name, "r:gz") as bundle: + members = bundle.getmembers() + if len(members) != len(expected) or {m.name for m in members} != expected: + raise SystemExit("unexpected release archive members") + for member in members: + if not member.isfile() or member.size > 256 * 1024 * 1024: + raise SystemExit("invalid release executable") + source = bundle.extractfile(member) + if source is None: + raise SystemExit("missing release executable") + (root / member.name).write_bytes(source.read()) + (root / member.name).chmod(0o755) +PYARCHIVE for binary in $binaries; do - test -x "$work/$binary" + test -f "$work/$binary" && test ! -L "$work/$binary" && test -x "$work/$binary" + reported=$("$work/$binary" --version) + case "$reported" in + "$binary ${version#v}"|"$binary ${version#v} "*) ;; + *) printf '%s\n' "release version mismatch: $binary" >&2; exit 1 ;; + esac done mkdir -p "$install_dir" for binary in $binaries; do diff --git a/scripts/test-install-bootstrap.py b/scripts/test-install-bootstrap.py index 0176e06..467dfc8 100644 --- a/scripts/test-install-bootstrap.py +++ b/scripts/test-install-bootstrap.py @@ -128,6 +128,37 @@ def test_bad_checksum_never_falls_back_or_replaces(self): self.assertEqual(existing.read_bytes(), b'prior installed bytes') self.assertNotIn('/caboodle-', self.requests.read_text()) + def test_root_installer_matches_compatibility_entrypoint(self): + self.assertEqual((ROOT / 'install.sh').read_bytes(), + (ROOT / 'scripts/install.sh').read_bytes()) + + def test_unsupported_platform_preserves_existing_install(self): + self.target = 'aarch64-unknown-linux-gnu' + existing = self.root / 'installed/tambo' + existing.write_bytes(b'prior installed bytes') + self.assertNotEqual(self.run_installer('v0.4.0').returncode, 0) + self.assertIn('unsupported', self.result.stderr) + self.assertEqual(existing.read_bytes(), b'prior installed bytes') + + def test_missing_assets_preserve_existing_install(self): + existing = self.root / 'installed/tambo' + existing.write_bytes(b'prior installed bytes') + self.assertNotEqual(self.run_installer('v0.4.0').returncode, 0) + self.assertEqual(existing.read_bytes(), b'prior installed bytes') + + def test_wrong_version_preserves_existing_install(self): + self.archive('tambo', 'v0.3.0', ['tambo', 'caboodle']) + for url, route in list(self.routes.items()): + self.routes[url.replace('v0.3.0', 'v0.4.0')] = route + # Rewrite only the checksum filename: verified bytes still report 0.3.0. + url = f'{self.base}/tambo-v0.4.0-{self.target}.tar.gz.sha256' + self.routes[url]['body'] = self.routes[url]['body'].replace('v0.3.0', 'v0.4.0') + existing = self.root / 'installed/tambo' + existing.write_bytes(b'prior installed bytes') + self.assertNotEqual(self.run_installer('v0.4.0').returncode, 0) + self.assertIn('version mismatch', self.result.stderr) + self.assertEqual(existing.read_bytes(), b'prior installed bytes') + def test_non_404_failure_never_falls_back(self): self.archive('caboodle', 'v0.3.0', ['caboodle']) for response in ({'status': 500}, {'status': 0, 'exit': 7}): diff --git a/scripts/test-readme.sh b/scripts/test-readme.sh index ce57481..28e26bc 100755 --- a/scripts/test-readme.sh +++ b/scripts/test-readme.sh @@ -25,6 +25,8 @@ text = pathlib.Path(sys.argv[1]).read_text() install = text.split("## Install", 1)[1].split("## First success", 1)[0] local = text.split("## First success", 1)[1].split("### Advanced:", 1)[0] bootstrap = re.findall(r"```bash\n(.*?)```", install, re.S)[0] +# Exercise this proposal's installer rather than fetching unrelated main bytes. +bootstrap = bootstrap.replace('https://raw.githubusercontent.com/scbrown/tambo/main/install.sh', pathlib.Path(sys.argv[1]).parent.joinpath('install.sh').as_uri()) commands = re.findall(r"```bash\n(.*?)```", local, re.S) assert commands == ["tambo quickstart\n", "tambo verify\n"], commands pathlib.Path(sys.argv[2]).write_text(bootstrap + '\nexport PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"\n' + "\n".join(commands)) diff --git a/scripts/test-stack-table.py b/scripts/test-stack-table.py index 98c91fa..9893a4f 100644 --- a/scripts/test-stack-table.py +++ b/scripts/test-stack-table.py @@ -82,14 +82,14 @@ def read(repo): self.assertEqual(CHECK.main(), expected) self.assertEqual(seen.count("shantytown"), 1) self.assertEqual(seen.count("rikuy"), 1) - self.assertEqual(len(seen), 9) + self.assertEqual(len(seen), 12) def test_remote_response_has_a_size_limit(self): response = MagicMock() response.__enter__.return_value.read.return_value = b"x" * (CHECK.MAX_BYTES + 1) with patch.object(CHECK, "urlopen", return_value=response): with self.assertRaisesRegex(ValueError, "size limit"): - CHECK.remote_readme("caboodle") + CHECK.remote_readme("tambo") if __name__ == "__main__": From f103bd4e7a402a6ff0acfb9e4b0d995e63bd8d3a Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 20 Oct 2026 19:24:59 -0400 Subject: [PATCH 2/4] docs: record verified installer refusal controls --- docs/demos/install-bootstrap.md | 21 +++++++++++++++++++++ docs/demos/install-bootstrap.timing | 24 ++++++++++++++++++++++++ docs/demos/install-bootstrap.txt | 18 ++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 docs/demos/install-bootstrap.md create mode 100644 docs/demos/install-bootstrap.timing create mode 100644 docs/demos/install-bootstrap.txt diff --git a/docs/demos/install-bootstrap.md b/docs/demos/install-bootstrap.md new file mode 100644 index 0000000..436c47a --- /dev/null +++ b/docs/demos/install-bootstrap.md @@ -0,0 +1,21 @@ +# Verified bootstrap demonstration + +Previously the bootstrap checked the installed version after replacing files. +It now checks the staged executable first, so a mismatched release preserves +previous bytes. A standalone root entrypoint supports the short README command. + +[Recorded fixture run](install-bootstrap.txt) Β· [timing](install-bootstrap.timing). +Ten controls pass, including valid current/compatibility archives, corrupt +checksum, missing asset, unsupported platform and wrong-version refusal. +These are synthetic local release fixtures, not cross-platform binary proof. + +Replay downloaded files with: + +```sh +scriptreplay --log-out install-bootstrap.txt --log-timing install-bootstrap.timing +``` + +Repeat the controls with `python3 scripts/test-install-bootstrap.py`. +Separately, the actual published Linux x86_64 v0.4.0 archive was checksum-verified, +installed in a temporary prefix, and both commands reported version 0.4.0. +No host installation or new release was performed. diff --git a/docs/demos/install-bootstrap.timing b/docs/demos/install-bootstrap.timing new file mode 100644 index 0000000..bb64703 --- /dev/null +++ b/docs/demos/install-bootstrap.timing @@ -0,0 +1,24 @@ +0.046478 119 +0.064428 4 +0.000023 149 +0.133871 4 +0.000006 117 +0.042938 4 +0.000007 105 +0.113277 4 +0.000017 105 +0.180362 4 +0.000020 101 +0.043462 4 +0.000005 79 +0.115002 4 +0.000016 131 +0.000556 4 +0.000016 131 +0.002756 4 +0.000007 117 +0.090240 4 +0.000035 2 +0.000010 72 +0.000009 26 +0.000028 4 diff --git a/docs/demos/install-bootstrap.txt b/docs/demos/install-bootstrap.txt new file mode 100644 index 0000000..06940fd --- /dev/null +++ b/docs/demos/install-bootstrap.txt @@ -0,0 +1,18 @@ +Script started on 2026-10-20 19:22:31-04:00 [COMMAND="python3 scripts/test-install-bootstrap.py" ] +test_bad_checksum_never_falls_back_or_replaces (__main__.Bootstrap.test_bad_checksum_never_falls_back_or_replaces) ... ok +test_default_uses_compatibility_repository_and_legacy_release (__main__.Bootstrap.test_default_uses_compatibility_repository_and_legacy_release) ... ok +test_missing_assets_preserve_existing_install (__main__.Bootstrap.test_missing_assets_preserve_existing_install) ... ok +test_new_archive_installs_both_commands (__main__.Bootstrap.test_new_archive_installs_both_commands) ... ok +test_new_archive_requires_both_commands (__main__.Bootstrap.test_new_archive_requires_both_commands) ... ok +test_non_404_failure_never_falls_back (__main__.Bootstrap.test_non_404_failure_never_falls_back) ... ok +test_pinned_legacy_release (__main__.Bootstrap.test_pinned_legacy_release) ... ok +test_root_installer_matches_compatibility_entrypoint (__main__.Bootstrap.test_root_installer_matches_compatibility_entrypoint) ... ok +test_unsupported_platform_preserves_existing_install (__main__.Bootstrap.test_unsupported_platform_preserves_existing_install) ... ok +test_wrong_version_preserves_existing_install (__main__.Bootstrap.test_wrong_version_preserves_existing_install) ... ok + +---------------------------------------------------------------------- +Ran 10 tests in 0.787s + +OK + +Script done on 2026-10-20 19:22:32-04:00 [COMMAND_EXIT_CODE="0"] From f9e9e849cd4ff9f5f6817c42f445885025a8b59a Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 20 Oct 2026 19:28:39 -0400 Subject: [PATCH 3/4] docs: link signed package guidance from installation reference --- docs/book/src/installation-reference.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/book/src/installation-reference.md b/docs/book/src/installation-reference.md index 5204adc..4b859cb 100644 --- a/docs/book/src/installation-reference.md +++ b/docs/book/src/installation-reference.md @@ -1,5 +1,7 @@ # Installation and agent reference +For Debian/Ubuntu packages, use the [signed apt repository guide](https://github.com/scbrown/apt#channels-and-pinning), including its fingerprint check, allowed update origins and rollback. The archive installer below is independent of apt. + ### Checksummed archives From 6f82b5f16cf25c5cf2e96782bf6514d3913a8867 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 20 Oct 2026 19:59:29 -0400 Subject: [PATCH 4/4] docs: align README template with canonical twelve-component table --- docs/stack/README-TEMPLATE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/stack/README-TEMPLATE.md b/docs/stack/README-TEMPLATE.md index 4495c2e..84d5ae6 100644 --- a/docs/stack/README-TEMPLATE.md +++ b/docs/stack/README-TEMPLATE.md @@ -126,9 +126,11 @@ when. | tool | what it gives your agents | |---|---| -| [caboodle](https://github.com/scbrown/caboodle) | 🧰 Agent Platform Toolkit - installs and configures the quipu stack | +| [tambo](https://github.com/scbrown/tambo) | 🧰 Agent Platform Toolkit - installs and configures the quipu stack | | [quipu](https://github.com/scbrown/quipu) | stores shared facts with provenance and history; can check writes against your rules | | [camayoc](https://github.com/scbrown/camayoc) | prepares knowledge and vocabulary for Quipu, with checks before it is admitted | +| [quechua](https://github.com/scbrown/quechua) | shared vocabulary and shapes for interoperable knowledge | +| [chaski](https://github.com/scbrown/chaski) | reacts to graph changes and delivers governed notifications | | [bobbin](https://github.com/scbrown/bobbin) | searches your code and can include related knowledge from Quipu | | [yupana](https://github.com/scbrown/yupana) | shows code impact and can read policies from Quipu to check changes | | [desire-path](https://github.com/scbrown/desire-path) | turns agent mistakes into feature requests; works on its own without Quipu |