carlos / platform Public

moves: env pin from the capture, claim launch structs back to main (home part 2, 2b r4)

A non-empty Instance in ClaimLaunchDigest made root refuse the launch:
root hashes a LaunchSpec built without it. ClaimLaunchDigest,
ClaimServingLaunch and ClaimLaunchInputs are main's again, byte for
byte (reverting a0d787e4's digest change and the Instance fields); root
and hostbox are untouched.

The launch's move-portable pin is computed at capture instead: the
moves launch source makes the env report from the same LaunchLayers
result as the launch's env, and the probe owners' Launch returns it
(movesReadLaunch passes it through). Select and selectCommitted compare
its digest with the pin and keep it in memory, bound to the exact env,
for Materialize and materializeCommitted. No layer names cross into
the claim; nothing is persisted.

Exclusion rule (platform-1e): a name is left out of the pin only when
it is box-owned and the instance did not supply its running value
(moves.Pinned). LaunchLayers never reports a name the agent decides
(agentOwnedNames).

Witnesses: ClaimLaunchDigest is main's for an empty and an app-var
launch and equals root's LaunchSpec hash; through the production probe
the reserved LaunchInputs equals root's spec hash for an instance with
app variables, an instance HOSTNAME and a box-only HOSTNAME; an
instance HOSTNAME is pinned and a box-only one excluded; the v2 skew
witnesses pass. Census: the overlay re-frozen (twelve units, ten legacy
declarations, new seal), the ClaimLaunchDigest note removed; ruling
owed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c16bdc436041b08241e2c0414fafdada5ffa1a59 parent e615c8d
20 files changed, +300 −225
  • internal/activator/agentowned_moves_test.go +9 −3
  • internal/activator/backend_exec.go +16 −6
  • internal/host/claimlaunchdigest_instance_test.go +0 −45
  • internal/host/claimlaunchdigest_main_test.go +40 −0
  • internal/host/claimmoveaccess.go +3 −8
  • internal/host/claimoperations.go +5 −20
  • internal/host/claimowners.go +1 −6
  • internal/host/claimwakeassembly.go +1 −5
  • internal/host/movesenvpin_test.go +46 −1
  • internal/host/movesparks_test.go +16 −12
  • internal/host/movesprobeowners.go +80 −48
  • internal/host/movesprobeowners_committed_test.go +1 −1
  • internal/host/movesprobeowners_test.go +8 −6
  • internal/host/movesrecoveryowners.go +1 −1
  • internal/host/moveswakesource.go +7 −7
  • internal/host/moveswakesource_linux_test.go +8 −2
  • internal/moves/envreport.go +20 −23
  • internal/moves/envreport_test.go +13 −6
  • internal/provision/plan14_root_reads_test.go +3 −3
  • internal/provision/testdata/plan14-root-reads-home-part2.tsv +22 −22

CI failed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	279.774s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.643s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.206s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.704s
ok  	github.com/carlosframework/platform/internal/activator	249.624s
ok  	github.com/carlosframework/platform/internal/agentsock	0.018s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.499s
ok  	github.com/carlosframework/platform/internal/apperr	0.086s
ok  	github.com/carlosframework/platform/internal/applog	0.144s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.659s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.692s
ok  	github.com/carlosframework/platform/internal/changelog	0.317s
ok  	github.com/carlosframework/platform/internal/clamd	0.055s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	385.221s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.600s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.335s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.651s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.127s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.018s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/host	134.680s
ok  	github.com/carlosframework/platform/internal/hostactors	0.458s
ok  	github.com/carlosframework/platform/internal/hostbox	2.206s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.496s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.018s
ok  	github.com/carlosframework/platform/internal/hostexec	4.776s
ok  	github.com/carlosframework/platform/internal/hostfence	0.027s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.521s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.018s
ok  	github.com/carlosframework/platform/internal/hostguard	1.008s
ok  	github.com/carlosframework/platform/internal/hostops	1.543s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.018s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.019s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.453s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.133s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.018s
ok  	github.com/carlosframework/platform/internal/infraview	0.018s
ok  	github.com/carlosframework/platform/internal/instancectl	4.168s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.724s
ok  	github.com/carlosframework/platform/internal/instancefs	0.023s
ok  	github.com/carlosframework/platform/internal/instancekey	0.019s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.027s
ok  	github.com/carlosframework/platform/internal/layout	1.495s
ok  	github.com/carlosframework/platform/internal/lease	9.805s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.023s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.158s
ok  	github.com/carlosframework/platform/internal/maintenance	0.081s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.506s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.069s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	20.974s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.750s
ok  	github.com/carlosframework/platform/internal/objstore	0.188s
ok  	github.com/carlosframework/platform/internal/ops	0.168s
ok  	github.com/carlosframework/platform/internal/park	0.209s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	15.523s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.006s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.891s
ok  	github.com/carlosframework/platform/internal/relay	7.236s
ok  	github.com/carlosframework/platform/internal/release	20.037s
ok  	github.com/carlosframework/platform/internal/rollout	3.686s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.006s
ok  	github.com/carlosframework/platform/internal/rootstate	0.024s
ok  	github.com/carlosframework/platform/internal/runs	0.568s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.454s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.085s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.913s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
2026/10/15 17:48:55 activator: shop.example hibernated
--- FAIL: TestFDIdentityDeletedAndReplacedDatabaseIsAHold (0.00s)
    fdholders_alias_linux_test.go:178: deleted and replaced: [] stopevidence: incomplete descriptor scan: pid 7160 tid 7160 fd 4: fd entry exists but its file cannot be reached: stat /tmp/TestFDIdentityDeletedAndReplacedDatabaseIsAHold883512247/002/7160/task/7160/fd/4: no such file or directory, want [7160]
FAIL
FAIL	github.com/carlosframework/platform/internal/stopevidence	0.221s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.006s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.006s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.552s
FAIL
make: *** [Makefile:18: test] Error 1

amadan: step gate failed: exit status 2

amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/internal/activator/agentowned_moves_test.go b/internal/activator/agentowned_moves_test.go
index b47e89908..f865edf4a 100644
--- a/internal/activator/agentowned_moves_test.go
+++ b/internal/activator/agentowned_moves_test.go
@@ -80,9 +80,13 @@ func TestAgentOwnedVarsAreMovesBoxOwned(t *testing.T) {
if want := slices.Sorted(maps.Keys(moves.AgentOwnedVars)); !slices.Equal(covered, want) {
t.Fatalf("the test covers %v, moves.AgentOwnedVars is %v", covered, want)
}
+ // LaunchLayers' list of the names the agent decides is the same set.
+ if names := slices.Sorted(slices.Values(agentOwnedNames)); !slices.Equal(names, covered) {
+ t.Fatalf("agentOwnedNames is %v, the agent decides %v", names, covered)
+ }
for _, k := range covered {
- if !moves.BoxOwned(k) || moves.Pinned(k, true) || moves.Class(k) == "equal" {
- t.Errorf("%s is agent-owned but pinned or compared (class %q)", k, moves.Class(k))
+ if !moves.BoxOwned(k) || moves.Pinned(k, false) || moves.Class(k) == "equal" {
+ t.Errorf("%s is agent-owned but pinned or compared when the box supplies it (class %q)", k, moves.Class(k))
}
}
}
@@ -118,7 +122,9 @@ func TestLaunchLayersNamesTheInstanceLayers(t *testing.T) {
if env["HOSTNAME"] != "instance" || env["PWD"] != "/inherited" {
t.Fatalf("HOSTNAME %q PWD %q", env["HOSTNAME"], env["PWD"])
}
- for _, k := range []string{"PWD", "CARLOS_HOST", "CARLOS_CONTROL_SOCKET"} {
+ // CARLOS_REGION is in the instance's .env, but the agent decides it: it
+ // is not the instance's.
+ for _, k := range []string{"PWD", "CARLOS_HOST", "CARLOS_CONTROL_SOCKET", "CARLOS_REGION"} {
if instance[k] {
t.Errorf("%s did not come from the instance's files but is named", k)
}
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index c29416eac..e7b0406c7 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -2283,6 +2283,12 @@ func (b *ExecBackend) spawnChildFrom(ctx context.Context, rt registry.Route, soc
return p, spawnVer, nil
}
+// agentOwnedNames are the variables agentOwnedVars decides: it sets or
+// removes each of them on every launch, whatever the instance's files say.
+// moves.AgentOwnedVars is the same set (TestAgentOwnedVarsAreMovesBoxOwned
+// pins both against agentOwnedVars itself).
+var agentOwnedNames = []string{"CARLOS_CONTROL_SOCKET", "CARLOS_PURGE", "CARLOS_REGION", "CARLOS_HOST"}
+
// agentOwnedVars composes the child's agent-owned environment variables,
// namely the control socket, the purge channel, and the region, on top of
// fileEnv, exactly as startChild did inline before the unit spawn path
@@ -2736,12 +2742,13 @@ func (b *ExecBackend) LaunchEnvironment(rt registry.Route) (map[string]string, [
return env, shadowed, err
}
-// LaunchLayers is LaunchEnvironment with the names the instance's own
-// layers supplied: every name of the rendered configuration file and the
-// instance's own <host>.env, as mergeEnv merged them, before the agent's
-// variables are applied. A name in the composed env that is not in
-// instance came from the agent: its own variables (agentOwnedVars) or its
-// process environment (childEnv). The move's env pin needs the difference
+// LaunchLayers is LaunchEnvironment with the names whose running value the
+// instance's own layers supplied: every name of the rendered configuration
+// file and the instance's own <host>.env, as mergeEnv merged them, except
+// the names agentOwnedVars decides (agentOwnedNames), whose running value
+// is the agent's whatever the files say. A name in the composed env that is
+// not in instance came from the agent: its own variables or its process
+// environment (childEnv). The move's env pin needs the difference
// (moves.ReportLaunch): a box variable such as HOSTNAME is the box's when
// inherited and the instance's when its files set it.
func (b *ExecBackend) LaunchLayers(rt registry.Route) (env map[string]string, instance map[string]bool, shadowed []string, err error) {
@@ -2763,6 +2770,9 @@ func (b *ExecBackend) LaunchLayers(rt registry.Route) (env map[string]string, in
for k := range fileEnv {
instance[k] = true
}
+ for _, k := range agentOwnedNames {
+ delete(instance, k)
+ }
out := map[string]string{}
for _, kv := range childEnv(b.agentOwnedVars(rt, rt.Socket+".control", fileEnv, true)) {
if i := strings.IndexByte(kv, '='); i > 0 {
diff --git a/internal/host/claimlaunchdigest_instance_test.go b/internal/host/claimlaunchdigest_instance_test.go
deleted file mode 100644
index 6e9d93779..000000000
--- a/internal/host/claimlaunchdigest_instance_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package host
-
-import (
- "testing"
-
- "github.com/carlosframework/platform/internal/hostwrite"
-)
-
-// ClaimLaunchDigest and the instance's layer names (home part 2, Task 2b
-// fix round 3). Instance is captured with Env by the moves launch source
-// (ExecBackend.LaunchLayers); the digest covers it only when it is
-// non-empty, so every launch that carries none (serving, relocation, and
-// every reservation already journalled) keeps main's digest byte for byte.
-func TestClaimLaunchDigestCoversInstanceOnlyWhenSet(t *testing.T) {
- base := ClaimLaunchInputs{Identity: hostwrite.Identity{Config: "launch-env:sha256:c", Release: "v7"}, Executable: "/opt/carlos/live/a.example.com", ExecutableSHA256: "aaaa",
- Args: []string{"--socket", "/run/a.sock"}, Env: []string{"A=1", "HOSTNAME=x"}}
- // main's digest of these inputs, computed by main's ClaimLaunchDigest.
- const mainDigest = "47246370546f9bec4ae2541bace116bd102be9655ab1b376de990d5374f26bbe"
- for name, instance := range map[string][]string{"nil": nil, "empty": {}} {
- in := base
- in.Instance = instance
- if d, err := ClaimLaunchDigest(in); err != nil || d != mainDigest {
- t.Fatalf("%s Instance: %s %v, want main's %s", name, d, err, mainDigest)
- }
- }
- with := func(names ...string) string {
- in := base
- in.Instance = names
- d, err := ClaimLaunchDigest(in)
- if err != nil {
- t.Fatal(err)
- }
- return d
- }
- a := with("A", "HOSTNAME")
- if a == mainDigest {
- t.Fatal("a non-empty Instance left the digest unchanged")
- }
- if with("A") == a || with("A", "HOSTNAME", "B") == a {
- t.Fatal("a changed Instance left the digest unchanged")
- }
- if with("HOSTNAME", "A") != a {
- t.Fatal("the digest depends on the order of the names")
- }
-}
diff --git a/internal/host/claimlaunchdigest_main_test.go b/internal/host/claimlaunchdigest_main_test.go
new file mode 100644
index 000000000..3593ca50f
--- /dev/null
+++ b/internal/host/claimlaunchdigest_main_test.go
@@ -0,0 +1,40 @@
+package host
+
+import (
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostwrite"
+)
+
+// rootLaunchSpec is the preimage root hashes for a launch, built exactly as
+// ClaimRoleAssembly.Launch builds it (claimroleassembly_linux.go): the
+// verifier (claimlaunch_authority.go) refuses a launch whose spec's hash is
+// not the attempt's reserved LaunchInputs.
+func rootLaunchSpec(in ClaimLaunchInputs) hostbox.LaunchSpec {
+ return hostbox.LaunchSpec{Identity: in.Identity, Executable: in.Executable, Digest: in.ExecutableSHA256, Args: append([]string(nil), in.Args...), Env: append([]string(nil), in.Env...)}
+}
+
+// TestClaimLaunchDigestIsMainsAndRoots (home part 2, Task 2b fix round 4):
+// ClaimLaunchDigest is main's, byte for byte, for an empty launch and for
+// one whose instance sets app variables, and it is root's LaunchSpec hash
+// of the same launch, so a reserved attempt passes root's launch check.
+func TestClaimLaunchDigestIsMainsAndRoots(t *testing.T) {
+ for name, c := range map[string]struct {
+ in ClaimLaunchInputs
+ main string
+ }{
+ "empty": {ClaimLaunchInputs{}, "0ed115c96598a2877d539d837d785f574bd5c4c8630fe1803ca216a0cb76396b"},
+ "app variables": {ClaimLaunchInputs{Identity: hostwrite.Identity{Config: "launch-env:sha256:c", Release: "v7"}, Executable: "/opt/carlos/live/a.example.com", ExecutableSHA256: "aaaa",
+ Args: []string{"--socket", "/run/a.sock"}, Env: []string{"A=1", "HOSTNAME=x"}}, "47246370546f9bec4ae2541bace116bd102be9655ab1b376de990d5374f26bbe"},
+ } {
+ d, err := ClaimLaunchDigest(c.in)
+ if err != nil || d != c.main {
+ t.Fatalf("%s: %s %v, want main's %s", name, d, err, c.main)
+ }
+ root, err := rootLaunchSpec(c.in).Hash()
+ if err != nil || root != d {
+ t.Fatalf("%s: root's spec hash %s %v, the reserved digest %s", name, root, err, d)
+ }
+ }
+}
diff --git a/internal/host/claimmoveaccess.go b/internal/host/claimmoveaccess.go
index 2e18929e2..f78a04d3c 100644
--- a/internal/host/claimmoveaccess.go
+++ b/internal/host/claimmoveaccess.go
@@ -189,14 +189,9 @@ func claimMoveLaunchSource(backend *activator.ExecBackend) func(context.Context,
}
digest := sha256.Sum256(raw)
snapshot.Identity.Config = hex.EncodeToString(digest[:])
- instance := make([]string, 0, len(supplied))
- for k := range supplied {
- if _, ok := env[k]; ok {
- instance = append(instance, k)
- }
- }
- sort.Strings(instance)
- return ClaimServingLaunch{Identity: snapshot.Identity, Executable: snapshot.Executable, ExecutableSHA256: snapshot.SHA256, Args: snapshot.Args, Env: entries, Instance: instance, Version: snapshot.Version, Staged: snapshot.Staged, Limit: snapshot.Limit}, moves.ReportLaunch(key, env, supplied), nil
+ // The report is made from this same composition, so its pin and the
+ // launch's env can never come from different reads (fix round 4).
+ return ClaimServingLaunch{Identity: snapshot.Identity, Executable: snapshot.Executable, ExecutableSHA256: snapshot.SHA256, Args: snapshot.Args, Env: entries, Version: snapshot.Version, Staged: snapshot.Staged, Limit: snapshot.Limit}, moves.ReportLaunch(key, env, supplied), nil
}
}
diff --git a/internal/host/claimoperations.go b/internal/host/claimoperations.go
index c577e4cb6..de67e118a 100644
--- a/internal/host/claimoperations.go
+++ b/internal/host/claimoperations.go
@@ -6,7 +6,6 @@ import (
"encoding/hex"
"encoding/json"
"os/exec"
- "slices"
"time"
"github.com/carlosframework/platform/internal/activator"
@@ -68,26 +67,12 @@ type ClaimProbeReadiness func(ctx context.Context, cmd *exec.Cmd, rt registry.Ro
// ClaimLaunchDigest names the launch inputs an attempt reservation binds: the
// identity, executable, digest, arguments and environment, never the
// admission. Serving retries and operation attempts use the same digest.
-// The instance's layer names (Instance, captured with Env by the moves
-// launch source) are covered only when there are any, sorted: a launch
-// that carries none keeps the digest's original form byte for byte.
func ClaimLaunchDigest(in ClaimLaunchInputs) (string, error) {
- var raw []byte
- var err error
- if len(in.Instance) == 0 {
- raw, err = json.Marshal(struct {
- Identity hostwrite.Identity
- Executable, Digest string
- Args, Env []string
- }{in.Identity, in.Executable, in.ExecutableSHA256, in.Args, in.Env})
- } else {
- raw, err = json.Marshal(struct {
- Identity hostwrite.Identity
- Executable, Digest string
- Args, Env []string
- Instance []string
- }{in.Identity, in.Executable, in.ExecutableSHA256, in.Args, in.Env, slices.Sorted(slices.Values(in.Instance))})
- }
+ raw, err := json.Marshal(struct {
+ Identity hostwrite.Identity
+ Executable, Digest string
+ Args, Env []string
+ }{in.Identity, in.Executable, in.ExecutableSHA256, in.Args, in.Env})
if err != nil {
return "", err
}
diff --git a/internal/host/claimowners.go b/internal/host/claimowners.go
index 50ef43514..75bf335ae 100644
--- a/internal/host/claimowners.go
+++ b/internal/host/claimowners.go
@@ -107,12 +107,7 @@ type ClaimServingLaunch struct {
Executable string
ExecutableSHA256 string
Args, Env []string
- // Instance is the names in Env the instance's own layers supplied
- // (ExecBackend.LaunchLayers), sorted; the moves launch source sets it.
- // The move's env pin covers a box variable only when the instance
- // supplied it (moves.Pinned).
- Instance []string
- Version string
+ Version string
// Staged is the file ExecutableSHA256 hashes and the version it is
// staged under, from one resolution of the live link
// (activator.ClaimLaunchSnapshot.Staged).
diff --git a/internal/host/claimwakeassembly.go b/internal/host/claimwakeassembly.go
index 644be9bf6..b5eb48b44 100644
--- a/internal/host/claimwakeassembly.go
+++ b/internal/host/claimwakeassembly.go
@@ -34,11 +34,7 @@ type ClaimLaunchInputs struct {
Executable string
ExecutableSHA256 string
Args, Env []string
- // Instance is the launch's ClaimServingLaunch.Instance, carried for the
- // moves owners' env pin check at Materialize. It is not a launch input:
- // ClaimLaunchDigest does not cover it.
- Instance []string
- DBPath, Version string
+ DBPath, Version string
// Staged is the file ExecutableSHA256 hashes and the version it is
// staged under (ClaimServingLaunch.Staged); Prepare hands it to the
// start, which records it before the exec (settings spec §4.2).
diff --git a/internal/host/movesenvpin_test.go b/internal/host/movesenvpin_test.go
index dd0d45fb1..4e8d26798 100644
--- a/internal/host/movesenvpin_test.go
+++ b/internal/host/movesenvpin_test.go
@@ -4,6 +4,7 @@ import (
"errors"
"os"
"path/filepath"
+ "slices"
"strings"
"testing"
@@ -42,6 +43,16 @@ func TestProbeEnvPinIsMovePortable(t *testing.T) {
t.Fatalf("the probe's admissions: %+v", p.admissions)
}
h.committedAndOpen(t)
+ // The probe starts through root's launch check: the reserved
+ // LaunchInputs is the hash of the launch root is asked to start,
+ // an instance whose files set app variables included.
+ if !slices.Contains(p.selected.Env, "APP_MODE=prod") {
+ t.Fatalf("the launch carries no instance app variable: %v", p.selected.Env)
+ }
+ root, err := rootLaunchSpec(p.selected).Hash()
+ if err != nil || p.attempt.LaunchInputs == "" || root != p.attempt.LaunchInputs {
+ t.Fatalf("root's spec hash %s %v, the reserved LaunchInputs %s", root, err, p.attempt.LaunchInputs)
+ }
})
}
}
@@ -120,6 +131,28 @@ func TestProbeEnvPinCoversAnInstanceBoxVariable(t *testing.T) {
t.Fatalf("the probe's admissions: %+v (move %+v)", p.admissions, h.read(t, p1Host).Move)
}
h.committedAndOpen(t)
+ root, err := rootLaunchSpec(p.selected).Hash()
+ if err != nil || root != p.attempt.LaunchInputs || !slices.Contains(p.selected.Env, "HOSTNAME=a") {
+ t.Fatalf("root's spec hash %s %v, the reserved LaunchInputs %s", root, err, p.attempt.LaunchInputs)
+ }
+ })
+ t.Run("a box-only HOSTNAME differs and is left out", func(t *testing.T) {
+ t.Setenv("HOSTNAME", "source-box")
+ h := newCloseHarness(t, opts...)
+ if inst := h.sourceReport(t).Env.Instance; len(inst) != 0 {
+ t.Fatalf("an inherited HOSTNAME is named the instance's: %v", inst)
+ }
+ p := h.productionProbe(t)
+ t.Setenv("HOSTNAME", "target-box")
+ h.land(t)
+ if len(p.admissions) != 1 {
+ t.Fatalf("a box-only HOSTNAME refused the probe: %+v (move %+v)", p.admissions, h.read(t, p1Host).Move)
+ }
+ h.committedAndOpen(t)
+ root, err := rootLaunchSpec(p.selected).Hash()
+ if err != nil || root != p.attempt.LaunchInputs || !slices.Contains(p.selected.Env, "HOSTNAME=target-box") {
+ t.Fatalf("root's spec hash %s %v, the reserved LaunchInputs %s, env %v", root, err, p.attempt.LaunchInputs, p.selected.Env)
+ }
})
t.Run("the target's env file changes HOSTNAME", func(t *testing.T) {
h := newCloseHarness(t, withHostname...)
@@ -202,7 +235,7 @@ func TestRecoveryAndCutInputsDigestTheirOwnEnv(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if cur.EnvReport != envDigest(key, list, names) {
+ if cur.EnvReport != envDigest(key, list, names...) {
t.Fatal("currentInputs is not the probe's move-portable digest")
}
if rin.EnvReport != localEnvDigest(key, list) {
@@ -215,3 +248,15 @@ func TestRecoveryAndCutInputsDigestTheirOwnEnv(t *testing.T) {
t.Fatalf("the recovery's other inputs differ: %+v %+v", rin, cur)
}
}
+
+// envDigest is a test's move-portable pin of a launch's K=V list
+// (CARLOS_MOVE_PROBE dropped), instance naming what the instance's own
+// layers supplied: what the production launch source's report carries
+// (claimMoveLaunchSource, moves.ReportLaunch).
+func envDigest(key []byte, env []string, instance ...string) string {
+ supplied := map[string]bool{}
+ for _, k := range instance {
+ supplied[k] = true
+ }
+ return moves.PortableDigest(key, launchVars(env), supplied)
+}
diff --git a/internal/host/movesparks_test.go b/internal/host/movesparks_test.go
index 504a59ed7..eb68782c5 100644
--- a/internal/host/movesparks_test.go
+++ b/internal/host/movesparks_test.go
@@ -544,6 +544,12 @@ type prodProbe struct {
rejectEvidence error
// writerEvidence is the writer's AdmissionEvidence as bound.
writerEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error
+ // selected is the probe's launch inputs as the owners select them for
+ // the attempt, and attempt the reservation the runtime journalled for
+ // it: root's launch check compares the reservation's LaunchInputs with
+ // the hash of the launch it is asked to start.
+ selected ClaimLaunchInputs
+ attempt hostclaim.WakeRetryAttempt
// admissions are the admissions the runtime made, one per attempt.
admissions []hostclaim.AdmissionRecord
last activator.ProbeResult
@@ -588,12 +594,13 @@ func (h *landingHarness) productionProbe(t *testing.T) *prodProbe {
// The held row's launch: what this box would launch, composed by
// the production composer over the target's data directory with
// the target's own region, as claimMoveLaunchSource composes it
- // (ExecBackend.LaunchLayers, CARLOS_MOVE_PROBE dropped, the names
- // the instance's files supplied), and the live release.
- Launch: func(_ context.Context, rt registry.Route) (ClaimServingLaunch, error) {
+ // (ExecBackend.LaunchLayers, CARLOS_MOVE_PROBE dropped), the live
+ // release, and the env report made from that same composition, whose
+ // digest is the launch's move-portable pin.
+ Launch: func(_ context.Context, rt registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
composed, supplied, _, err := (&activator.ExecBackend{DataDir: h.tdir, Region: h.dst}).LaunchLayers(rt)
if err != nil {
- return ClaimServingLaunch{}, err
+ return ClaimServingLaunch{}, moves.EnvReport{}, err
}
delete(composed, "CARLOS_MOVE_PROBE")
env := make([]string, 0, len(composed))
@@ -601,14 +608,7 @@ func (h *landingHarness) productionProbe(t *testing.T) *prodProbe {
env = append(env, k+"="+v)
}
sort.Strings(env)
- var instance []string
- for k := range supplied {
- if _, ok := composed[k]; ok {
- instance = append(instance, k)
- }
- }
- sort.Strings(instance)
- return ClaimServingLaunch{Executable: "/opt/carlos/live/" + p1Host, ExecutableSHA256: strings.Repeat("a", 64), Env: env, Instance: instance, Version: "v7"}, nil
+ return ClaimServingLaunch{Executable: "/opt/carlos/live/" + p1Host, ExecutableSHA256: strings.Repeat("a", 64), Env: env, Version: "v7"}, moves.ReportLaunch(key, composed, supplied), nil
},
EnvKey: key,
Admitted: MovesRecordAdmittedAttempts,
@@ -683,6 +683,9 @@ func (p *prodProbe) wake(ctx context.Context, cfg activator.ProbeConfig) (activa
p.ctl.wake.pending[p1Host] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done}
p.ctl.wake.mu.Unlock()
rt := h.route
+ if p.selected, err = p.owners.Select(ctx, rt, o, policy); err != nil {
+ return activator.ProbeResult{}, err
+ }
admitted, err := p.ctl.runtime.Admit(ctx, rt, o, policy)
if err != nil {
return activator.ProbeResult{}, err
@@ -701,6 +704,7 @@ func (p *prodProbe) wake(ctx context.Context, cfg activator.ProbeConfig) (activa
if err := s.Reserve(ctx, 0); err != nil {
return activator.ProbeResult{}, err
}
+ p.attempt = s.CurrentAttempt()
if err := s.Materialize(ctx); err != nil {
return activator.ProbeResult{}, err
}
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go
index 1fcd9d717..1fec01d19 100644
--- a/internal/host/movesprobeowners.go
+++ b/internal/host/movesprobeowners.go
@@ -112,20 +112,23 @@ type movesProbeOwners struct {
Markers ClaimMarkerStore
Route func(host string) (*registry.Route, error)
// Launch is the held row's launch: the live executable, its digest,
- // arguments, environment and version (the claim's launch snapshot).
- Launch func(context.Context, registry.Route) (ClaimServingLaunch, error)
- // EnvKey is the env report's HMAC key (moves.ReportKeyFromSecrets). The
- // pin is checked against moves.ReportEnv(EnvKey, env) of the selected
- // launch's own environment, so no report can be decoupled from what
- // runs (fix round 1, finding 3). Normalisation contract: env is the
- // launch's K=V list, last value wins (childEnv's dedupe), with the
- // probe-only variables (probeOnlyVars: CARLOS_MOVE_PROBE) removed,
- // since the source never ran with them and the probe adds
- // CARLOS_MOVE_PROBE=1 after the pin, and the digest is the
- // move-portable one (moves.PortableDigest): the box-owned variables,
- // which each box composes for itself, are left out on both sides. The
- // production Launch is this box's own composition
- // (ExecBackend.LaunchEnvironment; claim Plan 10's launch accessor).
+ // arguments, environment and version (the claim's launch snapshot), and
+ // the moves env report of that same environment, computed at capture
+ // from the one composition that produced the env (claimMoveLaunchSource:
+ // ExecBackend.LaunchLayers, moves.ReportLaunch). The report's Digest is
+ // the launch's move-portable pin. It is the moves owners' own value:
+ // never persisted, and never digested by the claim (fix round 4).
+ Launch func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error)
+ // EnvKey is the env report's HMAC key (moves.ReportKeyFromSecrets), the
+ // key Launch's report is made with. The pin is checked against the
+ // report of the selected launch's own environment, so no report can be
+ // decoupled from what runs (fix round 1, finding 3). Normalisation
+ // contract: the report covers the launch's K=V list, last value wins
+ // (childEnv's dedupe), with the probe-only variables (probeOnlyVars:
+ // CARLOS_MOVE_PROBE) removed, since the source never ran with them and
+ // the probe adds CARLOS_MOVE_PROBE=1 after the pin, and its digest is the
+ // move-portable one (moves.PortableDigest). The production Launch is this
+ // box's own composition (claim Plan 10's launch accessor).
EnvKey []byte
// Admitted is the number of probe attempts T11 admitted (H10).
Admitted func(context.Context, *provision.Record) (int, error)
@@ -154,6 +157,11 @@ type movesProbeOwners struct {
mu sync.Mutex
published map[string]ClaimPublicationReceipt
+ // envPins are the move-portable pins Select captured, by host, each
+ // bound to the exact launch environment it was computed for
+ // (captureEnvPin): what Materialize checks the same inputs against.
+ // Memory only, never persisted, never digested by the claim.
+ envPins map[string]capturedEnvPin
// fences are the admitted spawn selections Materialize ran under, by
// host and session: the live fence for the session's marker writes.
fences map[string]hostwrite.Work
@@ -332,24 +340,40 @@ func (m *movesProbeOwners) admissible(ctx context.Context, v probeLanding) error
// readiness pin never saw.
var probeOnlyVars = map[string]bool{"CARLOS_MOVE_PROBE": true}
-// envDigest is the env report digest of a launch environment, normalised
-// as EnvKey's comment states: the probe-only variables dropped, and the
-// digest the move-portable one (moves.PortableDigest, every variable that
-// is not moves.BoxOwned), as the source's pin is. The target's own
-// composition sets its own box-owned variables (its region, or none on a
-// home box; its socket; its purge channel), so a digest over them could
-// never match a pin made on another box (home part 2, Task 2b).
-//
-// instance is the names the instance's own layers supplied
-// (ClaimServingLaunch.Instance): a box variable among them is pinned like an
-// app variable, and one the box supplied is left out (moves.Pinned, fix
-// round 2).
-func envDigest(key []byte, env, instance []string) string {
- supplied := make(map[string]bool, len(instance))
- for _, k := range instance {
- supplied[k] = true
+// capturedEnvPin is a launch environment's move-portable pin as Select
+// computed it at capture (Launch's report), bound to that environment by
+// its fingerprint.
+type capturedEnvPin struct{ env, pin string }
+
+// envFingerprint identifies a launch environment exactly: every entry, in
+// order.
+func envFingerprint(env []string) string {
+ sum := sha256.Sum256([]byte(strings.Join(env, "\x00")))
+ return hex.EncodeToString(sum[:])
+}
+
+// captureEnvPin records the pin Select computed for the inputs it returns
+// (env is ClaimLaunchInputs.Env), replacing any earlier one for host.
+func (m *movesProbeOwners) captureEnvPin(host string, env []string, pin string) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if m.envPins == nil {
+ m.envPins = map[string]capturedEnvPin{}
+ }
+ m.envPins[host] = capturedEnvPin{env: envFingerprint(env), pin: pin}
+}
+
+// capturedPin is the pin Select captured for exactly env on host; false
+// when Select captured none for it in this process (Materialize then
+// refuses, fail closed).
+func (m *movesProbeOwners) capturedPin(host string, env []string) (string, bool) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ c, ok := m.envPins[host]
+ if !ok || c.env != envFingerprint(env) {
+ return "", false
}
- return moves.PortableDigest(key, launchVars(env), supplied)
+ return c.pin, true
}
// localEnvDigest is the digest of the whole launch environment, every
@@ -630,7 +654,7 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host
}
// The launch-input pin at spawn: the authoritative check, after
// readiness's (Plan 2's configured), since the inputs can change between.
- l, err := m.Launch(ctx, rt)
+ l, report, err := m.Launch(ctx, rt)
if err != nil {
return ClaimLaunchInputs{}, err
}
@@ -641,13 +665,16 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host
return ClaimLaunchInputs{}, probeInputsChanged()
}
// A v1 pin is refused whatever it hashes: it cannot tell an instance's
- // box variable from the box's (moves.EnvReportVersion).
- if !moves.CurrentEnvPin(pins.EnvReport) || envDigest(m.EnvKey, l.Env, l.Instance) != pins.EnvReport {
+ // box variable from the box's (moves.EnvReportVersion). The launch's own
+ // pin is its report's, computed at capture with its env.
+ if !moves.CurrentEnvPin(pins.EnvReport) || report.Digest != pins.EnvReport {
return ClaimLaunchInputs{}, probeInputsChanged()
}
- return ClaimLaunchInputs{Roles: []string{"main"}, Identity: launchIdentity(pins), Baseline: plan, Admission: admission,
- Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: probeEnv(l.Env), Instance: append([]string(nil), l.Instance...),
- DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, Prepared: prepared, ReadyPath: ready}, nil
+ in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: launchIdentity(pins), Baseline: plan, Admission: admission,
+ Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: probeEnv(l.Env),
+ DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, Prepared: prepared, ReadyPath: ready}
+ m.captureEnvPin(rt.Host, in.Env, report.Digest)
+ return in, nil
}
// AdmissionEvidence re-derives Select's admission against the observation
@@ -824,7 +851,9 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed
if err := m.Release(ctx, rt, pins.Release, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: pins.Release}}); err != nil {
return err
}
- if !moves.CurrentEnvPin(pins.EnvReport) || envDigest(m.EnvKey, in.Env, in.Instance) != pins.EnvReport {
+ // The launch Materialize prepares is the one Select pinned: its env's
+ // pin as captured (captureEnvPin), never recomputed from another read.
+ if pin, ok := m.capturedPin(o.Host, in.Env); !ok || !moves.CurrentEnvPin(pins.EnvReport) || pin != pins.EnvReport {
return probeInputsChanged()
}
m.recordFence(o.Host, h.Session, work)
@@ -1441,7 +1470,7 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout
if err != nil {
return ClaimLaunchInputs{}, err
}
- l, err := m.Launch(ctx, rt)
+ l, report, err := m.Launch(ctx, rt)
if err != nil {
return ClaimLaunchInputs{}, err
}
@@ -1451,7 +1480,7 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout
// The committed wake runs the move's pinned inputs (InputPolicy), judged
// on the launch itself: its environment's report and its release.
pins := v.rec.Move.Inputs
- id := committedLaunchIdentity(m.EnvKey, l.Env, l.Instance, l.Version)
+ id := committedLaunchIdentity(report.Digest, l.Version)
if !moves.CurrentEnvPin(pins.EnvReport) || id != pinnedLaunchIdentity(pins) {
return ClaimLaunchInputs{}, fmt.Errorf("%w: the launch is not the move's pinned inputs", hostbox.ErrChanged)
}
@@ -1459,9 +1488,11 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout
if err != nil {
return ClaimLaunchInputs{}, err
}
- return ClaimLaunchInputs{Roles: []string{"main"}, Identity: id, Baseline: plan, Admission: admission,
- Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: withoutProbeVars(l.Env), Instance: append([]string(nil), l.Instance...),
- DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit}, nil
+ in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: id, Baseline: plan, Admission: admission,
+ Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: withoutProbeVars(l.Env),
+ DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit}
+ m.captureEnvPin(rt.Host, in.Env, report.Digest)
+ return in, nil
}
func (m *movesProbeOwners) committedEvidence(ctx context.Context, o hostclaim.Observed, v probeLanding, in hostclaim.Admission, baseline *genselect.Plan) error {
@@ -1489,7 +1520,8 @@ func (m *movesProbeOwners) committedEvidence(ctx context.Context, o hostclaim.Ob
func (m *movesProbeOwners) materializeCommitted(ctx context.Context, o hostclaim.Observed, v probeLanding, in ClaimLaunchInputs, work hostwrite.Work) error {
// The identity is the launch that will run (in.Env, in.Version), and it
// is the pins'.
- if in.Identity != committedLaunchIdentity(m.EnvKey, in.Env, in.Instance, in.Version) || in.Identity != pinnedLaunchIdentity(v.rec.Move.Inputs) {
+ pin, ok := m.capturedPin(o.Host, in.Env)
+ if !ok || in.Identity != committedLaunchIdentity(pin, in.Version) || in.Identity != pinnedLaunchIdentity(v.rec.Move.Inputs) {
return hostbox.ErrChanged
}
if err := writerFence(work, in.Identity)(ctx); err != nil {
@@ -1554,11 +1586,11 @@ func (m *movesProbeOwners) InputPolicy(ctx context.Context, c *hostclaim.Claim,
const committedEnvPrefix = "launch-env:"
// committedLaunchIdentity is the committed wake's identity, computed from
-// the launch that will run (fix round 2, item 2): its environment's report
-// digest under the probe's normalisation contract (envDigest: CARLOS_MOVE_PROBE
+// the launch that will run (fix round 2, item 2): its environment's
+// move-portable pin as captured with it (Launch's report, CARLOS_MOVE_PROBE
// excluded) and its release. The input policy compares it with the pins.
-func committedLaunchIdentity(key []byte, env, instance []string, version string) hostwrite.Identity {
- return hostwrite.Identity{Config: committedEnvPrefix + envDigest(key, env, instance), Release: version}
+func committedLaunchIdentity(pin, version string) hostwrite.Identity {
+ return hostwrite.Identity{Config: committedEnvPrefix + pin, Release: version}
}
// pinnedLaunchIdentity is the identity a launch matching the move's pins
diff --git a/internal/host/movesprobeowners_committed_test.go b/internal/host/movesprobeowners_committed_test.go
index 99190bab4..98b6ac0c2 100644
--- a/internal/host/movesprobeowners_committed_test.go
+++ b/internal/host/movesprobeowners_committed_test.go
@@ -393,7 +393,7 @@ func TestMovesProbeOwnersCommittedWakeJudgesTheRealLaunch(t *testing.T) {
if _, err := f.owners.Select(ctx, f.route, o, poWake); err == nil {
t.Fatal("a changed launch environment was selected")
}
- changed := committedLaunchIdentity(poEnvKey, f.launch.Env, f.launch.Instance, f.launch.Version)
+ changed := committedLaunchIdentity(envDigest(poEnvKey, f.launch.Env), f.launch.Version)
if err := f.owners.InputPolicy(ctx, c, sel, changed); err == nil {
t.Fatal("the changed launch's identity was accepted")
}
diff --git a/internal/host/movesprobeowners_test.go b/internal/host/movesprobeowners_test.go
index 4b8473915..5fbe31165 100644
--- a/internal/host/movesprobeowners_test.go
+++ b/internal/host/movesprobeowners_test.go
@@ -187,7 +187,7 @@ func newPOFixture(t *testing.T, opts ...poOption) *poFixture {
Move: &provision.MoveState{ID: poOp, Incarnation: inc, From: f.srcRegion, To: f.dstCode, State: provision.MoveLanding, LandingPhase: provision.LandProbing, Inputs: pins, CutDeadline: &deadline,
Landing: &provision.MoveLandingJournal{Attempts: 1, InstalledDBSHA256: "sha256:" + f.manifest.DBSHA256}}}
f.launch = ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:snapshot", Release: "sha256:bytes"}, Executable: "/opt/carlos/live/" + poHost, ExecutableSHA256: strings.Repeat("b", 64), Args: []string{"--socket", f.route.Socket, "--db", f.route.DBPath}, Env: []string{"A=1", "CARLOS_MOVE_PROBE=0"}, Version: pins.Release}
- pins.EnvReport = envDigest(poEnvKey, f.launch.Env, f.launch.Instance)
+ pins.EnvReport = envDigest(poEnvKey, f.launch.Env)
f.admitted = 3
f.renderETags, f.renderOverlay = pins.BundleETags, pins.OverlayETag
f.owners = &movesProbeOwners{Claims: f.claims, Data: f.bucket, Bucket: "data", Region: f.dstEngine, Box: f.dstBox, DataDir: f.dataDir,
@@ -196,9 +196,11 @@ func newPOFixture(t *testing.T, opts ...poOption) *poFixture {
FilesDir: func(string) (string, string) { return f.files, filepath.Join(f.dataDir, poHost+".env") },
Markers: plan9D5Markers{f.bucket},
Route: func(string) (*registry.Route, error) { rt := f.route; return &rt, nil },
- Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) {
+ // The launch and its env report, made from the same env as the
+ // production launch source makes it (claimMoveLaunchSource).
+ Launch: func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
f.launches++
- return f.launch, nil
+ return f.launch, moves.ReportLaunch(poEnvKey, launchVars(f.launch.Env), nil), nil
},
EnvKey: poEnvKey,
Admitted: func(context.Context, *provision.Record) (int, error) { return f.admitted, nil },
@@ -888,7 +890,7 @@ func TestMovesProbeOwnersThroughTheClaimRuntime(t *testing.T) {
t.Fatal(err)
}
pins := mpPins()
- pins.EnvReport = envDigest(poEnvKey, []string{"A=1"}, nil)
+ pins.EnvReport = envDigest(poEnvKey, []string{"A=1"})
deadline := f.now.Add(time.Hour)
rec := &provision.Record{Host: recoveryHost, Account: "bab", App: "jam", Region: "ie",
Move: &provision.MoveState{ID: poOp, Incarnation: h.Incarnation, From: "uk", To: "ie", State: provision.MoveLanding, LandingPhase: provision.LandProbing, Inputs: pins, CutDeadline: &deadline, CutAttempts: 3,
@@ -900,8 +902,8 @@ func TestMovesProbeOwnersThroughTheClaimRuntime(t *testing.T) {
ReadyPath: func(context.Context, string, string) (string, error) { return "/ready", nil },
FilesDir: func(string) (string, string) { return filesDir, "" },
Route: func(string) (*registry.Route, error) { rt := route; return &rt, nil },
- Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) {
- return ClaimServingLaunch{Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Env: []string{"A=1"}, Version: pins.Release}, nil
+ Launch: func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
+ return ClaimServingLaunch{Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Env: []string{"A=1"}, Version: pins.Release}, moves.ReportLaunch(poEnvKey, map[string]string{"A": "1"}, nil), nil
},
EnvKey: poEnvKey,
Admitted: MovesRecordAdmittedAttempts,
diff --git a/internal/host/movesrecoveryowners.go b/internal/host/movesrecoveryowners.go
index 9ab731b8a..4938692fa 100644
--- a/internal/host/movesrecoveryowners.go
+++ b/internal/host/movesrecoveryowners.go
@@ -374,7 +374,7 @@ func (r movesRecoveryOwners) Select(ctx context.Context, rt registry.Route, o ho
if err != nil {
return ClaimLaunchInputs{}, err
}
- l, err := m.Launch(ctx, rt)
+ l, _, err := m.Launch(ctx, rt)
if err != nil {
return ClaimLaunchInputs{}, err
}
diff --git a/internal/host/moveswakesource.go b/internal/host/moveswakesource.go
index 6f4b2d00d..35f30ba69 100644
--- a/internal/host/moveswakesource.go
+++ b/internal/host/moveswakesource.go
@@ -199,13 +199,13 @@ func movesReadRoute(read ClaimMoveReadAccess) func(host string) (*registry.Route
}
}
-// movesReadLaunch is the claim's move read access as the owners' launch.
-// The owners judge the launch's environment themselves (envDigest), so the
-// access's report is not used.
-func movesReadLaunch(read ClaimMoveReadAccess) func(context.Context, registry.Route) (ClaimServingLaunch, error) {
- return func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) {
- l, _, err := read.Launch(ctx, rt.Host)
- return l, err
+// movesReadLaunch is the claim's move read access as the owners' launch,
+// with the access's env report: made from the same composition as the
+// launch's env (claimMoveLaunchSource), its digest is the launch's
+// move-portable pin, which the owners check (fix round 4).
+func movesReadLaunch(read ClaimMoveReadAccess) func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
+ return func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
+ return read.Launch(ctx, rt.Host)
}
}
diff --git a/internal/host/moveswakesource_linux_test.go b/internal/host/moveswakesource_linux_test.go
index d9e9cb2ca..f1825016c 100644
--- a/internal/host/moveswakesource_linux_test.go
+++ b/internal/host/moveswakesource_linux_test.go
@@ -65,7 +65,13 @@ func (a mwAccess) Launch(_ context.Context, host string) (ClaimServingLaunch, mo
if host != a.f.route.Host {
return ClaimServingLaunch{}, moves.EnvReport{}, hostbox.ErrChanged
}
- return a.f.launch, moves.EnvReport{}, nil
+ // The env report of the same launch env, as claimMoveLaunchSource makes
+ // it: its digest is the launch's move-portable pin.
+ key, err := moves.ReportKeyFromSecrets(mwSecretsKey)
+ if err != nil {
+ return ClaimServingLaunch{}, moves.EnvReport{}, err
+ }
+ return a.f.launch, moves.ReportLaunch(key, launchVars(a.f.launch.Env), nil), nil
}
// mwWorld is one box's registered moves wake source over a poFixture's
@@ -163,7 +169,7 @@ func mwRegister(t *testing.T, f *poFixture, box, region string, files ClaimFiles
}
if mv := f.rec.Move; mv != nil {
if mv.Inputs != nil {
- mv.Inputs.EnvReport = envDigest(key, f.launch.Env, f.launch.Instance)
+ mv.Inputs.EnvReport = envDigest(key, f.launch.Env)
}
if mv.RecoveryInputs != nil {
mv.RecoveryInputs.EnvReport = localEnvDigest(key, f.launch.Env)
diff --git a/internal/moves/envreport.go b/internal/moves/envreport.go
index dc95fd89a..09b650373 100644
--- a/internal/moves/envreport.go
+++ b/internal/moves/envreport.go
@@ -57,20 +57,16 @@ func BoxOwned(name string) bool { return AgentOwnedVars[name] || BoxVars[name] }
// Pinned reports whether a variable is part of the move-portable env, the
// env the move's pin covers on the source and the target's checks hash
-// (fix round 2): every variable but the agent-owned ones, which each box
-// sets for itself, and the box variables the box supplied itself. A box
-// variable the instance's own layers supply (its rendered configuration or
-// its env file: instanceSupplied) is the instance's, pinned like any app
-// variable, since the agent never overwrites it and the instance's value
-// is what runs.
+// (fix round 4, platform-1e's exclusion rule): a variable is left out only
+// when it is box-owned (BoxOwned) and the instance's own layers did not
+// supply the value that runs (instanceSupplied false). Everything else is
+// pinned. The composer reports as instance-supplied only the names whose
+// running value is the instance's (activator ExecBackend.LaunchLayers): an
+// agent-owned variable never is, since the agent sets or removes it
+// whatever the files say, and a box variable is when the instance's files
+// set it, since the agent never overwrites one.
func Pinned(name string, instanceSupplied bool) bool {
- if AgentOwnedVars[name] {
- return false
- }
- if BoxVars[name] {
- return instanceSupplied
- }
- return true
+ return !BoxOwned(name) || instanceSupplied
}
// ReportKeyFromSecrets derives the report's HMAC key from
@@ -97,14 +93,15 @@ type EnvReport struct {
V int `json:"v"`
Vars map[string]string `json:"vars"`
Digest string `json:"digest"`
- // Instance names the box variables (BoxVars) the instance's own layers
- // supplied, sorted: the box variables the report pins and T3 compares.
- // Empty means every box variable in Vars is the box's.
+ // Instance names the box-owned variables (BoxOwned) whose running value
+ // the instance's own layers supplied, sorted: the box-owned variables
+ // the report pins and T3 compares. Empty means every box-owned variable
+ // in Vars is the box's.
Instance []string `json:"instance,omitempty"`
}
// instanceSupplied reports whether the instance's own layers supplied the
-// box variable name in this report.
+// box-owned variable name in this report.
func (r EnvReport) instanceSupplied(name string) bool {
for _, n := range r.Instance {
if n == name {
@@ -126,7 +123,7 @@ func ReportLaunch(key []byte, env map[string]string, instance map[string]bool) E
r := EnvReport{V: EnvReportVersion, Vars: make(map[string]string, len(env))}
for k, v := range env {
r.Vars[k] = macVar(key, k, v)
- if BoxVars[k] && instance[k] {
+ if BoxOwned(k) && instance[k] {
r.Instance = append(r.Instance, k)
}
}
@@ -235,12 +232,12 @@ type EnvDiff struct {
Name, Kind string
}
-// classIn is Class in a comparison of two reports: a box variable either
-// side's instance supplied is the instance's, compared as "equal", as the
-// pin covers it (Pinned).
+// classIn is Class in a comparison of two reports: a box-owned variable
+// either side's instance supplied is the instance's, compared as "equal",
+// as the pin covers it (Pinned).
func classIn(name string, source, target EnvReport) string {
- if c := Class(name); c != "box" || AgentOwnedVars[name] || !(source.instanceSupplied(name) || target.instanceSupplied(name)) {
- return c
+ if !BoxOwned(name) || !(source.instanceSupplied(name) || target.instanceSupplied(name)) {
+ return Class(name)
}
return "equal"
}
diff --git a/internal/moves/envreport_test.go b/internal/moves/envreport_test.go
index 307457202..d50acfcc2 100644
--- a/internal/moves/envreport_test.go
+++ b/internal/moves/envreport_test.go
@@ -109,9 +109,15 @@ func TestPortableDigestCoversTheInstanceOnly(t *testing.T) {
return PortableDigest(key, env, instance)
}
for name := range AgentOwnedVars {
- // agent-owned: never pinned, whoever's files name it
- if got := with(func(e map[string]string) { e[name] = "box-a" }, map[string]bool{name: true}); got != pin {
- t.Errorf("%s present changed the pin", name)
+ // agent-owned and the box's: left out
+ if got := with(func(e map[string]string) { e[name] = "box-a" }, nil); got != pin {
+ t.Errorf("%s from the box changed the pin", name)
+ }
+ // the exclusion rule (fix round 4): a box-owned name the instance
+ // supplied is pinned. The composer never reports an agent-owned
+ // name so (ExecBackend.LaunchLayers), as the agent decides it.
+ if got := with(func(e map[string]string) { e[name] = "box-a" }, map[string]bool{name: true}); got == pin {
+ t.Errorf("%s supplied by the instance left the pin unchanged", name)
}
}
for name := range BoxVars {
@@ -147,15 +153,16 @@ func TestPortableDigestCoversTheInstanceOnly(t *testing.T) {
}
}
-// TestPinnedRule: agent-owned never, a box variable only when the instance
-// supplied it, anything else always.
+// TestPinnedRule (fix round 4, platform-1e's exclusion rule): a name is
+// left out only when it is box-owned and the instance did not supply it;
+// everything else is pinned.
func TestPinnedRule(t *testing.T) {
for _, c := range []struct {
name string
instance bool
want bool
}{
- {RegionVar, false, false}, {RegionVar, true, false}, {"CARLOS_HOST", true, false},
+ {RegionVar, false, false}, {RegionVar, true, true}, {"CARLOS_HOST", false, false}, {"CARLOS_HOST", true, true},
{"HOSTNAME", false, false}, {"HOSTNAME", true, true}, {"PWD", true, true},
{"APP_MODE", false, true}, {"APP_MODE", true, true},
} {
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 503ef88a1..2943c317c 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -744,13 +744,13 @@ func TestPlan14RootReadCensus(t *testing.T) {
// Census overlay home-part2 (amadan #236, moves into home part 2): T3's
// home rule (moves.CompareEnv's targetHome) and the move-portable env
// pin (moves.Pinned, ReportLaunch, LocalDigest; ExecBackend.LaunchLayers).
- // Eleven changed or new units re-frozen at main's dispositions, nine of
- // them legacy, add nine frozen declarations. Ruling owed: the Plan 14
+ // Twelve changed or new units re-frozen at main's dispositions, ten of
+ // them legacy, add ten frozen declarations. Ruling owed: the Plan 14
// owner has not yet approved it.
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-home-part2.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
- seal = "8d9a063305974175e1486b3a0b5e61ba298bbff6a637ecf77503c0ca2447015d"
+ seal = "351eba46c02a820d32e087490114f82c1bfdfc1d432d136b8bce271da268ac72"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-home-part2.tsv b/internal/provision/testdata/plan14-root-reads-home-part2.tsv
index b01fc351b..bc1c9fe82 100644
--- a/internal/provision/testdata/plan14-root-reads-home-part2.tsv
+++ b/internal/provision/testdata/plan14-root-reads-home-part2.tsv
@@ -1,29 +1,29 @@
-# Plan 14 census overlay home-part2 (amadan #236, moves into home part 2, Tasks 1 and 2b and 2b fix round 2; loaded after hostfiles-cap-236). RULING OWED: the Plan 14 owner has not yet approved this overlay.
-# Task 1: moves.CompareEnv takes targetHome, and T3 (internal/host checkEnv) passes rec.Move.To == "" for it: a home target must set no CARLOS_REGION and fails region-present when it does. Task 2b and its fix round 2: internal/moves owns the move-portable env rule once (AgentOwnedVars, BoxVars, Pinned: agent-owned variables never pinned, a box variable pinned only when the instance's own layers supplied it; ReportLaunch, PortableDigest), and LocalDigest, the whole composed env for a check made on the box that recorded it. The composer reports the instance's layer names (activator ExecBackend.LaunchLayers, LaunchEnvironment's composition moved into its own unit). The source pin, the cut's currentInputs and the probe owners' checks hash the portable env; the recovery records recoveryInputs and checks localEnvDigest, the whole local env.
-# Env digests, by where they are compared. Across boxes, the move-portable env: the source pin writeSourceReport sr.Env.Digest (host/movesphase1.go:875) copied to the target report's Inputs.EnvReport (host/movesreport.go:276) and checked on the target by the probe owners' Select and Materialize (host/movesprobeowners.go envDigest) and by the committed wake (committedLaunchIdentity against pinnedLaunchIdentity, selectCommitted and InputPolicy); the cut's currentInputs (host/movescut.go), compared on the source with that pin. Same box, the whole local env: the recovery's recoveryInputs (host/movescut.go, recorded through host/movesrecoveryprobe.go:139-146 on the recovering box) checked by the recovery owners' Select and Materialize (host/movesrecoveryowners.go localEnvDigest). Same box, full composed env kept and unchanged: ClaimLaunchDigest (host/claimoperations.go:70, host/claimowners.go:400), compared only with the attempt's own box, boot and controller (host/movesprobeattempts.go:312 and :353, host/relocationattempts.go:265, host/claimlaunch_authority.go:89-96, host/claimwakestop.go:162, activator/servingretry.go:188 and :222, activator/rolelaunch_linux.go:37 and :135, activator/rolelaunch_root_linux.go:41); snapshot.Identity.Config (host/claimmoveaccess.go:191), which the moves owners never read (they set their own identity from the pins). movesReadLaunch (host/moveswakesource.go:207) discards the launch source's EnvReport, not its identity.
-# Re-frozen units, every one at main's disposition (class, task and basis unchanged; kinds and digest refreshed). Authority rows, read set unchanged (only the source offsets moved): host/claimmoveaccess.go claimMoveLaunchSource (authority C,G,J1; it now calls LaunchLayers and returns the instance's layer names) and host/movespass.go armPhase1 (authority B2,C; its LaunchEnv hook now returns the layer names). Legacy rows: activator ExecBackend.LaunchEnvironment (now a wrapper of LaunchLayers) and the new ExecBackend.LaunchLayers (LaunchEnvironment's composition and reads, moved; LaunchEnvironment's disposition); host movesWorker.writeSourceReport (ReportLaunch with the layer names); movesProbeOwners.Select, Materialize, selectCommitted and materializeCommitted (envDigest with the layer names); movesRecoveryOwners.Select and Materialize (localEnvDigest). No new read and no root decision. Nine frozen declarations are added.
-# ClaimLaunchDigest (host/claimoperations.go, fix round 3): envelope only, it reads nothing and is no root-read unit. It covers ClaimLaunchInputs.Instance, sorted, only when non-empty, so every launch without the instance's layer names keeps main's digest byte for byte (TestClaimLaunchDigestCoversInstanceOnlyWhenSet pins main's bytes). Its comparisons stay same-box (listed above).
-# Fix round 3: the env report and pin are version 2 (moves.EnvReportVersion, envPinPrefix "env2:sha256:"). T3 (checkEnv) refuses a v1 source report and the probe owners (Select, Materialize, selectCommitted, InputPolicy) refuse a v1 pin, fail closed; movesProbeOwners.Select, Materialize and selectCommitted are re-frozen again at main's dispositions, read sets unchanged. The rows below replace this overlay's earlier, unruled rows for the same units.
+# Plan 14 census overlay home-part2 (amadan #236, moves into home part 2, Tasks 1 and 2b with its fix rounds 2 to 4; loaded after hostfiles-cap-236). RULING OWED: the Plan 14 owner has not yet approved this overlay.
+# Task 1: moves.CompareEnv takes targetHome, and T3 (internal/host checkEnv) passes rec.Move.To == "" for it: a home target must set no CARLOS_REGION and fails region-present when it does. Task 2b: internal/moves owns the move-portable env rule once: a name is left out of the pin only when it is box-owned (AgentOwnedVars, BoxVars) and the instance's own layers did not supply its running value (Pinned); ReportLaunch and PortableDigest make the v2 report and pin (EnvReportVersion, "env2:sha256:"), and LocalDigest the whole env for a check made on the box that recorded it. The composer reports the instance's layer names (activator ExecBackend.LaunchLayers, LaunchEnvironment's composition moved into its own unit), never a name the agent decides (agentOwnedNames). The moves launch source (claimMoveLaunchSource) makes the env report from that one composition and returns it with the launch; the probe owners take its digest as the launch's pin (movesReadLaunch passes it through) and keep it in memory for Materialize, bound to the exact env. No layer names cross into the claim: ClaimServingLaunch, ClaimLaunchInputs and ClaimLaunchDigest are main's. T3 refuses a v1 source report and the probe owners a v1 pin, fail closed. The recovery records recoveryInputs and checks localEnvDigest, the whole local env.
+# Env digests, by where they are compared. Across boxes, the move-portable v2 pin: the source pin writeSourceReport sr.Env.Digest (host/movesphase1.go) copied to the target report's Inputs.EnvReport (host/movesreport.go) and checked on the target against the launch's own pin, the digest of the env report claimMoveLaunchSource makes from the same composition as the launch's env (host/claimmoveaccess.go, passed through by movesReadLaunch, host/moveswakesource.go): the probe owners' Select and the committed wake's selectCommitted compare it, and Materialize and materializeCommitted check the pin Select captured in memory for exactly that env (host/movesprobeowners.go captureEnvPin); the cut's currentInputs (host/movescut.go), compared on the source with that pin. Same box, the whole local env: the recovery's recoveryInputs (host/movescut.go, recorded through host/movesrecoveryprobe.go:139-146 on the recovering box) checked by the recovery owners' Select and Materialize (host/movesrecoveryowners.go localEnvDigest). Same box, main's full composed env, unchanged: ClaimLaunchDigest (host/claimoperations.go, byte for byte main's; it is root's LaunchSpec hash), compared only with the attempt's own box, boot and controller (host/movesprobeattempts.go, host/relocationattempts.go, host/claimlaunch_authority.go, host/claimwakestop.go, activator/servingretry.go, activator/rolelaunch_linux.go, activator/rolelaunch_root_linux.go); snapshot.Identity.Config (host/claimmoveaccess.go), which the moves owners never read (they set their own identity from the pins).
+# Re-frozen units, every one at main's disposition (class, task and basis unchanged; kinds and digest refreshed); these rows replace this overlay's earlier, unruled rows. Authority rows, read set unchanged (only source offsets moved): host/claimmoveaccess.go claimMoveLaunchSource (authority C,G,J1; it calls LaunchLayers and reports the env from that composition) and host/movespass.go armPhase1 (authority B2,C; its LaunchEnv hook returns the layer names). Legacy rows: activator ExecBackend.LaunchEnvironment (a wrapper of LaunchLayers) and the new ExecBackend.LaunchLayers (LaunchEnvironment's composition and reads, moved; LaunchEnvironment's disposition); host movesWorker.writeSourceReport (ReportLaunch with the layer names); movesProbeOwners.Select, Materialize, selectCommitted and materializeCommitted (the captured report's pin, v2 only); movesRecoveryOwners.Select and Materialize (localEnvDigest); moveswakesource.go movesReadLaunch (passes the access's report through). No new read and no root decision. Ten frozen legacy declarations are added.
@legacy 849ce111c61c3dcedd80595348d0a26c67fb47462d497fba63d52d2c94e1d5d0 internal/activator/backend_exec.go ExecBackend.LaunchEnvironment legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route whole:Route@139 e8853e15b427b80a701b5e10b565f7d4f0b778e0c8185da63266e2408f6b4113
internal/activator/backend_exec.go ExecBackend.LaunchEnvironment legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route whole:Route@139 e8853e15b427b80a701b5e10b565f7d4f0b778e0c8185da63266e2408f6b4113
-@legacy 1c98a7ecd93fe84486af8cbc09bd1cb31915c69c16785c3fd3bcdd6d21eb6849 internal/activator/backend_exec.go ExecBackend.LaunchLayers legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:Socket@689,whole:Route@179,whole:Route@685 a82c0f1740d9242da29248be3a009ab34a528df105e3a269b91b49e40c839261
-internal/activator/backend_exec.go ExecBackend.LaunchLayers legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:Socket@689,whole:Route@179,whole:Route@685 a82c0f1740d9242da29248be3a009ab34a528df105e3a269b91b49e40c839261
-internal/host/claimmoveaccess.go claimMoveLaunchSource authority C,G,J1 root-route;kept=C,G;scope=protected-decision-only whole:Route@395,whole:Route@718,whole:Route@853 57730ed7d713836f148b5336d3cd2823e2cb6e7baf04f743088e58b079aa021a
+@legacy 1680e7269c40013e72c2f0a39abc13ee75cecadf2b3b8aa3d3a0278c060458ce internal/activator/backend_exec.go ExecBackend.LaunchLayers legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:Socket@751,whole:Route@179,whole:Route@747 66a14ac6d0a4c2c9333adf45a792407d8cc4fa389416feb31fcf58616fc5b611
+internal/activator/backend_exec.go ExecBackend.LaunchLayers legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:Socket@751,whole:Route@179,whole:Route@747 66a14ac6d0a4c2c9333adf45a792407d8cc4fa389416feb31fcf58616fc5b611
+internal/host/claimmoveaccess.go claimMoveLaunchSource authority C,G,J1 root-route;kept=C,G;scope=protected-decision-only whole:Route@395,whole:Route@718,whole:Route@853 cb8c06cf6f03628aa56f021f3d0c1d182d9792635279c8b1e91c27e2cb1019fa
internal/host/movespass.go armPhase1 authority B2,C rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with the S1 probe hook refusing a row whose import gate is set; main's B2,C authority retained registry:ImportGate@8279,whole:Route@8266,whole:Route@8408,whole:Route@8757,whole:Route@8898 b24834b54b93a21e8ba0757fa8f61a9a5eee2ea3c8fa85d634ef4ce4aba1c85d
@legacy 6080eef9badd1adefd1a5c4e7a6dd3c64999b8cf58eb016adbe4e72ab8efbff2 internal/host/movesphase1.go movesWorker.writeSourceReport legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input file:Stat@753 585c0c5bb9b8c20d5a5d77dc08e49720268951ec30bbc1175db9c83900c817f2
internal/host/movesphase1.go movesWorker.writeSourceReport legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input file:Stat@753 585c0c5bb9b8c20d5a5d77dc08e49720268951ec30bbc1175db9c83900c817f2
-@legacy ee65a41f912901bc0645265455b564ab7bfd2dc08875dcade50bec796444bb69 internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 6b21920ef49548fba1bfee767e496851451918eadde1ecc85f2a264eb91d3027
-internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 6b21920ef49548fba1bfee767e496851451918eadde1ecc85f2a264eb91d3027
-@legacy d3d37e02c781782a73bc9044cff03e90440ba490259914550017a0fa0763842c internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3175,whole:Route@206,whole:Route@2284,whole:Route@465,whole:Route@765 3bd1707831d739695d687fb3074db3f2911bec36fbcd55efadd0f2bec59478cb
-internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3175,whole:Route@206,whole:Route@2284,whole:Route@465,whole:Route@765 3bd1707831d739695d687fb3074db3f2911bec36fbcd55efadd0f2bec59478cb
-@legacy 45cda5b26dc120bbd9f580493ce79219ce21501f3de390c29a859dad6801c5aa internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@629,whole:Route@616,whole:Route@712 f0a7780b71f42a05595eec24ddb3654d621205a403b6d18c0cba4391209b9554
-internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@629,whole:Route@616,whole:Route@712 f0a7780b71f42a05595eec24ddb3654d621205a403b6d18c0cba4391209b9554
-@legacy c270bb7c3c88c9fc63421868b5bccff36fe89e341723b982f7314792e821f5d6 internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1663,whole:Route@176,whole:Route@611 31c9975dbc5307ccdaa70d7b0821b08b717fa716716fac938095d8d01f512357
-internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1663,whole:Route@176,whole:Route@611 31c9975dbc5307ccdaa70d7b0821b08b717fa716716fac938095d8d01f512357
+@legacy 31374a531ac40f3a026dda50d7a0e34f5dc20cfc9c2efa542179153fb1bd5a6b internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 74025530a72fa4db43de8510ce1757f333e8fc29aeb4a4d233faaa4e1be525bb
+internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 74025530a72fa4db43de8510ce1757f333e8fc29aeb4a4d233faaa4e1be525bb
+@legacy 21682c4aef12b99459a3f35f076b1514353a7d68de6f4cd17e61ff75037de8e5 internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3185,registry:Host@3305,whole:Route@206,whole:Route@2292,whole:Route@465,whole:Route@765 caa777dee227adb4efe3e56ce19651c35c777e042dd425fa34a294b87940b0b2
+internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3185,registry:Host@3305,whole:Route@206,whole:Route@2292,whole:Route@465,whole:Route@765 caa777dee227adb4efe3e56ce19651c35c777e042dd425fa34a294b87940b0b2
+@legacy 4974c2d7e20a2ce3851c4ffb0e9a13ed30bc4f6dc9735be24499db02e8ef5226 internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@652,whole:Route@639,whole:Route@735 4d00a204256a26f0d3158ef601c9dd96fee156dfac743b9a47132c3c45b34630
+internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@652,whole:Route@639,whole:Route@735 4d00a204256a26f0d3158ef601c9dd96fee156dfac743b9a47132c3c45b34630
+@legacy 1df478369249b1745a665becd8378c7a9d432e5f0c91e69990aeb9a5b3083165 internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1608,registry:Host@1690,whole:Route@176,whole:Route@619 b398f595a9b8e2b295b7c82c4c678345186cbed3e831590ece16dffc491fece2
+internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1608,registry:Host@1690,whole:Route@176,whole:Route@619 b398f595a9b8e2b295b7c82c4c678345186cbed3e831590ece16dffc491fece2
@legacy 7ba24c0aa59e9f30cdf08fdd9e7acb056ab11672ae774772df2c7bb3bbb15ac3 internal/host/movesrecoveryowners.go movesRecoveryOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@577,whole:Route@1566,whole:Route@750 a87029143a13dc05d5b873b6b4f099515be737f9e0188586fac5fe91719ccd81
internal/host/movesrecoveryowners.go movesRecoveryOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@577,whole:Route@1566,whole:Route@750 a87029143a13dc05d5b873b6b4f099515be737f9e0188586fac5fe91719ccd81
-@legacy 98403405dd4f1aa000814d268c71477fd7b3f54fe1921270f8ad3ffc41ec7c54 internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1753,registry:DBPath@792,whole:Route@1008,whole:Route@514 35686d5e12ca3008cfefe0e8a15fcfc8d1e7c120adf94619a55fa948f4554921
-internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1753,registry:DBPath@792,whole:Route@1008,whole:Route@514 35686d5e12ca3008cfefe0e8a15fcfc8d1e7c120adf94619a55fa948f4554921
-@coverage internal/activator 4b4b243c37f98df546ac893baaf98a80ae805705492dcd0792f532d41976766c
-@coverage internal/host 3699333bae7ea249340acd261f3de74ed3969bb5fcdb4cad988a0b38a2473553
-@coverage internal/moves d395a0a343cbdea42a899ed2fdb2fce7c352f5e6f3a31c2795ccd84a28eebfef
+@legacy c8ceeedc46203dacdf33f82908b901fe25afea0de4f86401d27bbfb5ab27293c internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1756,registry:DBPath@792,whole:Route@1011,whole:Route@514 746a4c5f4a87e6b5210ae12e986af05ea8bf3849d940a4fec6598000b555cd46
+internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1756,registry:DBPath@792,whole:Route@1011,whole:Route@514 746a4c5f4a87e6b5210ae12e986af05ea8bf3849d940a4fec6598000b555cd46
+@legacy 1a77e1c538d55076eb81dbe54e0c110803dbafd56a79990246430473fca9a16d internal/host/moveswakesource.go movesReadLaunch legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Moves launch-input adapter selects by host through existing read interface; generic launch payload selection remains legacy. registry:Host@258 780a60ccf1035f2c7d64fdc804a867b64d380b05a51b151155ca6efe70b01767
+internal/host/moveswakesource.go movesReadLaunch legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Moves launch-input adapter selects by host through existing read interface; generic launch payload selection remains legacy. registry:Host@258 780a60ccf1035f2c7d64fdc804a867b64d380b05a51b151155ca6efe70b01767
+@coverage internal/activator 3e4fae49c0669c0524ec2cadcedf5d86de1bc10df1ebe4ad370d99b5712819f1
+@coverage internal/host 500e7528b90e328c2e89ed753844c3325a3f206a6e016fceeac48f5934c390df
+@coverage internal/moves 51ae19338bf9f92d45cc166f9fb71cdd3280c617892f71bb9ce67b2b00a7ae27