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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Contributing

Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/0633de4b4ed8385f180284ae21c10cc2e9761399/labs/12-product-engineering-loop).
Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/340059469cf115431f0f91c0a88beb34374d1e5a/labs/12-product-engineering-loop).

The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR.

Expand Down
43 changes: 23 additions & 20 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 43583,
"estimated_tokens": 10896,
"characters": 43677,
"estimated_tokens": 10920,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
Expand All @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "c4a9a512b125fe3b19714b6ec09e9267480ea1653df2d9cf4f4c6173e17884c4",
"CONTRIBUTING.md": "79338a8a60c24719b8618b3ffe5b6112075d4035d9c2aec07a55aa50f09daa4c",
"README.md": "a840004db57a129ecd361bb0421adfe02065d86751736dd91953c8a8b178e5ef",
"assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea",
"assets/boatstack-mark.svg": "ec96165583b15cfd446c27049d49217973f3e9b1defa5771cc08eec0c9542ce4",
Expand All @@ -33,30 +33,32 @@
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
"boatstack/changelog.go": "5f0b1e7a66586c58aad03bfea8126178316f82ff6583bd67d35d6fcf71a6f9bc",
"boatstack/changelog_test.go": "2cee0d580dc2fa0752ee58564d030961b20139008de132ee8abff9e6b6713e28",
"boatstack/cmd/boatstack-helper/main.go": "b4b8b43d80dbf60f15e18885ff8ee01637df6e47c0249938714d885163350539",
"boatstack/cmd/boatstack-helper/main.go": "03637dce9ffd5e36355ec8c09bfdf4a9c34769808cdd7d330634e79b5bb9ac77",
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
"boatstack/delivery.go": "565bc8465252a798124a29874e1aeffff16e9027d1a86715e11b3e4bc0743bd2",
"boatstack/delivery_test.go": "2545c26389d330a92c11e7b09fb2342101db5d09a825a2a9e81b4ea9eea9407b",
"boatstack/export.go": "42b438d5b1976836ddb9322a09fa7949c044e2cd2ed0e50d4961329c0184b6a8",
"boatstack/export.go": "9f4b2b48ea19368232a7d335fc23f632e8a1081736582dab61fd1ba6125850a3",
"boatstack/export_test.go": "9474985bf2aab4f2f14d37dcc3cedc6d7a74db39739fbbfd7615297e35b079b5",
"boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641",
"boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017",
"boatstack/hooks.go": "1d5d8c4bf7e6e867c8bf07e391d86158347269f856647a5d256f345bbb8d3c96",
"boatstack/hooks_test.go": "c3f359416ea53f258d8747d0247381e8946efd4d4a5bcf072c4147f885475ad3",
"boatstack/init.go": "fb863a68a6cced5bd2dcf9cd8ca53f0dab7ef0309830269efd985ad8b8a92059",
"boatstack/init_test.go": "eb91d280077115c0dbe3d8472e1e4fa9794d5ae6e5a58afbb9c3f4f39aee80d8",
"boatstack/hooks.go": "ed131c526f434d7c3309a388b97f3d28df2a6bdf9bc7322499405d5174ab8518",
"boatstack/hooks_test.go": "6153759d463203607c50c418eb74017d2dc71118891f7567dec81fef9bd380d0",
"boatstack/init.go": "b0619af89a108442decb8848b670bb648e2a81726faa8e8aa22510a522248de5",
"boatstack/init_test.go": "fa48be69d07691fa7842224ec831e5f290504d8b6565263c9735d2dabd9b43b8",
"boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/next.go": "9a9b3b9dde0a528991d2b6ac358638b249a791a4eb3e7a2656bd43b652a2c628",
"boatstack/next_test.go": "1244fdf5e447646793ffdab81b8ff9403b849e9fa050a41bc9e7145bca08a0d2",
"boatstack/plan.go": "2497e1d32d88a8e433bc5996b57d175bda4e6ba334cf2510efb7c84fd88a98f3",
"boatstack/plan_test.go": "006cdc6681f77e579c5a0f709e30ede759c337132d4f2f5193b7b79b29bd7149",
"boatstack/planning.go": "3a26417a295e5dfc2b6dcac702287c04b6053e7b74215858a4ea11cf9f9dadfe",
"boatstack/planning.go": "1237ae552a72d5312c1ef61e30f430976f526d5473d91c7fbfcb9148eb7079b9",
"boatstack/planning_test.go": "6b156a64182ed76d4c3d392b4c5a26abe5d8b81cea27ee12ac7c4627c827e186",
"boatstack/pr.go": "ba537fc35cc8d2aaf584c5ba6de1194fdd1c4255dfb227a09478457752e2b9ad",
"boatstack/pr_test.go": "ae23130d9d96cf214cf272227aa572dd09e2fe3adac22921949f541ba99ecb23",
"boatstack/references/artifacts.md": "8f2e79b8af4bd3ad2e32aa3e8c07f10812555d0a3247e4991db75cc1cda395c3",
"boatstack/references/failure-moves.md": "1d35126348d0b681976e8819665e16fd745fd65eca271492603cb80aab75bf49",
"boatstack/references/irreversible-operation-boundary.md": "2a695f2d7de95cfc8750f107bef9c86581712aa1f02e7233b69b850d8c2af42e",
"boatstack/references/host-hook-contracts.md": "1382213ad004389de6da5a03af43ec28ace6329c9e7a3f07148566cf2ea12727",
"boatstack/references/irreversible-operation-boundary.md": "631743991ace65977586e4537f8dd50f8ae88f8e16f27cf7baad93b2791a73df",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
"boatstack/references/workflow.md": "b460b647bdd5d0ce4f1bd1015434a9e6c3b8ef525920ed018fb26a22bbc3aa9a",
"boatstack/release.go": "fa2ac926df89c90c5844e938a2e02d4b8dbbaefbf85bb7a1a89fc51690bea520",
Expand All @@ -65,9 +67,9 @@
"boatstack/run_test.go": "fdc416f15e787b5c8401fc0f0e3aeb58b4891c828a8a869c8dce4e8f1541d809",
"boatstack/runtime.go": "504b7b68e550ed178c2c12bb815aa21e67300cad65cf2311bb5e8b2860747f81",
"boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489",
"boatstack/runtime_cache_test.go": "4cbca9dec7800d7df6e3ec0d74c7ecbe1508e5c5a288d863f35fc8d22986c308",
"boatstack/safety.go": "8bcce4c11094b4018093ac5fb5a5256cc5c63c825ddf7ec1a46b9d0098557d33",
"boatstack/safety_test.go": "03885d5a93f42b6adba6eef6bbef0680d81c0e1442fb72626a105da4862aecdf",
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
"boatstack/safety.go": "6a70f6886a6a599dea60eaeea568c4a8df0f36c8a2f68f1f2c0fc2e28554584b",
"boatstack/safety_test.go": "cf07f67354e96437e72ddbe107a4071a39689bf2e50df4017e28909194be59e4",
"boatstack/skill_frontmatter.go": "85ba7389e5d41f52a716b912ffb6b03c81e575b0b049b2cc11f7cd45c3da93c4",
"boatstack/skill_frontmatter_test.go": "ad92021f44f09d99949bda6670a8e350af85254dee087eeb86f3815dbdf7f500",
"boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4",
Expand All @@ -78,14 +80,14 @@
"docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/evidence-engineered-coding.md": "e06074a2da10e6d1504bf19a3679095fa93d361fac8d66afc041eca44bac80d6",
"docs/evidence-engineered-coding.md": "3e51926eb4a23d1774b93c59e8cf2b670decabcb746bdd0db1dc2a790fa40f1f",
"docs/generated-files.md": "136422baf0c7fc2bd5100cfe0ebdb3d9d0705dfd7e7d54bf745dd1037e63492c",
"docs/getting-started.md": "c298c0d78054266099ae98232a4c29976fa4e176d4ace49b6a20a8c13c35bc51",
"docs/public-claims.json": "80ed7c175d3288ebe417dc1c4f9b6c60001c4a22453516f64d93c45534020cc3",
"docs/getting-started.md": "d57fa781176321dd25f2917488133f912ec1ef1645a3c43f9630db0485049dd8",
"docs/public-claims.json": "44bb7a014da3f53d627d441d4473262c712fdc80a19d979b8c89f906a4078a6e",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
"docs/troubleshooting.md": "6b3dc443752e88d13ca4b279fa8024061cb9095cc6822f7b79fa93d74fe8717c",
"docs/troubleshooting.md": "744786a3df7ff3ff52cb3e64393d1858332f8d178108fdac7b773a75c5bcc313",
"docs/validation-and-evidence.md": "e7d91ad49c6adb44784ebe7d94feceb6abd445857f9a0716f0758bf6b55296c5",
"docs/why-these-steps.md": "80957af13979070e8b2f2a8db78ce06d20d152bbc8ec41c3a8003f28393f6369",
"install.ps1": "960b2b20b406bb2878a560e9ace53fe7226bc510be6ee8466ce4e608beb5625a",
Expand All @@ -95,7 +97,7 @@
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
"labs/diagram-json/plan.lock.json": "ee380e4e0beedc036db86615fb112822614b10037d47a7bc1dbb45f142822f22",
"labs/diagram-json/plan.lock.json": "e28185904ad653ccf43858afaf5b17c07f773e22ed21b8d3f88987e753011a99",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand All @@ -119,13 +121,14 @@
"release-notes/2026-07-18-safety-sql-boundaries.md": "32011ca3d02a371e8f3f2899ffb34df3af0843d18d25e7db95fbca32c2dcf18c",
"release-notes/2026-07-18-stacked-bar-mark.md": "c4d5bd5fb89c280d7fba015384fd795fcb8c31ffe501078aa55a90cbcf66ba7b",
"release-notes/2026-07-18-startup-recovery-routing.md": "c305a1c2b8347e1bc6d7fcbd4a8e629438cdeb4bcf6f181a7d6fc154a88f4318",
"release-notes/2026-07-19-cross-harness-hook-conformance.md": "15dc22cc3b534c6d0bd89f34cd69cabf94a3610d445a039ba111fd8bce727e68",
"release-notes/2026-07-19-optional-repository-changelog.md": "7ae6bed436c269335ac5055e04438489451bef00836fecb3e5fcb06a18501853",
"release-notes/2026-07-19-valid-skill-frontmatter.md": "a0a002f7891474d7d1eea49e3234cae5ddbd941158726b83555dc48474181a92"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "0633de4b4ed8385f180284ae21c10cc2e9761399",
"commit": "340059469cf115431f0f91c0a88beb34374d1e5a",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
53 changes: 48 additions & 5 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"sort"
"strings"
"time"

boatstack "github.com/operatorstack/boatstack/boatstack"
)
Expand All @@ -16,6 +17,32 @@ func fail(err error) int {
return 1
}

const cursorHookSettleDelay = 50 * time.Millisecond

var hookOutputSleep = time.Sleep

func emitHookOutput(writer io.Writer, host string, value []byte) error {
if len(value) == 0 {
return nil
}
if _, err := writer.Write(value); err != nil {
return err
}
// Cursor currently has a host-side race that can lose output from compiled
// hooks which exit immediately. Keep the workaround isolated to its adapter.
if strings.EqualFold(strings.TrimSpace(host), "cursor") {
hookOutputSleep(cursorHookSettleDelay)
}
return nil
}

func failSafetyHook(err error) int {
fmt.Fprintln(os.Stderr, "BLOCKED:", err)
// Claude Code and Codex both define exit 2 as a blocking PreToolUse error.
// Exit 1 is non-blocking in Claude and must never represent policy failure.
return 2
}

func initCommand(arguments []string) int {
flags := flag.NewFlagSet("init", flag.ContinueOnError)
repo := flags.String("repo", ".", "repository to initialize")
Expand Down Expand Up @@ -383,6 +410,22 @@ func doctorCommand(arguments []string) int {
return fail(err)
}
fmt.Printf("PASS: Boatstack %s installation and generated adapters are healthy\n", boatstack.Version)
hosts, err := boatstack.DoctorHookHosts(*repo)
if err != nil {
return fail(err)
}
for _, host := range hosts {
name := strings.ToUpper(host)
fmt.Printf("HOST_CONTRACT_%s=PASS\nHOST_ACTIVATION_%s=OPERATOR_VERIFY\n", name, name)
switch host {
case "cursor":
fmt.Println("HOST_ACTIVATION_GUIDANCE_CURSOR=Reload Cursor and confirm both Boatstack hooks are enabled; Cursor hooks remain defense in depth.")
case "claude":
fmt.Println("HOST_ACTIVATION_GUIDANCE_CLAUDE=Reload Claude Code and use /hooks to confirm the Boatstack PreToolUse hook is active.")
case "codex":
fmt.Println("HOST_ACTIVATION_GUIDANCE_CODEX=Trust this linked worktree, use /hooks to review and trust the exact Boatstack hook, then start a new task.")
}
}
if update, ok := boatstack.CachedUpdate(*repo); ok {
fmt.Printf("UPDATE_AVAILABLE=%s\nRELEASE_URL=%s\n", update.LatestVersion, update.ReleaseURL)
}
Expand All @@ -401,8 +444,8 @@ func safetyHookCommand(arguments []string) int {
input = nil
}
value, _ := boatstack.HookDecision(boatstack.SafetyHookOptions{Host: *host, Repo: *repo, Input: input})
if len(value) > 0 {
fmt.Print(string(value))
if err := emitHookOutput(os.Stdout, *host, value); err != nil {
return failSafetyHook(fmt.Errorf("cannot emit hook decision: %w", err))
}
return 0
}
Expand All @@ -419,11 +462,11 @@ func bootstrapSafetyHookCommand(arguments []string) int {
input = nil
}
if err := boatstack.HydrateWorktree(*repo); err != nil {
return fail(fmt.Errorf("worktree runtime activation failed: %w", err))
return failSafetyHook(fmt.Errorf("worktree runtime activation failed: %w", err))
}
value, _ := boatstack.HookDecision(boatstack.SafetyHookOptions{Host: *host, Repo: *repo, Input: input})
if len(value) > 0 {
fmt.Print(string(value))
if err := emitHookOutput(os.Stdout, *host, value); err != nil {
return failSafetyHook(fmt.Errorf("cannot emit hook decision: %w", err))
}
return 0
}
Expand Down
158 changes: 158 additions & 0 deletions boatstack/cmd/boatstack-helper/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"bytes"
"context"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

boatstack "github.com/operatorstack/boatstack/boatstack"
)

func TestEmitHookOutputSettlesOnlyCursor(t *testing.T) {
previous := hookOutputSleep
defer func() { hookOutputSleep = previous }()
delays := []time.Duration{}
hookOutputSleep = func(value time.Duration) { delays = append(delays, value) }

var output bytes.Buffer
if err := emitHookOutput(&output, "cursor", []byte("{\"permission\":\"deny\"}\n")); err != nil {
t.Fatal(err)
}
if output.String() != "{\"permission\":\"deny\"}\n" {
t.Fatalf("unexpected output: %q", output.String())
}
if len(delays) != 1 || delays[0] != cursorHookSettleDelay {
t.Fatalf("Cursor settle delays = %v", delays)
}

delays = nil
if err := emitHookOutput(&output, "claude", []byte("{}\n")); err != nil {
t.Fatal(err)
}
if err := emitHookOutput(&output, "codex", nil); err != nil {
t.Fatal(err)
}
if len(delays) != 0 {
t.Fatalf("non-Cursor hosts unexpectedly settled: %v", delays)
}
}

func TestBootstrapFailureUsesBlockingExitCode(t *testing.T) {
if code := bootstrapSafetyHookCommand([]string{"--host", "claude", "--repo", t.TempDir()}); code != 2 {
t.Fatalf("bootstrap failure exit = %d, want 2", code)
}
}

func liveHostArguments(host, prompt string) []string {
switch host {
case "cursor":
return []string{"-p", "--force", prompt}
case "claude":
return []string{"-p", "--dangerously-skip-permissions", prompt}
case "codex":
return []string{"exec", "--dangerously-bypass-hook-trust", "--sandbox", "danger-full-access", prompt}
default:
return nil
}
}

func liveHostRepo(t *testing.T, host, helper string) string {
t.Helper()
repo := t.TempDir()
if output, err := exec.Command("git", "-C", repo, "init", "-b", "main").CombinedOutput(); err != nil {
t.Fatalf("git init: %v: %s", err, output)
}
config := boatstack.ProjectConfig{
SchemaVersion: 1,
Project: boatstack.Project{Name: "live-hook-" + host, DefaultBranch: "main", Commands: map[string]string{"test": "true"}},
Workflow: boatstack.Workflow{HumanPlanApproval: true, IndependentReviewForHighRisk: true, AllowPassWithGaps: true},
Adapters: []string{host},
}
value, err := boatstack.MarshalJSON(config)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repo, ".boatstack-project.json"), value, 0o644); err != nil {
t.Fatal(err)
}
if err := boatstack.RunInit(boatstack.InitOptions{Repo: repo, BinaryPath: helper, IntegrationChoice: "core", Yes: true, Output: io.Discard}); err != nil {
t.Fatal(err)
}
scriptDirectory := filepath.Join(repo, "scripts")
if err := os.MkdirAll(scriptDirectory, 0o755); err != nil {
t.Fatal(err)
}
// The script itself has a harmless side effect. The policy marker in the
// comment makes Boatstack deny its invocation, so a sentinel proves whether
// the real host honored the hook without executing a destructive operation.
script := "#!/usr/bin/env bash\n# git reset --hard is operator-only\ntouch \"$1\"\n"
if err := os.WriteFile(filepath.Join(scriptDirectory, "blocked-probe.sh"), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
return repo
}

func TestLiveHostHarnessesAreExplicitlyOptIn(t *testing.T) {
if os.Getenv("BOATSTACK_LIVE_HOST_TESTS") != "1" {
t.Skip("set BOATSTACK_LIVE_HOST_TESTS=1 to exercise installed host harnesses")
}
commands := map[string]string{"cursor": "cursor-agent", "claude": "claude", "codex": "codex"}
available := map[string]string{}
for host, command := range commands {
if path, err := exec.LookPath(command); err == nil {
available[host] = path
}
}
if len(available) == 0 {
t.Skip("no supported live host is installed")
}
helper := filepath.Join(t.TempDir(), "boatstack-helper")
if output, err := exec.Command("go", "build", "-o", helper, ".").CombinedOutput(); err != nil {
t.Fatalf("build live helper: %v: %s", err, output)
}
for _, host := range []string{"cursor", "claude", "codex"} {
path := available[host]
if path == "" {
continue
}
t.Run(host, func(t *testing.T) {
repo := liveHostRepo(t, host, helper)
safe := filepath.Join(repo, "safe-sentinel")
blocked := filepath.Join(repo, "blocked-sentinel")
prompts := []struct {
text string
sentinel string
wantExists bool
}{
{"Use the shell exactly once to run: touch " + safe, safe, true},
{"Use the shell exactly once to run: bash scripts/blocked-probe.sh " + blocked, blocked, false},
}
for _, probe := range prompts {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
command := exec.CommandContext(ctx, path, liveHostArguments(host, probe.text)...)
command.Dir = repo
output, runErr := command.CombinedOutput()
cancel()
if ctx.Err() == context.DeadlineExceeded {
t.Fatalf("%s live probe timed out", host)
}
exists := false
if _, err := os.Stat(probe.sentinel); err == nil {
exists = true
}
if exists != probe.wantExists {
t.Fatalf("%s sentinel exists=%t want=%t; err=%v output=%s", host, exists, probe.wantExists, runErr, output)
}
if !probe.wantExists && !strings.Contains(string(output), "Boatstack denied") {
t.Fatalf("%s blocked probe lacked Boatstack evidence: err=%v output=%s", host, runErr, output)
}
}
})
}
}
Loading
Loading