Skip to content

Commit e40b989

Browse files
committed
fix: hydrate detached launcher bootstrap runtime
1 parent 25ba13c commit e40b989

9 files changed

Lines changed: 232 additions & 5 deletions

File tree

boatstack/hydrate_runtime_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"os"
66
"path/filepath"
7+
"runtime"
78
"strings"
89
"testing"
910
)
@@ -51,6 +52,103 @@ func TestRunHydrateRuntimePopulatesSlotIdempotentlyWithoutTouchingCommittedState
5152
}
5253
}
5354

55+
// control-law: tracked-launcher-selects-only-the-pinned-runtime
56+
// Positive and relation conformance: detached hydration must publish the
57+
// Git-common bootstrap consumed by tracked launchers and the external shared
58+
// runtime consumed by supervision-aware worktree activation.
59+
func TestRunHydrateRuntimePopulatesDetachedBootstrapAndSharedSlots(t *testing.T) {
60+
t.Setenv(stateRootEnv, t.TempDir())
61+
invalidateWorkspaceCache()
62+
repo := runtimeTestRepo(t)
63+
result, err := AttachDetached(AttachOptions{Repo: repo, BinaryPath: os.Args[0]})
64+
if err != nil || result.VerificationStatus != "VERIFIED" {
65+
t.Fatalf("attach failed: %v %+v", err, result)
66+
}
67+
sharedBinary, _, err := sharedRuntimePaths(repo, Version, SourceCommit)
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
bootstrapBinary, _, err := bootstrapRuntimePaths(repo, Version, SourceCommit)
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
if filepath.Clean(sharedBinary) == filepath.Clean(bootstrapBinary) {
76+
t.Fatalf("detached shared and bootstrap slots unexpectedly alias: %s", sharedBinary)
77+
}
78+
for _, path := range []string{filepath.Dir(sharedBinary), filepath.Dir(bootstrapBinary), filepath.Join(repo, ".product-loop", "bin")} {
79+
if err := os.RemoveAll(path); err != nil {
80+
t.Fatal(err)
81+
}
82+
}
83+
before := readGeneratedLockBytes(t, repo)
84+
85+
if err := RunHydrateRuntime(repo); err != nil {
86+
t.Fatalf("detached hydration failed: %v", err)
87+
}
88+
for name, path := range map[string]string{"Git-common bootstrap": bootstrapBinary, "detached shared runtime": sharedBinary} {
89+
if _, err := os.Stat(path); err != nil {
90+
t.Fatalf("%s was not populated: %v", name, err)
91+
}
92+
}
93+
if err := verifyLocalRuntime(repo); err != nil {
94+
t.Fatalf("detached hydration did not activate the local runtime: %v", err)
95+
}
96+
if after := readGeneratedLockBytes(t, repo); string(before) != string(after) {
97+
t.Fatalf("detached hydration mutated committed generated.lock.json")
98+
}
99+
if err := RunHydrateRuntime(repo); err != nil {
100+
t.Fatalf("second detached hydration was not idempotent: %v", err)
101+
}
102+
}
103+
104+
// control-law: tracked-launcher-selects-only-the-pinned-runtime
105+
// Negative, bypass, and failure-state conformance: an unsafe detached shared
106+
// path must fail before publishing an admissible bootstrap or local runtime.
107+
func TestRunHydrateRuntimeRejectsUnsafeDetachedSharedSlotBeforeBootstrap(t *testing.T) {
108+
if runtime.GOOS == "windows" {
109+
t.Skip("symlink creation requires privileges not guaranteed on Windows CI")
110+
}
111+
t.Setenv(stateRootEnv, t.TempDir())
112+
invalidateWorkspaceCache()
113+
repo := runtimeTestRepo(t)
114+
result, err := AttachDetached(AttachOptions{Repo: repo, BinaryPath: os.Args[0]})
115+
if err != nil || result.VerificationStatus != "VERIFIED" {
116+
t.Fatalf("attach failed: %v %+v", err, result)
117+
}
118+
sharedBinary, _, err := sharedRuntimePaths(repo, Version, SourceCommit)
119+
if err != nil {
120+
t.Fatal(err)
121+
}
122+
bootstrapBinary, _, err := bootstrapRuntimePaths(repo, Version, SourceCommit)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
for _, path := range []string{filepath.Dir(sharedBinary), filepath.Dir(bootstrapBinary), filepath.Join(repo, ".product-loop", "bin")} {
127+
if err := os.RemoveAll(path); err != nil {
128+
t.Fatal(err)
129+
}
130+
}
131+
if err := os.MkdirAll(filepath.Dir(filepath.Dir(sharedBinary)), 0o755); err != nil {
132+
t.Fatal(err)
133+
}
134+
if err := os.Symlink(t.TempDir(), filepath.Dir(sharedBinary)); err != nil {
135+
t.Fatal(err)
136+
}
137+
138+
err = RunHydrateRuntime(repo)
139+
if err == nil || !strings.Contains(err.Error(), "symlink") {
140+
t.Fatalf("expected unsafe detached slot refusal, got %v", err)
141+
}
142+
for name, path := range map[string]string{
143+
"bootstrap runtime": bootstrapBinary,
144+
"worktree runtime": filepath.Join(repo, ".product-loop", "bin", helperName()),
145+
} {
146+
if _, statErr := os.Lstat(path); !os.IsNotExist(statErr) {
147+
t.Fatalf("failed hydration partially published %s: %v", name, statErr)
148+
}
149+
}
150+
}
151+
54152
// TestRunHydrateRuntimeRefusesRunningVersusPinMismatch pins the incident-
55153
// prevention invariant: hydration must never populate a version-keyed slot with
56154
// a binary whose identity disagrees with the worktree's committed pin.

boatstack/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ func RunInit(options InitOptions) (returnErr error) {
566566
}
567567
}
568568
}()
569-
if _, err := installSharedRuntime(helperSource, repo, config.Integrations); err != nil {
569+
if _, err := installCommandRuntime(helperSource, repo, config.Integrations); err != nil {
570570
return fmt.Errorf("cannot install the repository-family Boatstack runtime: %w", err)
571571
}
572572
var states map[string]IntegrationState
@@ -644,7 +644,7 @@ func RunInit(options InitOptions) (returnErr error) {
644644
if err := initCheckpoint("helper-written"); err != nil {
645645
return fmt.Errorf("initialization checkpoint helper-written: %w", err)
646646
}
647-
if _, err := installSharedRuntime(helperSource, repo, states); err != nil {
647+
if _, err := installCommandRuntime(helperSource, repo, states); err != nil {
648648
return fmt.Errorf("cannot finalize the repository-family Boatstack runtime: %w", err)
649649
}
650650
if err := writeInstallLock(repo, binaryPath, binaryHash, states); err != nil {

boatstack/launcher_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,50 @@ func TestTrackedLauncherActivatesFreshLinkedWorktreeWithoutHookTrust(t *testing.
148148
}
149149
}
150150

151+
// control-law: tracked-launcher-selects-only-the-pinned-runtime
152+
// Relation conformance for the detached failure mode: tracked launcher -> exact
153+
// hydrate operation -> Git-common bootstrap -> detached shared activation ->
154+
// verified local command dispatch.
155+
func TestTrackedLauncherHydratesDetachedRepositoryThroughGitCommonBootstrap(t *testing.T) {
156+
t.Setenv(stateRootEnv, t.TempDir())
157+
invalidateWorkspaceCache()
158+
primary, _ := launcherTestRepository(t)
159+
helper := buildLauncherTestHelper(t)
160+
result, err := AttachDetached(AttachOptions{Repo: primary, BinaryPath: helper})
161+
if err != nil || result.VerificationStatus != "VERIFIED" {
162+
t.Fatalf("attach failed: %v %+v", err, result)
163+
}
164+
sharedBinary, _, err := sharedRuntimePaths(primary, Version, SourceCommit)
165+
if err != nil {
166+
t.Fatal(err)
167+
}
168+
bootstrapBinary, _, err := bootstrapRuntimePaths(primary, Version, SourceCommit)
169+
if err != nil {
170+
t.Fatal(err)
171+
}
172+
for _, path := range []string{filepath.Dir(sharedBinary), filepath.Dir(bootstrapBinary), filepath.Join(primary, ".product-loop", "bin")} {
173+
if err := os.RemoveAll(path); err != nil {
174+
t.Fatal(err)
175+
}
176+
}
177+
hydrate := quotedLiteral(t, helper) + " hydrate-runtime --repo " + quotedLiteral(t, primary)
178+
command := launcherCommand(primary, "version")
179+
command.Dir = primary
180+
command.Env = append(os.Environ(), "BOATSTACK_HYDRATE_COMMAND="+hydrate)
181+
value, runErr := command.CombinedOutput()
182+
if runErr != nil || !strings.Contains(string(value), Version) {
183+
t.Fatalf("detached launcher hydration failed: %v\n%s", runErr, value)
184+
}
185+
for name, path := range map[string]string{"Git-common bootstrap": bootstrapBinary, "detached shared runtime": sharedBinary} {
186+
if _, err := os.Stat(path); err != nil {
187+
t.Fatalf("%s missing after launcher hydration: %v", name, err)
188+
}
189+
}
190+
if err := verifyLocalRuntime(primary); err != nil {
191+
t.Fatalf("launcher did not dispatch through a verified local runtime: %v", err)
192+
}
193+
}
194+
151195
func runErrString(err error, output []byte) string {
152196
if err == nil {
153197
return string(output)

boatstack/paths.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,23 @@ func (w WorkspaceContext) RuntimeDir(version, sourceCommit string) (string, erro
386386
}
387387
return filepath.Join(base, "runtimes", version, sourceCommit, platformKey()), nil
388388
}
389+
390+
// BootstrapRuntimeDir holds the exact runtime used by tracked launchers and
391+
// repository guards before supervision mode can be resolved by trusted Go code.
392+
// It is always Git-common, including for detached supervision. The bootstrap
393+
// helper then activates the mode-aware shared runtime through HydrateWorktree.
394+
func (w WorkspaceContext) BootstrapRuntimeDir(version, sourceCommit string) (string, error) {
395+
version, err := safeCacheSegment(version, "Boatstack version")
396+
if err != nil {
397+
return "", err
398+
}
399+
sourceCommit, err = safeCacheSegment(sourceCommit, "source commit")
400+
if err != nil {
401+
return "", err
402+
}
403+
common, err := gitCommonDir(w.RepoRoot)
404+
if err != nil {
405+
return "", err
406+
}
407+
return filepath.Join(common, controlDirName, "runtimes", version, sourceCommit, platformKey()), nil
408+
}

boatstack/references/artifacts.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ clone, `external` outside the repository (Detached Supervision).
151151
| flow-logs | runtime-worktree | per-worktree | flow |
152152
| guard-denial-ledger | runtime-worktree | per-worktree | safety-hook, ambient-safety-hook |
153153
| runtime-slots | runtime-shared | git-common | init, update, hydrate-runtime |
154+
| runtime-bootstrap-slots | runtime-shared | git-common | init, update, hydrate-runtime |
154155
| mutation-receipts | runtime-shared | git-common | activate-plan, undo |
155156
| update-previews | runtime-shared | git-common | prepare-update-pr, publish-update-pr |
156157
| repair-receipts | runtime-shared | git-common | update |

boatstack/runtime_cache.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ func sharedRuntimeOwnedPaths(repo, version, sourceCommit string) (controllerPath
112112
return binary, manifest, err
113113
}
114114

115+
func bootstrapRuntimePaths(repo, version, sourceCommit string) (string, string, error) {
116+
binary, manifest, err := bootstrapRuntimeOwnedPaths(repo, version, sourceCommit)
117+
return binary.path, manifest.path, err
118+
}
119+
120+
func bootstrapRuntimeOwnedPaths(repo, version, sourceCommit string) (controllerPath, controllerPath, error) {
121+
ctx := WorkspaceFor(repo)
122+
directory, err := ctx.BootstrapRuntimeDir(version, sourceCommit)
123+
if err != nil {
124+
return controllerPath{}, controllerPath{}, err
125+
}
126+
common, err := gitCommonDir(repo)
127+
if err != nil {
128+
return controllerPath{}, controllerPath{}, err
129+
}
130+
binary, err := newControllerPath(common, filepath.Join(directory, helperName()))
131+
if err != nil {
132+
return controllerPath{}, controllerPath{}, err
133+
}
134+
manifest, err := newControllerPath(common, filepath.Join(directory, "runtime.lock.json"))
135+
return binary, manifest, err
136+
}
137+
115138
func atomicWriteMode(path string, content []byte, mode fs.FileMode) error {
116139
directory := filepath.Dir(path)
117140
if err := os.MkdirAll(directory, 0o755); err != nil {
@@ -154,6 +177,31 @@ func installSharedRuntime(source, repo string, integrations map[string]Integrati
154177
return writeRuntimeSlot(source, binaryPath, manifestPath, integrations)
155178
}
156179

180+
// installCommandRuntime publishes the exact runtime needed by both sides of
181+
// tracked command activation. The mode-aware shared slot is installed first so
182+
// the Git-common bootstrap is never made admissible before it can activate the
183+
// worktree-local helper. Embedded mode uses one physical slot; detached mode
184+
// deliberately uses an external shared slot plus a Git-common bootstrap slot.
185+
// control-law: tracked-launcher-selects-only-the-pinned-runtime
186+
func installCommandRuntime(source, repo string, integrations map[string]IntegrationState) (runtimeManifest, error) {
187+
sharedManifest, err := installSharedRuntime(source, repo, integrations)
188+
if err != nil {
189+
return runtimeManifest{}, err
190+
}
191+
sharedBinary, _, err := sharedRuntimePaths(repo, Version, SourceCommit)
192+
if err != nil {
193+
return runtimeManifest{}, err
194+
}
195+
bootstrapBinary, bootstrapLock, err := bootstrapRuntimeOwnedPaths(repo, Version, SourceCommit)
196+
if err != nil {
197+
return runtimeManifest{}, err
198+
}
199+
if filepath.Clean(sharedBinary) == bootstrapBinary.path {
200+
return sharedManifest, nil
201+
}
202+
return writeRuntimeSlot(source, bootstrapBinary, bootstrapLock, integrations)
203+
}
204+
157205
// installDetachedRuntime populates a detached repository's external shared-runtime
158206
// slot from the running helper, so the developer-level ambient guard has a stable
159207
// helper to invoke. Unlike installSharedRuntime it scopes the symlink check to the
@@ -369,7 +417,7 @@ func HydrateWorktree(repoPath string) error {
369417
// this, the running binary equals the repo's committed pin by construction. The
370418
// verifyGeneratedRuntime gate refuses to populate a slot for any other version,
371419
// so hydration can never write a mislabeled runtime (the taxweave incident's
372-
// invariant), and installSharedRuntime's own post-write verify+rollback is the
420+
// invariant), and installCommandRuntime's own post-write verify+rollback is the
373421
// backstop. The operation is idempotent and safe under concurrent first use.
374422
func RunHydrateRuntime(repoPath string) error {
375423
repo, err := ResolveRepository(repoPath)
@@ -387,8 +435,8 @@ func RunHydrateRuntime(repoPath string) error {
387435
if err != nil {
388436
return fmt.Errorf("load project configuration for runtime hydration: %w", err)
389437
}
390-
if _, err := installSharedRuntime(source, repo, config.Integrations); err != nil {
391-
return fmt.Errorf("populate the repository-family Boatstack runtime: %w", err)
438+
if _, err := installCommandRuntime(source, repo, config.Integrations); err != nil {
439+
return fmt.Errorf("populate the Boatstack command runtime: %w", err)
392440
}
393441
return HydrateWorktree(repo)
394442
}

boatstack/statemap.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,17 @@ func StateRegistry() []StateEntry {
236236
return filepath.Join(base, "runtime.lock.json"), nil
237237
},
238238
},
239+
{
240+
Name: "runtime-bootstrap-slots", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true,
241+
OwnerVerbs: []string{"init", "update", "hydrate-runtime"},
242+
Sample: func(w WorkspaceContext) (string, error) {
243+
base, err := w.BootstrapRuntimeDir("v0.0.0", "0000000")
244+
if err != nil {
245+
return "", err
246+
}
247+
return filepath.Join(base, "runtime.lock.json"), nil
248+
},
249+
},
239250
{
240251
Name: "mutation-receipts", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true,
241252
OwnerVerbs: []string{"activate-plan", "undo"},

boatstack/statemap_conformance_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ func TestEveryWorkspaceResolverIsDeclared(t *testing.T) {
7272
"RuntimeDir": {resolve(func() (string, error) {
7373
return w.RuntimeDir("v0.0.0", "0000000")
7474
}), ClassRuntimeShared},
75+
"BootstrapRuntimeDir": {resolve(func() (string, error) {
76+
return w.BootstrapRuntimeDir("v0.0.0", "0000000")
77+
}), ClassRuntimeShared},
7578
}
7679

7780
for name, want := range resolverOutputs {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Restore tracked commands in detached repositories
2+
Tracked Boatstack launchers now hydrate their exact pinned runtime correctly when a repository uses detached supervision, so a missing local helper no longer leaves updates stuck in a recovery loop.

0 commit comments

Comments
 (0)