Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .agents/skills/boatstack-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ For this operation, request only checksum-verified installation authority. Do no
request or materialize repository, provider, publication, product-delivery, or
merge authority. Installation receipts cannot be reused to broaden this scope.

If the candidate reports exact compiled-program drift, preserve the healthy old
launcher and present the prior program fingerprint, candidate program
If the candidate reports exact compiled-program drift, preserve the healthy admitted
runtime and present the prior program fingerprint, candidate program
fingerprint, and program-delta fingerprint. Do not accept the delta implicitly.
After explicit human acceptance, rerun the same checksum-bound update with
`--accept-program-change` so the Kernel uses the single atomic
`installation.reconcile-update` boundary. If the update has an interrupted local
transaction and `recovery.rollback` is permitted, carry the same human authority
through that rollback, preserve its complete receipt, and retry once from the
restored healthy old state. Never acquire repository authority to escape an
restored healthy prior runtime. Never acquire repository authority to escape an
update recovery frontier.

Begin each cycle with an untargeted authority-bearing `next`. A `CANDIDATE`
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/boatstack-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ For this operation, request only checksum-verified installation authority. Do no
request or materialize repository, provider, publication, product-delivery, or
merge authority. Installation receipts cannot be reused to broaden this scope.

If the candidate reports exact compiled-program drift, preserve the healthy old
launcher and present the prior program fingerprint, candidate program
If the candidate reports exact compiled-program drift, preserve the healthy admitted
runtime and present the prior program fingerprint, candidate program
fingerprint, and program-delta fingerprint. Do not accept the delta implicitly.
After explicit human acceptance, rerun the same checksum-bound update with
`--accept-program-change` so the Kernel uses the single atomic
`installation.reconcile-update` boundary. If the update has an interrupted local
transaction and `recovery.rollback` is permitted, carry the same human authority
through that rollback, preserve its complete receipt, and retry once from the
restored healthy old state. Never acquire repository authority to escape an
restored healthy prior runtime. Never acquire repository authority to escape an
update recovery frontier.

Begin each cycle with an untargeted authority-bearing `next`. A `CANDIDATE`
Expand Down
6 changes: 3 additions & 3 deletions .cursor/commands/boatstack-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ For this operation, request only checksum-verified installation authority. Do no
request or materialize repository, provider, publication, product-delivery, or
merge authority. Installation receipts cannot be reused to broaden this scope.

If the candidate reports exact compiled-program drift, preserve the healthy old
launcher and present the prior program fingerprint, candidate program
If the candidate reports exact compiled-program drift, preserve the healthy admitted
runtime and present the prior program fingerprint, candidate program
fingerprint, and program-delta fingerprint. Do not accept the delta implicitly.
After explicit human acceptance, rerun the same checksum-bound update with
`--accept-program-change` so the Kernel uses the single atomic
`installation.reconcile-update` boundary. If the update has an interrupted local
transaction and `recovery.rollback` is permitted, carry the same human authority
through that rollback, preserve its complete receipt, and retry once from the
restored healthy old state. Never acquire repository authority to escape an
restored healthy prior runtime. Never acquire repository authority to escape an
update recovery frontier.

Begin each cycle with an untargeted authority-bearing `next`. A `CANDIDATE`
Expand Down
6 changes: 3 additions & 3 deletions .gemini/skills/boatstack-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ For this operation, request only checksum-verified installation authority. Do no
request or materialize repository, provider, publication, product-delivery, or
merge authority. Installation receipts cannot be reused to broaden this scope.

If the candidate reports exact compiled-program drift, preserve the healthy old
launcher and present the prior program fingerprint, candidate program
If the candidate reports exact compiled-program drift, preserve the healthy admitted
runtime and present the prior program fingerprint, candidate program
fingerprint, and program-delta fingerprint. Do not accept the delta implicitly.
After explicit human acceptance, rerun the same checksum-bound update with
`--accept-program-change` so the Kernel uses the single atomic
`installation.reconcile-update` boundary. If the update has an interrupted local
transaction and `recovery.rollback` is permitted, carry the same human authority
through that rollback, preserve its complete receipt, and retry once from the
restored healthy old state. Never acquire repository authority to escape an
restored healthy prior runtime. Never acquire repository authority to escape an
update recovery frontier.

Begin each cycle with an untargeted authority-bearing `next`. A `CANDIDATE`
Expand Down
13 changes: 12 additions & 1 deletion .github/tests/test_detached_supervision.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import hashlib
import json
import os
import shutil
import subprocess
import tempfile
import unittest
Expand Down Expand Up @@ -40,6 +41,7 @@ def setUp(self) -> None:
self.addCleanup(self.work.cleanup)
root = Path(self.work.name)
self.state_root = root / "state"
self.boatstack_home = root / "boatstack-home"
self.repo = root / "repo"
self.repo.mkdir()
self._git(self.repo, "init", "-b", "main")
Expand All @@ -48,6 +50,13 @@ def setUp(self) -> None:
(self.repo / "README.md").write_text("# fixture\n")
self._git(self.repo, "add", "README.md")
self._git(self.repo, "commit", "-m", "fixture")
digest = hashlib.sha256(self.binary.read_bytes()).hexdigest()
version = self.run_helper("version").stdout.strip()
runtime_dir = self.boatstack_home / "runtimes" / f"{version}-{digest}"
runtime_dir.mkdir(parents=True)
runtime = runtime_dir / ("boatstack-runtime.exe" if os.name == "nt" else "boatstack-runtime")
shutil.copy2(self.binary, runtime)
runtime.chmod(0o755)

def _git(self, repository: Path, *args: str) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
Expand All @@ -59,6 +68,7 @@ def _git(self, repository: Path, *args: str) -> subprocess.CompletedProcess[str]
def _env(self) -> dict[str, str]:
env = dict(os.environ)
env["BOATSTACK_STATE_ROOT"] = str(self.state_root)
env["BOATSTACK_HOME"] = str(self.boatstack_home)
return env

def run_helper(
Expand Down Expand Up @@ -174,6 +184,7 @@ def test_detached_installation_and_engaged_guard_use_the_same_kernel(self) -> No
"?? .agents/skills/boatstack-update/agents/openai.yaml",
"?? .boatstack/host-skills.json",
"?? .boatstack/project.json",
"?? .boatstack/runtime.json",
"?? .claude/skills/boatstack-autoplan/SKILL.md",
"?? .claude/skills/boatstack-run/SKILL.md",
"?? .claude/skills/boatstack-update/SKILL.md",
Expand Down Expand Up @@ -345,7 +356,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali
"next", "--repo", self.repo,
"--transition", "installation.initialize", *goal, *flow, *actor,
"--param", f"source_revision={self._git(self.repo, 'rev-parse', 'HEAD').stdout.strip()}",
"--param", f"runtime_path={self.binary.resolve()}",
"--param", f"runtime_version={self.run_helper('version').stdout.strip()}",
"--param", f"runtime_sha256={hashlib.sha256(self.binary.read_bytes()).hexdigest()}",
"--param", f"config_path={config}",
"--param", f"config_sha256={config_fingerprint}",
Expand Down
45 changes: 28 additions & 17 deletions .github/tests/test_repository_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def setUpClass(cls) -> None:
):
ldflags = " ".join(
(
f"-X github.com/operatorstack/boatstack/boatstack.Version={version}",
f"-X github.com/operatorstack/boatstack/boatstack.SourceCommit={source}",
f"-X github.com/operatorstack/boatstack/boatstack/internal/buildinfo.Version={version}",
f"-X github.com/operatorstack/boatstack/boatstack/internal/buildinfo.SourceCommit={source}",
)
)
result = subprocess.run(
Expand Down Expand Up @@ -111,7 +111,7 @@ def test_release_builds_six_checksum_bound_v2_runtimes(self) -> None:
"boatstack-helper_windows_arm64.exe",
):
self.assertIn(asset, release)
for symbol in ("boatstack.Version", "boatstack.SourceCommit", "boatstack.ChecksumsSHA256"):
for symbol in ("internal/buildinfo.Version", "internal/buildinfo.SourceCommit", "internal/buildinfo.ChecksumsSHA256"):
self.assertIn(symbol, release)
self.assertIn('source_commit="$(git rev-parse HEAD)"', release)
self.assertIn("sha256sum", release)
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_operation_skills_are_three_distinct_authority_preserving_surfaces(self)
)
self.assertNotIn("repository-policy source remains requested", update)
for contract in (
"preserve the healthy old\nlauncher",
"preserve the healthy admitted\nruntime",
"program-delta fingerprint",
"Do not accept the delta implicitly",
"`--accept-program-change`",
Expand Down Expand Up @@ -422,12 +422,12 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
install_dir = root / "bin"
repository.mkdir()
self.init_repository(repository)
digest = hashlib.sha256(self.helper.read_bytes()).hexdigest()
digest = hashlib.sha256(self.old_helper.read_bytes()).hexdigest()
env = dict(os.environ)
env.update(
{
"BOATSTACK_REPO": str(repository),
"BOATSTACK_BINARY": str(self.helper),
"BOATSTACK_BINARY": str(self.old_helper),
"BOATSTACK_BINARY_SHA256": digest,
"BOATSTACK_INSTALL_DIR": str(install_dir),
"BOATSTACK_CONFIG": str(CONFIG),
Expand All @@ -438,7 +438,8 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
)
self.run_command("bash", REPO / "install.sh", cwd=repository, env=env)
launcher = install_dir / "boatstack"
self.assertTrue(launcher.is_symlink())
self.assertTrue(launcher.is_file())
self.assertFalse(launcher.is_symlink())
self.assertTrue((repository / ".boatstack" / "project.json").is_file())

doctor = json.loads(
Expand Down Expand Up @@ -487,21 +488,26 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
self.assertFalse(destructive["guard"]["allowed"])

env["BOATSTACK_MODE"] = "update"
env["BOATSTACK_VERSION"] = "contract-v2-next"
env["BOATSTACK_BINARY"] = str(self.helper)
env["BOATSTACK_BINARY_SHA256"] = hashlib.sha256(self.helper.read_bytes()).hexdigest()
env["BOATSTACK_ACCEPT_PROGRAM_CHANGE"] = "true"
self.run_command("bash", REPO / "install.sh", cwd=repository, env=env)
updated = json.loads(
self.run_command(launcher, "doctor", "--repo", repository, env=env).stdout
)
self.assertTrue(updated["doctor"]["healthy"])
self.assertIn("contract-v2-next", updated["snapshot"]["invocation"]["runtime_path"])
pin = json.loads((repository / ".boatstack" / "runtime.json").read_text())
self.assertEqual(pin["version"], "v0.7.contract-new")
self.assertEqual(pin["sha256"], hashlib.sha256(self.helper.read_bytes()).hexdigest())
self.assertNotIn("path", pin)
events = self.run_command(
launcher, "events", "--repo", repository, "--format", "jsonl", env=env
).stdout.splitlines()
transitions = {json.loads(line)["transition_id"] for line in events}
self.assertTrue({"installation.initialize", "engagement.begin", "installation.update"}.issubset(transitions))
self.assertTrue({"installation.initialize", "engagement.begin", "installation.reconcile-update"}.issubset(transitions))

def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> None:
# control-law: accepted-program-delta-atomically-installs-runtime-launcher-and-program
# control-law: accepted-program-delta-atomically-pins-runtime-and-program
if os.name == "nt":
self.skipTest("the repository contract job exercises the POSIX installer")
with tempfile.TemporaryDirectory() as temp:
Expand All @@ -526,7 +532,8 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No
)
self.run_command("bash", REPO / "install.sh", cwd=repository, env=env)
launcher = install_dir / "boatstack"
old_target = os.readlink(launcher)
old_launcher = launcher.read_bytes()
old_pin = (repository / ".boatstack" / "runtime.json").read_bytes()
state_path = next((repository / ".git" / "boatstack").rglob("state.json"))
state_before = state_path.read_bytes()

Expand Down Expand Up @@ -570,7 +577,8 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No
"catalog reconciliation cannot activate a different runtime",
split_reconciliation.stdout + split_reconciliation.stderr,
)
self.assertEqual(os.readlink(launcher), old_target)
self.assertEqual(launcher.read_bytes(), old_launcher)
self.assertEqual((repository / ".boatstack" / "runtime.json").read_bytes(), old_pin)
self.assertEqual(state_path.read_bytes(), state_before)
rejected = self.run_command(
"bash", REPO / "install.sh", cwd=repository, env=env, expected=1
Expand All @@ -589,12 +597,16 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No
self.assertRegex(change["program_delta_fingerprint"], r"^[0-9a-f]{64}$")
self.assertEqual(change["required_transition"], "installation.reconcile-update")
self.assertEqual(change["acceptance_flag"], "--accept-program-change")
self.assertEqual(os.readlink(launcher), old_target)
self.assertEqual(launcher.read_bytes(), old_launcher)
self.assertEqual((repository / ".boatstack" / "runtime.json").read_bytes(), old_pin)
self.assertEqual(state_path.read_bytes(), state_before)

env["BOATSTACK_ACCEPT_PROGRAM_CHANGE"] = "true"
self.run_command("bash", REPO / "install.sh", cwd=repository, env=env)
self.assertIn("contract-new", os.readlink(launcher))
pin = json.loads((repository / ".boatstack" / "runtime.json").read_text())
self.assertEqual(pin["version"], "v0.7.contract-new")
self.assertEqual(pin["sha256"], hashlib.sha256(self.helper.read_bytes()).hexdigest())
self.assertNotIn("path", pin)
doctor = json.loads(
self.run_command(launcher, "doctor", "--repo", repository, env=env).stdout
)
Expand All @@ -615,9 +627,8 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No
self.assertRegex(update["prior_program_fingerprint"], r"^[0-9a-f]{64}$")
self.assertRegex(update["program_fingerprint"], r"^[0-9a-f]{64}$")
self.assertRegex(update["program_delta_fingerprint"], r"^[0-9a-f]{64}$")
candidate = install_dir / os.readlink(launcher)
self.assertEqual(
update["runtime_fingerprint"], hashlib.sha256(candidate.read_bytes()).hexdigest()
update["runtime_fingerprint"], hashlib.sha256(self.helper.read_bytes()).hexdigest()
)
self.assertEqual(update["runtime_source_revision"], "b" * 40)

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
mkdir -p dist
cd boatstack
go build -trimpath \
-ldflags "-s -w -X github.com/operatorstack/boatstack/boatstack.Version=$VERSION -X github.com/operatorstack/boatstack/boatstack.SourceCommit=$source_commit -X github.com/operatorstack/boatstack/boatstack.ChecksumsSHA256=per-asset-sidecar" \
-ldflags "-s -w -X github.com/operatorstack/boatstack/boatstack/internal/buildinfo.Version=$VERSION -X github.com/operatorstack/boatstack/boatstack/internal/buildinfo.SourceCommit=$source_commit -X github.com/operatorstack/boatstack/boatstack/internal/buildinfo.ChecksumsSHA256=per-asset-sidecar" \
-o "../dist/$ASSET" ./cmd/boatstack-helper
cd ../dist
sha256sum "$ASSET" > "$ASSET.sha256"
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ On Windows PowerShell:
irm https://raw.githubusercontent.com/operatorstack/boatstack/main/install.ps1 | iex
```

The installer verifies the release checksum, runs the registered
`installation.initialize` transition, and then installs the launcher. Review
and commit `.boatstack/project.json`, `.boatstack/host-skills.json`, and the
generated host skills; machine-local controller state stays outside the
worktree.
The installer verifies the release checksum, stores the runtime under its
immutable version and digest, runs `installation.initialize`, and installs a
stable dispatcher. Review and commit `.boatstack/project.json`,
`.boatstack/runtime.json`, `.boatstack/host-skills.json`, and the generated host
skills; machine-local controller state and runtime bytes stay outside the
worktree. See [Runtime selection](docs/runtime-selection.md).

Run the independent health query:

Expand Down Expand Up @@ -107,9 +108,10 @@ coding host:
- `boatstack-run` drives delivery through a normal PR open or update and never
grants merge authority.
- `boatstack-update` runs the checksum-verified installation update path. A
control-program change preserves the old launcher until a human accepts the
exact prior-to-candidate program delta; Boatstack then activates the runtime,
launcher, managed skills, and program state through one recoverable transition.
control-program change preserves the admitted runtime until a human accepts
the exact prior-to-candidate program delta; Boatstack then commits the new
repository pin, managed skills, and program state through one recoverable
transition.

Codex and compatible Agent Skills hosts show `$boatstack-autoplan`,
`$boatstack-run`, and `$boatstack-update`. Claude Code and Gemini CLI receive
Expand Down
Loading