detached 4e0fee9

refactor(app): keep only the production broker composition

commit
4e0fee95183da1e4ccc96439e2e4ad30aa7aedaa
author
brice · · 2026-09-23 15:39 UTC
parent
f7c33a7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
22 files · +343 -1816
M internal/adapters/brokerconfig/config.go +0 -82
@@ -120,53 +120,6 @@ type Layout struct {
120120 reserved []string
121121 }
122122
123-// ResolveLayout validates one offline root and derives its sandbox paths.
124-//
125-// The root must be absolute and clean, must not be the filesystem root, must
126-// not contain a symlinked component, and must not equal, contain, or sit inside
127-// any reserved production directory. Reserved directories are compared cleaned
128-// and absolute; empty entries are ignored. The result is purely textual: no
129-// directory is created here, and the caller secures the paths with
130-// pkg/safedir.EnsurePrivate.
131-func ResolveLayout(rawRoot string, reserved []string) (Layout, error) {
132- if rawRoot == "" {
133- return Layout{}, errors.New("brokerconfig: offline root is empty")
134- }
135- if !filepath.IsAbs(rawRoot) {
136- return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not absolute", rawRoot)
137- }
138- if cleaned := filepath.Clean(rawRoot); cleaned != rawRoot {
139- return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not clean", rawRoot)
140- }
141- if rawRoot == string(filepath.Separator) {
142- return Layout{}, errors.New("brokerconfig: offline root must not be the filesystem root")
143- }
144- if err := rejectSymlinkedComponents(rawRoot); err != nil {
145- return Layout{}, err
146- }
147- for _, other := range reserved {
148- if other == "" {
149- continue
150- }
151- other = filepath.Clean(other)
152- if !filepath.IsAbs(other) {
153- return Layout{}, fmt.Errorf("brokerconfig: reserved directory %q is not absolute", other)
154- }
155- if overlaps(rawRoot, other) {
156- return Layout{}, fmt.Errorf("brokerconfig: offline root %q overlaps production path %q", rawRoot, other)
157- }
158- }
159- runtime := filepath.Join(rawRoot, RuntimeDirName)
160- return Layout{
161- Root: rawRoot,
162- Runtime: runtime,
163- State: filepath.Join(rawRoot, StateDirName),
164- Log: filepath.Join(rawRoot, LogDirName),
165- Spawn: filepath.Join(runtime, SpawnDirName),
166- reserved: append([]string(nil), reserved...),
167- }, nil
168-}
169-
170123 // VerifyCreated re-walks every sandbox-owned path and refuses any existing
171124 // component that is a symlink.
172125 //
@@ -289,22 +242,6 @@ type Config struct {
289242 warmIdleTimeout time.Duration
290243 }
291244
292-// Load reads and strictly validates the config marker inside layout.Root.
293-//
294-// The file must be a regular owner-only file that is not a symlink and is
295-// bounded by MaxConfigBytes; the JSON must carry the exact marker, reject
296-// unknown fields, duplicate keys, trailing data, invalid UTF-8, and excessive
297-// nesting. Every registration is validated independently and endpoints must be
298-// unique. Two registrations may alias one identical route (the same opaque
299-// address) under different endpoints, but two that share the pool's exact
300-// (authenticated identity, policy) pair must agree on that one route: the pool
301-// keys a physical transport by the pair and ignores the address, so a second
302-// route for the same pair is refused rather than silently dialed or ignored. A
303-// missing or malformed config is an error, never an empty default.
304-func Load(layout Layout) (*Config, error) {
305- return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
306-}
307-
308245 // LoadPath parses the broker configuration at an explicit application-owned
309246 // path while retaining the route and reserved-path validation from layout.
310247 func LoadPath(layout Layout, path string) (*Config, error) {
@@ -543,25 +480,6 @@ func (c *Config) RouteByAddress(address string) (Route, bool) {
543480 return route, ok
544481 }
545482
546-// LocalMuxRoute returns the single provisioned Unix route a remote-side mux
547-// helper bridges to. A configuration that provisions no Unix route, or more
548-// than one, is refused: a helper must never guess which daemon-mux endpoint it
549-// serves. The local binding's own carriage is deliberately excluded: the broker
550-// dials it itself, and no remote-side helper ever bridges it.
551-func (c *Config) LocalMuxRoute() (Route, error) {
552- if c == nil {
553- return Route{}, errors.New("brokerconfig: no configuration")
554- }
555- switch len(c.unixRoutes) {
556- case 0:
557- return Route{}, errors.New("brokerconfig: no Unix daemon-mux route is provisioned")
558- case 1:
559- return c.unixRoutes[0], nil
560- default:
561- return Route{}, errors.New("brokerconfig: more than one Unix daemon-mux route is provisioned")
562- }
563-}
564-
565483 // Resolver returns the immutable endpoint resolver over this configuration.
566484 func (c *Config) Resolver() *Resolver {
567485 if c == nil {
@@ -120,53 +120,6 @@ type Layout struct {
120 reserved []string120 reserved []string
121 }121 }
122 122
123-// ResolveLayout validates one offline root and derives its sandbox paths.
124-//
125-// The root must be absolute and clean, must not be the filesystem root, must
126-// not contain a symlinked component, and must not equal, contain, or sit inside
127-// any reserved production directory. Reserved directories are compared cleaned
128-// and absolute; empty entries are ignored. The result is purely textual: no
129-// directory is created here, and the caller secures the paths with
130-// pkg/safedir.EnsurePrivate.
131-func ResolveLayout(rawRoot string, reserved []string) (Layout, error) {
132- if rawRoot == "" {
133- return Layout{}, errors.New("brokerconfig: offline root is empty")
134- }
135- if !filepath.IsAbs(rawRoot) {
136- return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not absolute", rawRoot)
137- }
138- if cleaned := filepath.Clean(rawRoot); cleaned != rawRoot {
139- return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not clean", rawRoot)
140- }
141- if rawRoot == string(filepath.Separator) {
142- return Layout{}, errors.New("brokerconfig: offline root must not be the filesystem root")
143- }
144- if err := rejectSymlinkedComponents(rawRoot); err != nil {
145- return Layout{}, err
146- }
147- for _, other := range reserved {
148- if other == "" {
149- continue
150- }
151- other = filepath.Clean(other)
152- if !filepath.IsAbs(other) {
153- return Layout{}, fmt.Errorf("brokerconfig: reserved directory %q is not absolute", other)
154- }
155- if overlaps(rawRoot, other) {
156- return Layout{}, fmt.Errorf("brokerconfig: offline root %q overlaps production path %q", rawRoot, other)
157- }
158- }
159- runtime := filepath.Join(rawRoot, RuntimeDirName)
160- return Layout{
161- Root: rawRoot,
162- Runtime: runtime,
163- State: filepath.Join(rawRoot, StateDirName),
164- Log: filepath.Join(rawRoot, LogDirName),
165- Spawn: filepath.Join(runtime, SpawnDirName),
166- reserved: append([]string(nil), reserved...),
167- }, nil
168-}
169-
170 // VerifyCreated re-walks every sandbox-owned path and refuses any existing123 // VerifyCreated re-walks every sandbox-owned path and refuses any existing
171 // component that is a symlink.124 // component that is a symlink.
172 //125 //
@@ -289,22 +242,6 @@ type Config struct {
289 warmIdleTimeout time.Duration242 warmIdleTimeout time.Duration
290 }243 }
291 244
292-// Load reads and strictly validates the config marker inside layout.Root.
293-//
294-// The file must be a regular owner-only file that is not a symlink and is
295-// bounded by MaxConfigBytes; the JSON must carry the exact marker, reject
296-// unknown fields, duplicate keys, trailing data, invalid UTF-8, and excessive
297-// nesting. Every registration is validated independently and endpoints must be
298-// unique. Two registrations may alias one identical route (the same opaque
299-// address) under different endpoints, but two that share the pool's exact
300-// (authenticated identity, policy) pair must agree on that one route: the pool
301-// keys a physical transport by the pair and ignores the address, so a second
302-// route for the same pair is refused rather than silently dialed or ignored. A
303-// missing or malformed config is an error, never an empty default.
304-func Load(layout Layout) (*Config, error) {
305- return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
306-}
307-
308 // LoadPath parses the broker configuration at an explicit application-owned245 // LoadPath parses the broker configuration at an explicit application-owned
309 // path while retaining the route and reserved-path validation from layout.246 // path while retaining the route and reserved-path validation from layout.
310 func LoadPath(layout Layout, path string) (*Config, error) {247 func LoadPath(layout Layout, path string) (*Config, error) {
@@ -543,25 +480,6 @@ func (c *Config) RouteByAddress(address string) (Route, bool) {
543 return route, ok480 return route, ok
544 }481 }
545 482
546-// LocalMuxRoute returns the single provisioned Unix route a remote-side mux
547-// helper bridges to. A configuration that provisions no Unix route, or more
548-// than one, is refused: a helper must never guess which daemon-mux endpoint it
549-// serves. The local binding's own carriage is deliberately excluded: the broker
550-// dials it itself, and no remote-side helper ever bridges it.
551-func (c *Config) LocalMuxRoute() (Route, error) {
552- if c == nil {
553- return Route{}, errors.New("brokerconfig: no configuration")
554- }
555- switch len(c.unixRoutes) {
556- case 0:
557- return Route{}, errors.New("brokerconfig: no Unix daemon-mux route is provisioned")
558- case 1:
559- return c.unixRoutes[0], nil
560- default:
561- return Route{}, errors.New("brokerconfig: more than one Unix daemon-mux route is provisioned")
562- }
563-}
564-
565 // Resolver returns the immutable endpoint resolver over this configuration.483 // Resolver returns the immutable endpoint resolver over this configuration.
566 func (c *Config) Resolver() *Resolver {484 func (c *Config) Resolver() *Resolver {
567 if c == nil {485 if c == nil {
A internal/adapters/brokerconfig/legacy_sandbox_test.go +89 -0
@@ -0,0 +1,89 @@
1+package brokerconfig
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "path/filepath"
7+)
8+
9+// ResolveLayout validates one offline root and derives its sandbox paths.
10+//
11+// The root must be absolute and clean, must not be the filesystem root, must
12+// not contain a symlinked component, and must not equal, contain, or sit inside
13+// any reserved production directory. Reserved directories are compared cleaned
14+// and absolute; empty entries are ignored. The result is purely textual: no
15+// directory is created here, and the caller secures the paths with
16+// pkg/safedir.EnsurePrivate.
17+func ResolveLayout(rawRoot string, reserved []string) (Layout, error) {
18+ if rawRoot == "" {
19+ return Layout{}, errors.New("brokerconfig: offline root is empty")
20+ }
21+ if !filepath.IsAbs(rawRoot) {
22+ return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not absolute", rawRoot)
23+ }
24+ if cleaned := filepath.Clean(rawRoot); cleaned != rawRoot {
25+ return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not clean", rawRoot)
26+ }
27+ if rawRoot == string(filepath.Separator) {
28+ return Layout{}, errors.New("brokerconfig: offline root must not be the filesystem root")
29+ }
30+ if err := rejectSymlinkedComponents(rawRoot); err != nil {
31+ return Layout{}, err
32+ }
33+ for _, other := range reserved {
34+ if other == "" {
35+ continue
36+ }
37+ other = filepath.Clean(other)
38+ if !filepath.IsAbs(other) {
39+ return Layout{}, fmt.Errorf("brokerconfig: reserved directory %q is not absolute", other)
40+ }
41+ if overlaps(rawRoot, other) {
42+ return Layout{}, fmt.Errorf("brokerconfig: offline root %q overlaps production path %q", rawRoot, other)
43+ }
44+ }
45+ runtime := filepath.Join(rawRoot, RuntimeDirName)
46+ return Layout{
47+ Root: rawRoot,
48+ Runtime: runtime,
49+ State: filepath.Join(rawRoot, StateDirName),
50+ Log: filepath.Join(rawRoot, LogDirName),
51+ Spawn: filepath.Join(runtime, SpawnDirName),
52+ reserved: append([]string(nil), reserved...),
53+ }, nil
54+}
55+
56+// Load reads and strictly validates the config marker inside layout.Root.
57+//
58+// The file must be a regular owner-only file that is not a symlink and is
59+// bounded by MaxConfigBytes; the JSON must carry the exact marker, reject
60+// unknown fields, duplicate keys, trailing data, invalid UTF-8, and excessive
61+// nesting. Every registration is validated independently and endpoints must be
62+// unique. Two registrations may alias one identical route (the same opaque
63+// address) under different endpoints, but two that share the pool's exact
64+// (authenticated identity, policy) pair must agree on that one route: the pool
65+// keys a physical transport by the pair and ignores the address, so a second
66+// route for the same pair is refused rather than silently dialed or ignored. A
67+// missing or malformed config is an error, never an empty default.
68+func Load(layout Layout) (*Config, error) {
69+ return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
70+}
71+
72+// LocalMuxRoute returns the single provisioned Unix route a remote-side mux
73+// helper bridges to. A configuration that provisions no Unix route, or more
74+// than one, is refused: a helper must never guess which daemon-mux endpoint it
75+// serves. The local binding's own carriage is deliberately excluded: the broker
76+// dials it itself, and no remote-side helper ever bridges it.
77+func (c *Config) LocalMuxRoute() (Route, error) {
78+ if c == nil {
79+ return Route{}, errors.New("brokerconfig: no configuration")
80+ }
81+ switch len(c.unixRoutes) {
82+ case 0:
83+ return Route{}, errors.New("brokerconfig: no Unix daemon-mux route is provisioned")
84+ case 1:
85+ return c.unixRoutes[0], nil
86+ default:
87+ return Route{}, errors.New("brokerconfig: more than one Unix daemon-mux route is provisioned")
88+ }
89+}
@@ -0,0 +1,89 @@
1+package brokerconfig
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "path/filepath"
7+)
8+
9+// ResolveLayout validates one offline root and derives its sandbox paths.
10+//
11+// The root must be absolute and clean, must not be the filesystem root, must
12+// not contain a symlinked component, and must not equal, contain, or sit inside
13+// any reserved production directory. Reserved directories are compared cleaned
14+// and absolute; empty entries are ignored. The result is purely textual: no
15+// directory is created here, and the caller secures the paths with
16+// pkg/safedir.EnsurePrivate.
17+func ResolveLayout(rawRoot string, reserved []string) (Layout, error) {
18+ if rawRoot == "" {
19+ return Layout{}, errors.New("brokerconfig: offline root is empty")
20+ }
21+ if !filepath.IsAbs(rawRoot) {
22+ return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not absolute", rawRoot)
23+ }
24+ if cleaned := filepath.Clean(rawRoot); cleaned != rawRoot {
25+ return Layout{}, fmt.Errorf("brokerconfig: offline root %q is not clean", rawRoot)
26+ }
27+ if rawRoot == string(filepath.Separator) {
28+ return Layout{}, errors.New("brokerconfig: offline root must not be the filesystem root")
29+ }
30+ if err := rejectSymlinkedComponents(rawRoot); err != nil {
31+ return Layout{}, err
32+ }
33+ for _, other := range reserved {
34+ if other == "" {
35+ continue
36+ }
37+ other = filepath.Clean(other)
38+ if !filepath.IsAbs(other) {
39+ return Layout{}, fmt.Errorf("brokerconfig: reserved directory %q is not absolute", other)
40+ }
41+ if overlaps(rawRoot, other) {
42+ return Layout{}, fmt.Errorf("brokerconfig: offline root %q overlaps production path %q", rawRoot, other)
43+ }
44+ }
45+ runtime := filepath.Join(rawRoot, RuntimeDirName)
46+ return Layout{
47+ Root: rawRoot,
48+ Runtime: runtime,
49+ State: filepath.Join(rawRoot, StateDirName),
50+ Log: filepath.Join(rawRoot, LogDirName),
51+ Spawn: filepath.Join(runtime, SpawnDirName),
52+ reserved: append([]string(nil), reserved...),
53+ }, nil
54+}
55+
56+// Load reads and strictly validates the config marker inside layout.Root.
57+//
58+// The file must be a regular owner-only file that is not a symlink and is
59+// bounded by MaxConfigBytes; the JSON must carry the exact marker, reject
60+// unknown fields, duplicate keys, trailing data, invalid UTF-8, and excessive
61+// nesting. Every registration is validated independently and endpoints must be
62+// unique. Two registrations may alias one identical route (the same opaque
63+// address) under different endpoints, but two that share the pool's exact
64+// (authenticated identity, policy) pair must agree on that one route: the pool
65+// keys a physical transport by the pair and ignores the address, so a second
66+// route for the same pair is refused rather than silently dialed or ignored. A
67+// missing or malformed config is an error, never an empty default.
68+func Load(layout Layout) (*Config, error) {
69+ return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
70+}
71+
72+// LocalMuxRoute returns the single provisioned Unix route a remote-side mux
73+// helper bridges to. A configuration that provisions no Unix route, or more
74+// than one, is refused: a helper must never guess which daemon-mux endpoint it
75+// serves. The local binding's own carriage is deliberately excluded: the broker
76+// dials it itself, and no remote-side helper ever bridges it.
77+func (c *Config) LocalMuxRoute() (Route, error) {
78+ if c == nil {
79+ return Route{}, errors.New("brokerconfig: no configuration")
80+ }
81+ switch len(c.unixRoutes) {
82+ case 0:
83+ return Route{}, errors.New("brokerconfig: no Unix daemon-mux route is provisioned")
84+ case 1:
85+ return c.unixRoutes[0], nil
86+ default:
87+ return Route{}, errors.New("brokerconfig: more than one Unix daemon-mux route is provisioned")
88+ }
89+}
A internal/app/app_sources_test.go +33 -0
@@ -0,0 +1,33 @@
1+package app
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+
8+ "github.com/stretchr/testify/require"
9+)
10+
11+// loadAppSources reads every Go file of this package, keyed by file name, so
12+// composition tests can assert which files own a given call.
13+func loadAppSources(t *testing.T, includeTests bool) map[string]string {
14+ t.Helper()
15+
16+ entries, err := os.ReadDir(".")
17+ require.NoError(t, err, "read app package dir")
18+ sources := make(map[string]string)
19+ for _, entry := range entries {
20+ name := entry.Name()
21+ if entry.IsDir() || !strings.HasSuffix(name, ".go") {
22+ continue
23+ }
24+ if !includeTests && strings.HasSuffix(name, "_test.go") {
25+ continue
26+ }
27+ raw, err := os.ReadFile(name)
28+ require.NoError(t, err, "read app source %q", name)
29+ sources[name] = string(raw)
30+ }
31+ require.NotEmpty(t, sources, "app sources must not be empty")
32+ return sources
33+}
@@ -0,0 +1,33 @@
1+package app
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+
8+ "github.com/stretchr/testify/require"
9+)
10+
11+// loadAppSources reads every Go file of this package, keyed by file name, so
12+// composition tests can assert which files own a given call.
13+func loadAppSources(t *testing.T, includeTests bool) map[string]string {
14+ t.Helper()
15+
16+ entries, err := os.ReadDir(".")
17+ require.NoError(t, err, "read app package dir")
18+ sources := make(map[string]string)
19+ for _, entry := range entries {
20+ name := entry.Name()
21+ if entry.IsDir() || !strings.HasSuffix(name, ".go") {
22+ continue
23+ }
24+ if !includeTests && strings.HasSuffix(name, "_test.go") {
25+ continue
26+ }
27+ raw, err := os.ReadFile(name)
28+ require.NoError(t, err, "read app source %q", name)
29+ sources[name] = string(raw)
30+ }
31+ require.NotEmpty(t, sources, "app sources must not be empty")
32+ return sources
33+}
M internal/app/broker_daemon_start.go +0 -75
@@ -8,7 +8,6 @@ import (
88 "path/filepath"
99 "syscall"
1010
11- "github.com/bnema/vev/internal/adapters/brokerconfig"
1211 "github.com/bnema/vev/internal/adapters/daemonmux"
1312 "github.com/bnema/vev/internal/adapters/ipc"
1413 "github.com/bnema/vev/internal/ports"
@@ -287,77 +286,3 @@ func parseBrokerDaemonStartArgvFlag(value string) (ports.BrokerDaemonStartMode,
287286 return 0, fmt.Errorf("vev: unknown daemon start mode %q (want existing-only or if-needed)", value)
288287 }
289288 }
290-
291-// helperCarriageTarget resolves the dial target for the private daemonmux
292-// carriage a remote-side mux helper bridges to, strictly from that helper's own
293-// broker-owned configuration. A helper owns no request and no client authority:
294-// the target is the one provisioned entry whose route is exactly the carriage
295-// being bridged, so the helper can only ever narrow what its own configuration
296-// already permits.
297-//
298-// The provisioned local binding wins when it names the carriage, because the
299-// local binding is the broker's own daemon authority. Otherwise the single
300-// registration that provisions this carriage supplies the fence, identity, and
301-// policy. A configuration that provisions neither yields no target, and a
302-// start-if-needed request is then refused rather than widened.
303-func helperCarriageTarget(config *brokerconfig.Config, route brokerconfig.Route) (ports.BrokerDialTarget, error) {
304- if binding, ok := config.LocalBinding(); ok && binding.Route.Address() == route.Address() {
305- return ports.BrokerDialTarget{
306- Fence: ports.BrokerEndpointFence{Local: true},
307- Address: binding.Route.Address(),
308- Policy: binding.Policy,
309- ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: binding.Identity, Bound: binding.Identity != ""},
310- }, nil
311- }
312- for _, endpoint := range config.Endpoints() {
313- registration, ok := config.Registration(endpoint)
314- if !ok || registration.Route.Address() != route.Address() {
315- continue
316- }
317- return ports.BrokerDialTarget{
318- Fence: ports.BrokerEndpointFence{Registration: registration.Registration},
319- Address: registration.Route.Address(),
320- Policy: registration.Policy,
321- ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: registration.Identity, Bound: registration.Identity != ""},
322- }, nil
323- }
324- return ports.BrokerDialTarget{}, localDaemonStartRefused("the helper configuration provisions no daemon authority for its mux carriage")
325-}
326-
327-// dialMuxHelperCarriage dials the private daemonmux carriage one remote-side mux
328-// helper bridges to, under the start authorization the broker propagated. The
329-// helper applies the same local election the broker does, from its own
330-// broker-owned configuration, and it never starts a broker, an observer, or a
331-// recursive helper: an existing-only mode is one carriage dial, and a
332-// start-if-needed mode is the shared daemon start gated by the helper's own
333-// provisioned policy and identity.
334-func dialMuxHelperCarriage(ctx context.Context, config *brokerconfig.Config, mode ports.BrokerDaemonStartMode) (daemonmux.RawFramedTransport, error) {
335- if config == nil {
336- return nil, errors.New("vev: broker mux helper requires a configuration")
337- }
338- if err := mode.Validate(); err != nil {
339- return nil, err
340- }
341- route, err := config.LocalMuxRoute()
342- if err != nil {
343- return nil, err
344- }
345- target, err := helperCarriageTarget(config, route)
346- if err != nil {
347- return nil, err
348- }
349- target.StartMode = mode
350- if err := target.Validate(); err != nil {
351- return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: err}
352- }
353- // Only a policy that names the launch token may start the daemon. A carriage
354- // provisioned under a non-launching policy refuses the start outright. The
355- // existing-only mode never reaches this check: dialBrokerDaemonCarriage
356- // returns its single dial's result immediately.
357- if mode == ports.BrokerDaemonStartIfNeeded {
358- if err := daemonStartRefusal(target); err != nil {
359- return nil, err
360- }
361- }
362- return dialBrokerDaemonCarriage(ctx, route.Path(), target)
363-}
@@ -8,7 +8,6 @@ import (
8 "path/filepath"8 "path/filepath"
9 "syscall"9 "syscall"
10 10
11- "github.com/bnema/vev/internal/adapters/brokerconfig"
12 "github.com/bnema/vev/internal/adapters/daemonmux"11 "github.com/bnema/vev/internal/adapters/daemonmux"
13 "github.com/bnema/vev/internal/adapters/ipc"12 "github.com/bnema/vev/internal/adapters/ipc"
14 "github.com/bnema/vev/internal/ports"13 "github.com/bnema/vev/internal/ports"
@@ -287,77 +286,3 @@ func parseBrokerDaemonStartArgvFlag(value string) (ports.BrokerDaemonStartMode,
287 return 0, fmt.Errorf("vev: unknown daemon start mode %q (want existing-only or if-needed)", value)286 return 0, fmt.Errorf("vev: unknown daemon start mode %q (want existing-only or if-needed)", value)
288 }287 }
289 }288 }
290-
291-// helperCarriageTarget resolves the dial target for the private daemonmux
292-// carriage a remote-side mux helper bridges to, strictly from that helper's own
293-// broker-owned configuration. A helper owns no request and no client authority:
294-// the target is the one provisioned entry whose route is exactly the carriage
295-// being bridged, so the helper can only ever narrow what its own configuration
296-// already permits.
297-//
298-// The provisioned local binding wins when it names the carriage, because the
299-// local binding is the broker's own daemon authority. Otherwise the single
300-// registration that provisions this carriage supplies the fence, identity, and
301-// policy. A configuration that provisions neither yields no target, and a
302-// start-if-needed request is then refused rather than widened.
303-func helperCarriageTarget(config *brokerconfig.Config, route brokerconfig.Route) (ports.BrokerDialTarget, error) {
304- if binding, ok := config.LocalBinding(); ok && binding.Route.Address() == route.Address() {
305- return ports.BrokerDialTarget{
306- Fence: ports.BrokerEndpointFence{Local: true},
307- Address: binding.Route.Address(),
308- Policy: binding.Policy,
309- ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: binding.Identity, Bound: binding.Identity != ""},
310- }, nil
311- }
312- for _, endpoint := range config.Endpoints() {
313- registration, ok := config.Registration(endpoint)
314- if !ok || registration.Route.Address() != route.Address() {
315- continue
316- }
317- return ports.BrokerDialTarget{
318- Fence: ports.BrokerEndpointFence{Registration: registration.Registration},
319- Address: registration.Route.Address(),
320- Policy: registration.Policy,
321- ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: registration.Identity, Bound: registration.Identity != ""},
322- }, nil
323- }
324- return ports.BrokerDialTarget{}, localDaemonStartRefused("the helper configuration provisions no daemon authority for its mux carriage")
325-}
326-
327-// dialMuxHelperCarriage dials the private daemonmux carriage one remote-side mux
328-// helper bridges to, under the start authorization the broker propagated. The
329-// helper applies the same local election the broker does, from its own
330-// broker-owned configuration, and it never starts a broker, an observer, or a
331-// recursive helper: an existing-only mode is one carriage dial, and a
332-// start-if-needed mode is the shared daemon start gated by the helper's own
333-// provisioned policy and identity.
334-func dialMuxHelperCarriage(ctx context.Context, config *brokerconfig.Config, mode ports.BrokerDaemonStartMode) (daemonmux.RawFramedTransport, error) {
335- if config == nil {
336- return nil, errors.New("vev: broker mux helper requires a configuration")
337- }
338- if err := mode.Validate(); err != nil {
339- return nil, err
340- }
341- route, err := config.LocalMuxRoute()
342- if err != nil {
343- return nil, err
344- }
345- target, err := helperCarriageTarget(config, route)
346- if err != nil {
347- return nil, err
348- }
349- target.StartMode = mode
350- if err := target.Validate(); err != nil {
351- return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: err}
352- }
353- // Only a policy that names the launch token may start the daemon. A carriage
354- // provisioned under a non-launching policy refuses the start outright. The
355- // existing-only mode never reaches this check: dialBrokerDaemonCarriage
356- // returns its single dial's result immediately.
357- if mode == ports.BrokerDaemonStartIfNeeded {
358- if err := daemonStartRefusal(target); err != nil {
359- return nil, err
360- }
361- }
362- return dialBrokerDaemonCarriage(ctx, route.Path(), target)
363-}
M internal/app/broker_daemon_start_test.go +3 -211
@@ -4,7 +4,6 @@ import (
44 "context"
55 "errors"
66 "io"
7- "log/slog"
87 "os"
98 "path/filepath"
109 "sync"
@@ -214,30 +213,6 @@ func startableLocalTarget(t *testing.T, carriage string, mode ports.BrokerDaemon
214213 return target
215214 }
216215
217-// writePrivateSandboxConfig writes one marker-valid sandbox whose single local
218-// binding is the supplied private carriage under the supplied policy.
219-func writePrivateSandboxConfig(t *testing.T, root, carriage string, policy ports.BrokerPolicy) {
220- t.Helper()
221- writeSandboxConfig(t, root, map[string]any{
222- "marker": brokerconfig.Marker,
223- "registrations": []any{},
224- "local": map[string]any{
225- "identity": brokerLocalTestIdentity,
226- "displayOrigin": "local",
227- "route": carriage,
228- "policy": map[string]any{
229- "protocolVersion": policy.ProtocolVersion,
230- "catalogSchemaVersion": policy.CatalogSchemaVersion,
231- "environmentPolicy": "client-owned",
232- "transport": policy.Transport,
233- "trust": policy.Trust,
234- "launch": policy.Launch,
235- "isolation": policy.Isolation,
236- },
237- },
238- })
239-}
240-
241216 // TestDaemonStartExistingOnlyNeverSpawns proves an existing-only acquisition is
242217 // exactly one carriage dial: no lifecycle probe, no spawn lock, no process, and
243218 // no fallback of any kind, even though the carriage is absent.
@@ -635,105 +610,6 @@ func TestDaemonStartRefusesInvalidTarget(t *testing.T) {
635610 require.Zero(t, spawns)
636611 }
637612
638-// helperSandboxConfigForCarriage provisions one sandbox whose single
639-// registration bridges the supplied private Unix carriage under the supplied
640-// policy, and returns its loaded configuration. A remote-side mux helper owns
641-// exactly this shape: the provisioning is the helper's own authority.
642-func helperSandboxConfigForCarriage(t *testing.T, carriage string, policy ports.BrokerPolicy) *brokerconfig.Config {
643- t.Helper()
644- root := filepath.Dir(carriage)
645- writeSandboxConfig(t, root, map[string]any{
646- "marker": brokerconfig.Marker,
647- "registrations": []any{map[string]any{
648- "endpoint": brokerTestEndpoint,
649- "incarnation": "0102030405060708090a0b0c0d0e0f10",
650- "generation": 1,
651- "identity": brokerTestIdentity,
652- "route": carriage,
653- "policy": map[string]any{
654- "protocolVersion": policy.ProtocolVersion,
655- "catalogSchemaVersion": policy.CatalogSchemaVersion,
656- "environmentPolicy": "client-owned",
657- "transport": policy.Transport,
658- "trust": policy.Trust,
659- "launch": policy.Launch,
660- "isolation": policy.Isolation,
661- },
662- }},
663- })
664- config, err := brokerconfig.Load(mustOfflineLayout(t, root))
665- require.NoError(t, err)
666- return config
667-}
668-
669-// TestMuxHelperCarriageStartAuthorization proves the remote-side helper applies
670-// the propagated authorization against its own provisioned authority: an
671-// existing-only request is one carriage dial, a start-if-needed request is gated
672-// by the helper's own policy, and an unknown mode is refused outright.
673-func TestMuxHelperCarriageStartAuthorization(t *testing.T) {
674- isolateSandboxEnv(t)
675- dir, carriage := testDaemonCarriage(t)
676- config := helperSandboxConfigForCarriage(t, carriage, brokerTestPolicy())
677-
678- scripted := &scriptedDaemonStarter{carriage: carriage}
679- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
680-
681- _, err := dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonExistingOnly)
682- require.ErrorIs(t, err, os.ErrNotExist)
683- dials, spawns := scripted.counters()
684- require.Equal(t, 1, dials, "existing-only is exactly one carriage dial")
685- require.Zero(t, spawns)
686-
687- // The provisioned policy does not name the launch authority, so a
688- // start-if-needed request is refused before any dial and never spawns: the
689- // helper's own authority is static, so the refusal is decided up front.
690- _, err = dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonStartIfNeeded)
691- require.Error(t, err)
692- var typed ports.BrokerError
693- require.ErrorAs(t, err, &typed)
694- require.Equal(t, ports.BrokerErrorConflictingPolicy, typed.Code)
695- dials, spawns = scripted.counters()
696- require.Equal(t, 1, dials, "a policy refusal is decided before any dial")
697- require.Zero(t, spawns, "a non-launching helper policy must never spawn")
698- require.NoFileExists(t, filepath.Join(dir, spawnLockName))
699-
700- // An unknown mode and a missing configuration are never defaulted.
701- _, err = dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonStartMode(9))
702- require.Error(t, err)
703- _, err = dialMuxHelperCarriage(context.Background(), nil, ports.BrokerDaemonExistingOnly)
704- require.Error(t, err)
705- dials, spawns = scripted.counters()
706- require.Equal(t, 1, dials, "a refused mode never reaches a dial")
707- require.Zero(t, spawns)
708-}
709-
710-// TestMuxHelperCarriageStartsUnderItsOwnLaunchAuthority proves the helper starts
711-// the daemon only when its own provisioned policy authorizes launching, and that
712-// it starts the daemon published on its own machine rather than another broker.
713-func TestMuxHelperCarriageStartsUnderItsOwnLaunchAuthority(t *testing.T) {
714- isolateSandboxEnv(t)
715- _, carriage := testDaemonCarriage(t)
716- config := helperSandboxConfigForCarriage(t, carriage, brokerTestPolicyForLaunch())
717-
718- scripted := &scriptedDaemonStarter{carriage: carriage}
719- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
720-
721- raw, err := dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonStartIfNeeded)
722- require.NoError(t, err)
723- require.NotNil(t, raw)
724- _, spawns := scripted.counters()
725- require.Equal(t, 1, spawns)
726-}
727-
728-// brokerTestPolicyForLaunch is the sandbox policy with the launch authority the
729-// broker's own production local daemon names, so a helper test can exercise the
730-// authorized start path without provisioning production layout.
731-func brokerTestPolicyForLaunch() ports.BrokerPolicy {
732- policy := brokerTestPolicy()
733- policy.Launch = brokerLaunchAuthority
734- return policy
735-}
736-
737613 // TestDaemonStartArgvFlagRoundTrips proves the helper argument spelling is one
738614 // closed value both sides agree on, and that an unknown value is refused in
739615 // either direction.
@@ -761,97 +637,13 @@ func TestDaemonStartArgvFlagRoundTrips(t *testing.T) {
761637 }
762638 }
763639
764-// testLocalSandboxConfigForCarriage provisions one sandbox whose single local
765-// binding is the supplied private carriage, and returns its loaded
766-// configuration and binding.
640+// testLocalSandboxConfigForCarriage derives local authority from production state.
767641 func testLocalSandboxConfigForCarriage(t *testing.T, carriage string) (*brokerconfig.Config, brokerconfig.LocalBinding) {
768642 t.Helper()
769- root := filepath.Dir(carriage)
770- writePrivateSandboxConfig(t, root, carriage, brokerTestPolicy())
771- layout := mustOfflineLayout(t, root)
772- config, err := brokerconfig.Load(layout)
643+ layout := emptyProductionBrokerLayout(t, "")
644+ config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), brokerLocalTestIdentity, brokerTestPolicy(), carriage)
773645 require.NoError(t, err)
774646 binding, ok := config.LocalBinding()
775647 require.True(t, ok)
776648 return config, binding
777649 }
778-
779-func mustOfflineLayout(t *testing.T, root string) brokerconfig.Layout {
780- t.Helper()
781- layout, err := offlineLayout(root)
782- require.NoError(t, err)
783- return layout
784-}
785-
786-func mustLocalMuxAddress(t *testing.T, config *brokerconfig.Config) string {
787- t.Helper()
788- route, err := config.LocalMuxRoute()
789- require.NoError(t, err)
790- return route.Address()
791-}
792-
793-// TestBrokerMuxConnectorObservationNeverSpawns proves the composed broker
794-// connector dials the broker-owned local carriage for an observation under the
795-// existing-only authorization and spawns nothing, so observing a stopped local
796-// daemon stays a reported failure rather than a start.
797-func TestBrokerMuxConnectorObservationNeverSpawns(t *testing.T) {
798- isolateSandboxEnv(t)
799- _, carriage := testDaemonCarriage(t)
800- config, binding := testLocalSandboxConfigForCarriage(t, carriage)
801-
802- scripted := &scriptedDaemonStarter{carriage: carriage}
803- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
804-
805- connector, err := brokerMuxConnector(config, slog.New(slog.NewTextHandler(io.Discard, nil)))
806- require.NoError(t, err)
807- target, err := config.Resolver().ResolveDialTarget(context.Background(), ports.BrokerOpenStreamRequest{
808- Purpose: ports.BrokerStreamObservation,
809- Local: true,
810- Stream: 1,
811- Policy: binding.Policy,
812- StartMode: ports.BrokerDaemonExistingOnly,
813- })
814- require.NoError(t, err)
815-
816- _, err = connector.Connect(context.Background(), target)
817- require.ErrorIs(t, err, os.ErrNotExist)
818- dials, spawns := scripted.counters()
819- require.Equal(t, 1, dials, "the observation dials its local carriage once")
820- require.Zero(t, spawns, "an observation must never start a daemon")
821-}
822-
823-// TestBrokerMuxConnectorRefusesMismatchedAuthority proves the transport never
824-// dials under an authority the configuration did not provision: a target whose
825-// policy or address no longer matches the route is refused before any dial.
826-func TestBrokerMuxConnectorRefusesMismatchedAuthority(t *testing.T) {
827- isolateSandboxEnv(t)
828- _, carriage := testDaemonCarriage(t)
829- config, _ := testLocalSandboxConfigForCarriage(t, carriage)
830-
831- scripted := &scriptedDaemonStarter{carriage: carriage}
832- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
833-
834- connector, err := brokerMuxConnector(config, slog.New(slog.NewTextHandler(io.Discard, nil)))
835- require.NoError(t, err)
836-
837- target := startableLocalTarget(t, carriage, ports.BrokerDaemonExistingOnly)
838- // The address is provisioned, but the policy is not.
839- target.Policy.Launch = "other-launch"
840- require.NoError(t, target.Validate())
841- _, err = connector.Connect(context.Background(), target)
842- require.Error(t, err)
843- var typed ports.BrokerError
844- require.ErrorAs(t, err, &typed)
845- require.Equal(t, ports.BrokerErrorConflictingPolicy, typed.Code)
846-
847- // An address the configuration never produced is refused too.
848- unknown := target
849- unknown.Address = "0000000000000000000000000000000000000000000000000000000000000000"
850- require.NoError(t, unknown.Validate())
851- _, err = connector.Connect(context.Background(), unknown)
852- require.Error(t, err)
853-
854- dials, spawns := scripted.counters()
855- require.Zero(t, dials, "an unmatched authority must never reach a dial")
856- require.Zero(t, spawns)
857-}
@@ -4,7 +4,6 @@ import (
4 "context"4 "context"
5 "errors"5 "errors"
6 "io"6 "io"
7- "log/slog"
8 "os"7 "os"
9 "path/filepath"8 "path/filepath"
10 "sync"9 "sync"
@@ -214,30 +213,6 @@ func startableLocalTarget(t *testing.T, carriage string, mode ports.BrokerDaemon
214 return target213 return target
215 }214 }
216 215
217-// writePrivateSandboxConfig writes one marker-valid sandbox whose single local
218-// binding is the supplied private carriage under the supplied policy.
219-func writePrivateSandboxConfig(t *testing.T, root, carriage string, policy ports.BrokerPolicy) {
220- t.Helper()
221- writeSandboxConfig(t, root, map[string]any{
222- "marker": brokerconfig.Marker,
223- "registrations": []any{},
224- "local": map[string]any{
225- "identity": brokerLocalTestIdentity,
226- "displayOrigin": "local",
227- "route": carriage,
228- "policy": map[string]any{
229- "protocolVersion": policy.ProtocolVersion,
230- "catalogSchemaVersion": policy.CatalogSchemaVersion,
231- "environmentPolicy": "client-owned",
232- "transport": policy.Transport,
233- "trust": policy.Trust,
234- "launch": policy.Launch,
235- "isolation": policy.Isolation,
236- },
237- },
238- })
239-}
240-
241 // TestDaemonStartExistingOnlyNeverSpawns proves an existing-only acquisition is216 // TestDaemonStartExistingOnlyNeverSpawns proves an existing-only acquisition is
242 // exactly one carriage dial: no lifecycle probe, no spawn lock, no process, and217 // exactly one carriage dial: no lifecycle probe, no spawn lock, no process, and
243 // no fallback of any kind, even though the carriage is absent.218 // no fallback of any kind, even though the carriage is absent.
@@ -635,105 +610,6 @@ func TestDaemonStartRefusesInvalidTarget(t *testing.T) {
635 require.Zero(t, spawns)610 require.Zero(t, spawns)
636 }611 }
637 612
638-// helperSandboxConfigForCarriage provisions one sandbox whose single
639-// registration bridges the supplied private Unix carriage under the supplied
640-// policy, and returns its loaded configuration. A remote-side mux helper owns
641-// exactly this shape: the provisioning is the helper's own authority.
642-func helperSandboxConfigForCarriage(t *testing.T, carriage string, policy ports.BrokerPolicy) *brokerconfig.Config {
643- t.Helper()
644- root := filepath.Dir(carriage)
645- writeSandboxConfig(t, root, map[string]any{
646- "marker": brokerconfig.Marker,
647- "registrations": []any{map[string]any{
648- "endpoint": brokerTestEndpoint,
649- "incarnation": "0102030405060708090a0b0c0d0e0f10",
650- "generation": 1,
651- "identity": brokerTestIdentity,
652- "route": carriage,
653- "policy": map[string]any{
654- "protocolVersion": policy.ProtocolVersion,
655- "catalogSchemaVersion": policy.CatalogSchemaVersion,
656- "environmentPolicy": "client-owned",
657- "transport": policy.Transport,
658- "trust": policy.Trust,
659- "launch": policy.Launch,
660- "isolation": policy.Isolation,
661- },
662- }},
663- })
664- config, err := brokerconfig.Load(mustOfflineLayout(t, root))
665- require.NoError(t, err)
666- return config
667-}
668-
669-// TestMuxHelperCarriageStartAuthorization proves the remote-side helper applies
670-// the propagated authorization against its own provisioned authority: an
671-// existing-only request is one carriage dial, a start-if-needed request is gated
672-// by the helper's own policy, and an unknown mode is refused outright.
673-func TestMuxHelperCarriageStartAuthorization(t *testing.T) {
674- isolateSandboxEnv(t)
675- dir, carriage := testDaemonCarriage(t)
676- config := helperSandboxConfigForCarriage(t, carriage, brokerTestPolicy())
677-
678- scripted := &scriptedDaemonStarter{carriage: carriage}
679- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
680-
681- _, err := dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonExistingOnly)
682- require.ErrorIs(t, err, os.ErrNotExist)
683- dials, spawns := scripted.counters()
684- require.Equal(t, 1, dials, "existing-only is exactly one carriage dial")
685- require.Zero(t, spawns)
686-
687- // The provisioned policy does not name the launch authority, so a
688- // start-if-needed request is refused before any dial and never spawns: the
689- // helper's own authority is static, so the refusal is decided up front.
690- _, err = dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonStartIfNeeded)
691- require.Error(t, err)
692- var typed ports.BrokerError
693- require.ErrorAs(t, err, &typed)
694- require.Equal(t, ports.BrokerErrorConflictingPolicy, typed.Code)
695- dials, spawns = scripted.counters()
696- require.Equal(t, 1, dials, "a policy refusal is decided before any dial")
697- require.Zero(t, spawns, "a non-launching helper policy must never spawn")
698- require.NoFileExists(t, filepath.Join(dir, spawnLockName))
699-
700- // An unknown mode and a missing configuration are never defaulted.
701- _, err = dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonStartMode(9))
702- require.Error(t, err)
703- _, err = dialMuxHelperCarriage(context.Background(), nil, ports.BrokerDaemonExistingOnly)
704- require.Error(t, err)
705- dials, spawns = scripted.counters()
706- require.Equal(t, 1, dials, "a refused mode never reaches a dial")
707- require.Zero(t, spawns)
708-}
709-
710-// TestMuxHelperCarriageStartsUnderItsOwnLaunchAuthority proves the helper starts
711-// the daemon only when its own provisioned policy authorizes launching, and that
712-// it starts the daemon published on its own machine rather than another broker.
713-func TestMuxHelperCarriageStartsUnderItsOwnLaunchAuthority(t *testing.T) {
714- isolateSandboxEnv(t)
715- _, carriage := testDaemonCarriage(t)
716- config := helperSandboxConfigForCarriage(t, carriage, brokerTestPolicyForLaunch())
717-
718- scripted := &scriptedDaemonStarter{carriage: carriage}
719- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
720-
721- raw, err := dialMuxHelperCarriage(context.Background(), config, ports.BrokerDaemonStartIfNeeded)
722- require.NoError(t, err)
723- require.NotNil(t, raw)
724- _, spawns := scripted.counters()
725- require.Equal(t, 1, spawns)
726-}
727-
728-// brokerTestPolicyForLaunch is the sandbox policy with the launch authority the
729-// broker's own production local daemon names, so a helper test can exercise the
730-// authorized start path without provisioning production layout.
731-func brokerTestPolicyForLaunch() ports.BrokerPolicy {
732- policy := brokerTestPolicy()
733- policy.Launch = brokerLaunchAuthority
734- return policy
735-}
736-
737 // TestDaemonStartArgvFlagRoundTrips proves the helper argument spelling is one613 // TestDaemonStartArgvFlagRoundTrips proves the helper argument spelling is one
738 // closed value both sides agree on, and that an unknown value is refused in614 // closed value both sides agree on, and that an unknown value is refused in
739 // either direction.615 // either direction.
@@ -761,97 +637,13 @@ func TestDaemonStartArgvFlagRoundTrips(t *testing.T) {
761 }637 }
762 }638 }
763 639
764-// testLocalSandboxConfigForCarriage provisions one sandbox whose single local640+// testLocalSandboxConfigForCarriage derives local authority from production state.
765-// binding is the supplied private carriage, and returns its loaded
766-// configuration and binding.
767 func testLocalSandboxConfigForCarriage(t *testing.T, carriage string) (*brokerconfig.Config, brokerconfig.LocalBinding) {641 func testLocalSandboxConfigForCarriage(t *testing.T, carriage string) (*brokerconfig.Config, brokerconfig.LocalBinding) {
768 t.Helper()642 t.Helper()
769- root := filepath.Dir(carriage)643+ layout := emptyProductionBrokerLayout(t, "")
770- writePrivateSandboxConfig(t, root, carriage, brokerTestPolicy())644+ config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), brokerLocalTestIdentity, brokerTestPolicy(), carriage)
771- layout := mustOfflineLayout(t, root)
772- config, err := brokerconfig.Load(layout)
773 require.NoError(t, err)645 require.NoError(t, err)
774 binding, ok := config.LocalBinding()646 binding, ok := config.LocalBinding()
775 require.True(t, ok)647 require.True(t, ok)
776 return config, binding648 return config, binding
777 }649 }
778-
779-func mustOfflineLayout(t *testing.T, root string) brokerconfig.Layout {
780- t.Helper()
781- layout, err := offlineLayout(root)
782- require.NoError(t, err)
783- return layout
784-}
785-
786-func mustLocalMuxAddress(t *testing.T, config *brokerconfig.Config) string {
787- t.Helper()
788- route, err := config.LocalMuxRoute()
789- require.NoError(t, err)
790- return route.Address()
791-}
792-
793-// TestBrokerMuxConnectorObservationNeverSpawns proves the composed broker
794-// connector dials the broker-owned local carriage for an observation under the
795-// existing-only authorization and spawns nothing, so observing a stopped local
796-// daemon stays a reported failure rather than a start.
797-func TestBrokerMuxConnectorObservationNeverSpawns(t *testing.T) {
798- isolateSandboxEnv(t)
799- _, carriage := testDaemonCarriage(t)
800- config, binding := testLocalSandboxConfigForCarriage(t, carriage)
801-
802- scripted := &scriptedDaemonStarter{carriage: carriage}
803- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
804-
805- connector, err := brokerMuxConnector(config, slog.New(slog.NewTextHandler(io.Discard, nil)))
806- require.NoError(t, err)
807- target, err := config.Resolver().ResolveDialTarget(context.Background(), ports.BrokerOpenStreamRequest{
808- Purpose: ports.BrokerStreamObservation,
809- Local: true,
810- Stream: 1,
811- Policy: binding.Policy,
812- StartMode: ports.BrokerDaemonExistingOnly,
813- })
814- require.NoError(t, err)
815-
816- _, err = connector.Connect(context.Background(), target)
817- require.ErrorIs(t, err, os.ErrNotExist)
818- dials, spawns := scripted.counters()
819- require.Equal(t, 1, dials, "the observation dials its local carriage once")
820- require.Zero(t, spawns, "an observation must never start a daemon")
821-}
822-
823-// TestBrokerMuxConnectorRefusesMismatchedAuthority proves the transport never
824-// dials under an authority the configuration did not provision: a target whose
825-// policy or address no longer matches the route is refused before any dial.
826-func TestBrokerMuxConnectorRefusesMismatchedAuthority(t *testing.T) {
827- isolateSandboxEnv(t)
828- _, carriage := testDaemonCarriage(t)
829- config, _ := testLocalSandboxConfigForCarriage(t, carriage)
830-
831- scripted := &scriptedDaemonStarter{carriage: carriage}
832- withDaemonStarter(t, scripted.starter(fastDaemonStartBackoff))
833-
834- connector, err := brokerMuxConnector(config, slog.New(slog.NewTextHandler(io.Discard, nil)))
835- require.NoError(t, err)
836-
837- target := startableLocalTarget(t, carriage, ports.BrokerDaemonExistingOnly)
838- // The address is provisioned, but the policy is not.
839- target.Policy.Launch = "other-launch"
840- require.NoError(t, target.Validate())
841- _, err = connector.Connect(context.Background(), target)
842- require.Error(t, err)
843- var typed ports.BrokerError
844- require.ErrorAs(t, err, &typed)
845- require.Equal(t, ports.BrokerErrorConflictingPolicy, typed.Code)
846-
847- // An address the configuration never produced is refused too.
848- unknown := target
849- unknown.Address = "0000000000000000000000000000000000000000000000000000000000000000"
850- require.NoError(t, unknown.Validate())
851- _, err = connector.Connect(context.Background(), unknown)
852- require.Error(t, err)
853-
854- dials, spawns := scripted.counters()
855- require.Zero(t, dials, "an unmatched authority must never reach a dial")
856- require.Zero(t, spawns)
857-}
M internal/app/broker_offline.go +51 -162
@@ -10,8 +10,6 @@ import (
1010 "log/slog"
1111 "os"
1212 "os/signal"
13- "path/filepath"
14- "strings"
1513 "syscall"
1614 "time"
1715
@@ -46,10 +44,7 @@ import (
4644 // until a signal or its parent context ends it, or until the broker idles out,
4745 // and then shuts down in a fixed order.
4846
49-const (
50- brokerServeCommand = "_broker-serve"
51- productionBrokerServeCommand = "_broker-production-serve"
52-)
47+const productionBrokerServeCommand = "_broker-production-serve"
5348
5449 // brokerServePoolLimits bound the offline broker pool. They are explicit, not
5550 // derived from the environment, and small enough that a stalled peer is
@@ -73,70 +68,17 @@ func brokerPoolLimits(config *brokerconfig.Config) broker.PoolLimits {
7368
7469 // brokerServeOptions is the parsed hidden invocation.
7570 type brokerServeOptions struct {
76- offlineRoot string
77- production bool
7871 // idleGrace is zero when the operator did not override it; the supervisor
7972 // then applies its own 5m default.
8073 idleGrace time.Duration
8174 }
8275
83-// parseBrokerServeArgs strictly parses `_broker-serve --offline-root ABS
84-// [--idle-grace DURATION]`. Unknown flags, duplicate flags, positionals, a
85-// missing value, and a non-positive or unparsable duration are refused; the
86-// path itself is validated later against the filesystem.
76+// parseProductionBrokerServeArgs parses the argument-free hidden serve entry.
8777 func parseProductionBrokerServeArgs(args []string) (command, error) {
8878 if len(args) != 0 {
8979 return command{}, usagef("`%s` does not accept arguments", productionBrokerServeCommand)
9080 }
91- return command{kind: kindProductionBrokerServe, brokerServe: brokerServeOptions{production: true}}, nil
92-}
93-
94-func parseBrokerServeArgs(args []string) (command, error) {
95- if len(args) == 0 {
96- return command{}, usagef("`%s` requires --offline-root", brokerServeCommand)
97- }
98- options := brokerServeOptions{}
99- var rootSet, graceSet bool
100- for index := 0; index < len(args); {
101- switch args[index] {
102- case "--offline-root":
103- if rootSet {
104- return command{}, usagef("`%s` received duplicate --offline-root", brokerServeCommand)
105- }
106- if index+1 >= len(args) || args[index+1] == "" {
107- return command{}, usagef("`--offline-root` requires a path")
108- }
109- options.offlineRoot = args[index+1]
110- rootSet = true
111- index += 2
112- case "--idle-grace":
113- if graceSet {
114- return command{}, usagef("`%s` received duplicate --idle-grace", brokerServeCommand)
115- }
116- if index+1 >= len(args) {
117- return command{}, usagef("`--idle-grace` requires a duration")
118- }
119- grace, err := time.ParseDuration(args[index+1])
120- if err != nil {
121- return command{}, usagef("`--idle-grace` %q is not a duration", args[index+1])
122- }
123- if grace <= 0 {
124- return command{}, usagef("`--idle-grace` must be positive")
125- }
126- options.idleGrace = grace
127- graceSet = true
128- index += 2
129- default:
130- if strings.HasPrefix(args[index], "-") {
131- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerServeCommand)
132- }
133- return command{}, usagef("`%s` does not accept positional arguments", brokerServeCommand)
134- }
135- }
136- if !rootSet {
137- return command{}, usagef("`%s` requires --offline-root", brokerServeCommand)
138- }
139- return command{kind: kindBrokerServe, brokerServe: options}, nil
81+ return command{kind: kindProductionBrokerServe, brokerServe: brokerServeOptions{}}, nil
14082 }
14183
14284 // brokerServeDeps are the composition seams. Production supplies the real
@@ -194,16 +136,12 @@ func defaultBrokerServeDeps() brokerServeDeps {
194136 func runBrokerServeCommand(ctx context.Context, options brokerServeOptions) (retErr error) {
195137 ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
196138 defer stop()
197- var observerCloser io.Closer
198- if options.production {
199- observer, closer, err := newPerformanceTrace(clock.New())
200- if err != nil {
201- return fmt.Errorf("vev: performance trace: %w", err)
202- }
203- observerCloser = closer
204- if observer != nil {
205- observer.ObserveRuntime(ports.NewRuntimeMark("broker", ports.RuntimeTransportDiagnostic, 0, true))
206- }
139+ observer, observerCloser, err := newPerformanceTrace(clock.New())
140+ if err != nil {
141+ return fmt.Errorf("vev: performance trace: %w", err)
142+ }
143+ if observer != nil {
144+ observer.ObserveRuntime(ports.NewRuntimeMark("broker", ports.RuntimeTransportDiagnostic, 0, true))
207145 }
208146 if observerCloser != nil {
209147 defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }()
@@ -235,12 +173,6 @@ func newBrokerEpoch() (ports.BrokerEpoch, error) {
235173 return epoch, nil
236174 }
237175
238-// offlineLayout resolves the hidden offline sandbox layout for one operator
239-// root, refusing any root that overlaps the production runtime or state paths.
240-func offlineLayout(offlineRoot string) (brokerconfig.Layout, error) {
241- return brokerconfig.ResolveLayout(offlineRoot, []string{ipc.SocketDir(), platform.StateDir()})
242-}
243-
244176 // effectiveIdleGrace resolves the sandbox idle grace from an explicit flag and
245177 // the provisioned configuration. The provisioned value is the single
246178 // deterministic source: a status probe cannot read a running broker's effective
@@ -274,50 +206,32 @@ func effectiveIdleGrace(config *brokerconfig.Config, explicit time.Duration) (ti
274206 func runBrokerServe(ctx context.Context, options brokerServeOptions, deps brokerServeDeps) (retErr error) {
275207 ctx, cancel := context.WithCancel(ctx)
276208 defer cancel()
277- var layout brokerconfig.Layout
278- var configPath string
279- var err error
280- if options.production {
281- if err := ensureProductionBrokerConfig(); err != nil {
282- return err
283- }
284- if err := safedir.EnsurePrivate(ipc.SocketDir()); err != nil {
285- return fmt.Errorf("vev: secure broker runtime parent: %w", err)
286- }
287- layout = productionBrokerLayout()
288- configPath = productionBrokerConfigPath()
289- } else {
290- layout, err = offlineLayout(options.offlineRoot)
291- if err != nil {
292- return err
293- }
294- configPath = filepath.Join(layout.Root, brokerconfig.ConfigFileName)
209+ if err := ensureProductionBrokerConfig(); err != nil {
210+ return err
295211 }
212+ if err := safedir.EnsurePrivate(ipc.SocketDir()); err != nil {
213+ return fmt.Errorf("vev: secure broker runtime parent: %w", err)
214+ }
215+ layout := productionBrokerLayout()
216+ configPath := productionBrokerConfigPath()
296217 for _, dir := range []string{layout.Root, layout.Runtime, layout.State, layout.Log} {
297218 if err := safedir.EnsurePrivate(dir); err != nil {
298219 return fmt.Errorf("vev: secure broker sandbox directory %s: %w", dir, err)
299220 }
300221 }
301- // ResolveLayout validates the root textually and stops at the first
302- // component that does not exist yet; now that EnsurePrivate has created the
303- // sandbox paths, re-walk every component so a component swapped for a
304- // symlink in that window fails closed instead of being followed.
222+ // Re-walk every component now that EnsurePrivate created them, so a
223+ // component swapped for a symlink in that window fails closed.
305224 if err := layout.VerifyCreated(); err != nil {
306- return fmt.Errorf("vev: verify broker sandbox paths: %w", err)
225+ return fmt.Errorf("vev: verify broker paths: %w", err)
307226 }
308- var config *brokerconfig.Config
309- if options.production {
310- // The broker is allowed to precede the daemon. Identity is therefore an
311- // optional existing authority here; acquisition binds it after an
312- // authenticated StartIfNeeded connection when the daemon is first born.
313- identity, loadErr := daemonidentity.Load(platform.StateDir())
314- if loadErr != nil && !errors.Is(loadErr, os.ErrNotExist) {
315- return fmt.Errorf("vev: load daemon authority: %w", loadErr)
316- }
317- config, err = brokerconfig.LoadProduction(layout, configPath, identity, localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
318- } else {
319- config, err = brokerconfig.LoadPath(layout, configPath)
227+ // The broker is allowed to precede the daemon. Identity is therefore an
228+ // optional existing authority here; acquisition binds it after an
229+ // authenticated StartIfNeeded connection when the daemon is first born.
230+ identity, err := daemonidentity.Load(platform.StateDir())
231+ if err != nil && !errors.Is(err, os.ErrNotExist) {
232+ return fmt.Errorf("vev: load daemon authority: %w", err)
320233 }
234+ config, err := brokerconfig.LoadProduction(layout, configPath, identity, localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
321235 if err != nil {
322236 return err
323237 }
@@ -337,15 +251,13 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
337251 }
338252 defer func() { retErr = errors.Join(retErr, logCloser.Close()) }()
339253
340- if options.production {
341- // The durable route schema intentionally delegates SSH trust to OpenSSH.
342- // Refuse route overrides that OpenSSH configuration must represent instead of dropping
343- // security inputs during production import.
344- for _, endpoint := range config.Endpoints() {
345- registration, _ := config.Registration(endpoint)
346- if registration.Route.KnownHostsFile() != "" || registration.Route.ConnectTimeout() != 0 {
347- return errors.New("vev: production broker routes require SSH trust and timeouts in OpenSSH configuration")
348- }
254+ // The durable route schema delegates SSH trust to OpenSSH. Refuse route
255+ // overrides that OpenSSH configuration must represent instead of dropping
256+ // security inputs during import.
257+ for _, endpoint := range config.Endpoints() {
258+ registration, _ := config.Registration(endpoint)
259+ if registration.Route.KnownHostsFile() != "" || registration.Route.ConnectTimeout() != 0 {
260+ return errors.New("vev: broker routes require SSH trust and timeouts in OpenSSH configuration")
349261 }
350262 }
351263 initialHosts, err := configuredBrokerHosts(config)
@@ -388,53 +300,32 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
388300 }
389301 defer startupLease.Release()
390302
391- // The sandbox retains its explicit route/trust fixture contract. Production
392- // imports membership once, then uses only durable remote authority.
393- dynamicRoutes := &brokerRoutes{local: config.Resolver()}
394- var resolver ports.BrokerRouteAuthority = config.Resolver()
395- connector, err := brokerMuxConnector(config, log)
396- if options.production {
397- resolver = dynamicRoutes
398- connector, err = brokerDynamicMuxConnector(config, dynamicRoutes, log)
399- }
303+ // Membership is imported once, then only durable remote authority is used.
304+ resolver := &brokerRoutes{local: config.Resolver()}
305+ connector, err := brokerDynamicMuxConnector(config, resolver, log)
400306 if err != nil {
401307 return err
402308 }
403- var localIdentityLoaders []localIdentityLoader
404- if options.production {
405- localIdentityLoaders = append(localIdentityLoaders, func() (ports.BrokerDaemonIdentity, error) {
406- identity, loadErr := daemonidentity.Load(platform.StateDir())
407- if errors.Is(loadErr, os.ErrNotExist) {
408- return "", nil
409- }
410- return identity, loadErr
411- })
309+ loadLocalIdentity := func() (ports.BrokerDaemonIdentity, error) {
310+ identity, loadErr := daemonidentity.Load(platform.StateDir())
311+ if errors.Is(loadErr, os.ErrNotExist) {
312+ return "", nil
313+ }
314+ return identity, loadErr
412315 }
413- registryConfig, err := offlineRegistryConfig(config, epoch, connector, localIdentityLoaders...)
316+ registryConfig, err := offlineRegistryConfig(config, epoch, connector, loadLocalIdentity)
414317 if err != nil {
415318 return err
416319 }
417- var remoteProbe ports.BrokerHostProbe
418- var concreteRemoteProbe *brokerRemoteProbe
419- if options.production || len(config.Endpoints()) != 0 {
420- concreteRemoteProbe = &brokerRemoteProbe{
421- epoch: epoch, routes: resolver, connector: connector,
422- }
423- remoteProbe = concreteRemoteProbe
424- }
320+ remoteProbe := &brokerRemoteProbe{epoch: epoch, routes: resolver, connector: connector}
425321 registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig)
426322 if err != nil {
427323 return err
428324 }
429- dynamicRoutes.hosts = registry
430- binder := ports.BrokerIdentityBinder(registry)
431- if options.production {
432- binder = productionIdentityBinder{delegate: registry, stateDir: platform.StateDir(), policy: localDaemonPolicy()}
433- }
434- if concreteRemoteProbe != nil {
435- concreteRemoteProbe.binder = binder
436- concreteRemoteProbe.hosts = registry
437- }
325+ resolver.hosts = registry
326+ binder := productionIdentityBinder{delegate: registry, stateDir: platform.StateDir(), policy: localDaemonPolicy()}
327+ remoteProbe.binder = binder
328+ remoteProbe.hosts = registry
438329 // RegisterRunner starts immediately: finish the cyclic composition before
439330 // publishing it to any goroutine. Shutdown remains listener -> pool -> registry.
440331 if err := supervisor.RegisterRunner("registry", registry); err != nil {
@@ -445,9 +336,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
445336 return err
446337 }
447338 // Observation borrows pooled transports rather than dialing its own.
448- if concreteRemoteProbe != nil {
449- concreteRemoteProbe.shared.share(pool)
450- }
339+ remoteProbe.shared.share(pool)
451340 if registryConfig.Local != nil {
452341 if local, ok := registryConfig.Local.Probe.(*localRouteProbe); ok {
453342 local.shared.share(pool)
@@ -476,7 +365,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
476365 go drainBrokerAccept(listener, log, started, drained, failed)
477366 <-started
478367 startupLease.Release()
479- log.Info("broker_ready", "socket", socketPath, "endpoints", len(config.Endpoints()), "production", options.production)
368+ log.Info("broker_ready", "socket", socketPath, "endpoints", len(config.Endpoints()))
480369 if deps.onReady != nil {
481370 deps.onReady(socketPath)
482371 }
@@ -495,7 +384,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
495384 closeErr := supervisor.Close()
496385 supervisorClosed = true
497386 <-drained
498- log.Info("broker_stopped", "production", options.production)
387+ log.Info("broker_stopped")
499388 return closeErr
500389 }
501390
@@ -10,8 +10,6 @@ import (
10 "log/slog"10 "log/slog"
11 "os"11 "os"
12 "os/signal"12 "os/signal"
13- "path/filepath"
14- "strings"
15 "syscall"13 "syscall"
16 "time"14 "time"
17 15
@@ -46,10 +44,7 @@ import (
46 // until a signal or its parent context ends it, or until the broker idles out,44 // until a signal or its parent context ends it, or until the broker idles out,
47 // and then shuts down in a fixed order.45 // and then shuts down in a fixed order.
48 46
49-const (47+const productionBrokerServeCommand = "_broker-production-serve"
50- brokerServeCommand = "_broker-serve"
51- productionBrokerServeCommand = "_broker-production-serve"
52-)
53 48
54 // brokerServePoolLimits bound the offline broker pool. They are explicit, not49 // brokerServePoolLimits bound the offline broker pool. They are explicit, not
55 // derived from the environment, and small enough that a stalled peer is50 // derived from the environment, and small enough that a stalled peer is
@@ -73,70 +68,17 @@ func brokerPoolLimits(config *brokerconfig.Config) broker.PoolLimits {
73 68
74 // brokerServeOptions is the parsed hidden invocation.69 // brokerServeOptions is the parsed hidden invocation.
75 type brokerServeOptions struct {70 type brokerServeOptions struct {
76- offlineRoot string
77- production bool
78 // idleGrace is zero when the operator did not override it; the supervisor71 // idleGrace is zero when the operator did not override it; the supervisor
79 // then applies its own 5m default.72 // then applies its own 5m default.
80 idleGrace time.Duration73 idleGrace time.Duration
81 }74 }
82 75
83-// parseBrokerServeArgs strictly parses `_broker-serve --offline-root ABS76+// parseProductionBrokerServeArgs parses the argument-free hidden serve entry.
84-// [--idle-grace DURATION]`. Unknown flags, duplicate flags, positionals, a
85-// missing value, and a non-positive or unparsable duration are refused; the
86-// path itself is validated later against the filesystem.
87 func parseProductionBrokerServeArgs(args []string) (command, error) {77 func parseProductionBrokerServeArgs(args []string) (command, error) {
88 if len(args) != 0 {78 if len(args) != 0 {
89 return command{}, usagef("`%s` does not accept arguments", productionBrokerServeCommand)79 return command{}, usagef("`%s` does not accept arguments", productionBrokerServeCommand)
90 }80 }
91- return command{kind: kindProductionBrokerServe, brokerServe: brokerServeOptions{production: true}}, nil81+ return command{kind: kindProductionBrokerServe, brokerServe: brokerServeOptions{}}, nil
92-}
93-
94-func parseBrokerServeArgs(args []string) (command, error) {
95- if len(args) == 0 {
96- return command{}, usagef("`%s` requires --offline-root", brokerServeCommand)
97- }
98- options := brokerServeOptions{}
99- var rootSet, graceSet bool
100- for index := 0; index < len(args); {
101- switch args[index] {
102- case "--offline-root":
103- if rootSet {
104- return command{}, usagef("`%s` received duplicate --offline-root", brokerServeCommand)
105- }
106- if index+1 >= len(args) || args[index+1] == "" {
107- return command{}, usagef("`--offline-root` requires a path")
108- }
109- options.offlineRoot = args[index+1]
110- rootSet = true
111- index += 2
112- case "--idle-grace":
113- if graceSet {
114- return command{}, usagef("`%s` received duplicate --idle-grace", brokerServeCommand)
115- }
116- if index+1 >= len(args) {
117- return command{}, usagef("`--idle-grace` requires a duration")
118- }
119- grace, err := time.ParseDuration(args[index+1])
120- if err != nil {
121- return command{}, usagef("`--idle-grace` %q is not a duration", args[index+1])
122- }
123- if grace <= 0 {
124- return command{}, usagef("`--idle-grace` must be positive")
125- }
126- options.idleGrace = grace
127- graceSet = true
128- index += 2
129- default:
130- if strings.HasPrefix(args[index], "-") {
131- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerServeCommand)
132- }
133- return command{}, usagef("`%s` does not accept positional arguments", brokerServeCommand)
134- }
135- }
136- if !rootSet {
137- return command{}, usagef("`%s` requires --offline-root", brokerServeCommand)
138- }
139- return command{kind: kindBrokerServe, brokerServe: options}, nil
140 }82 }
141 83
142 // brokerServeDeps are the composition seams. Production supplies the real84 // brokerServeDeps are the composition seams. Production supplies the real
@@ -194,16 +136,12 @@ func defaultBrokerServeDeps() brokerServeDeps {
194 func runBrokerServeCommand(ctx context.Context, options brokerServeOptions) (retErr error) {136 func runBrokerServeCommand(ctx context.Context, options brokerServeOptions) (retErr error) {
195 ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)137 ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
196 defer stop()138 defer stop()
197- var observerCloser io.Closer139+ observer, observerCloser, err := newPerformanceTrace(clock.New())
198- if options.production {140+ if err != nil {
199- observer, closer, err := newPerformanceTrace(clock.New())141+ return fmt.Errorf("vev: performance trace: %w", err)
200- if err != nil {142+ }
201- return fmt.Errorf("vev: performance trace: %w", err)143+ if observer != nil {
202- }144+ observer.ObserveRuntime(ports.NewRuntimeMark("broker", ports.RuntimeTransportDiagnostic, 0, true))
203- observerCloser = closer
204- if observer != nil {
205- observer.ObserveRuntime(ports.NewRuntimeMark("broker", ports.RuntimeTransportDiagnostic, 0, true))
206- }
207 }145 }
208 if observerCloser != nil {146 if observerCloser != nil {
209 defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }()147 defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }()
@@ -235,12 +173,6 @@ func newBrokerEpoch() (ports.BrokerEpoch, error) {
235 return epoch, nil173 return epoch, nil
236 }174 }
237 175
238-// offlineLayout resolves the hidden offline sandbox layout for one operator
239-// root, refusing any root that overlaps the production runtime or state paths.
240-func offlineLayout(offlineRoot string) (brokerconfig.Layout, error) {
241- return brokerconfig.ResolveLayout(offlineRoot, []string{ipc.SocketDir(), platform.StateDir()})
242-}
243-
244 // effectiveIdleGrace resolves the sandbox idle grace from an explicit flag and176 // effectiveIdleGrace resolves the sandbox idle grace from an explicit flag and
245 // the provisioned configuration. The provisioned value is the single177 // the provisioned configuration. The provisioned value is the single
246 // deterministic source: a status probe cannot read a running broker's effective178 // deterministic source: a status probe cannot read a running broker's effective
@@ -274,50 +206,32 @@ func effectiveIdleGrace(config *brokerconfig.Config, explicit time.Duration) (ti
274 func runBrokerServe(ctx context.Context, options brokerServeOptions, deps brokerServeDeps) (retErr error) {206 func runBrokerServe(ctx context.Context, options brokerServeOptions, deps brokerServeDeps) (retErr error) {
275 ctx, cancel := context.WithCancel(ctx)207 ctx, cancel := context.WithCancel(ctx)
276 defer cancel()208 defer cancel()
277- var layout brokerconfig.Layout209+ if err := ensureProductionBrokerConfig(); err != nil {
278- var configPath string210+ return err
279- var err error
280- if options.production {
281- if err := ensureProductionBrokerConfig(); err != nil {
282- return err
283- }
284- if err := safedir.EnsurePrivate(ipc.SocketDir()); err != nil {
285- return fmt.Errorf("vev: secure broker runtime parent: %w", err)
286- }
287- layout = productionBrokerLayout()
288- configPath = productionBrokerConfigPath()
289- } else {
290- layout, err = offlineLayout(options.offlineRoot)
291- if err != nil {
292- return err
293- }
294- configPath = filepath.Join(layout.Root, brokerconfig.ConfigFileName)
295 }211 }
212+ if err := safedir.EnsurePrivate(ipc.SocketDir()); err != nil {
213+ return fmt.Errorf("vev: secure broker runtime parent: %w", err)
214+ }
215+ layout := productionBrokerLayout()
216+ configPath := productionBrokerConfigPath()
296 for _, dir := range []string{layout.Root, layout.Runtime, layout.State, layout.Log} {217 for _, dir := range []string{layout.Root, layout.Runtime, layout.State, layout.Log} {
297 if err := safedir.EnsurePrivate(dir); err != nil {218 if err := safedir.EnsurePrivate(dir); err != nil {
298 return fmt.Errorf("vev: secure broker sandbox directory %s: %w", dir, err)219 return fmt.Errorf("vev: secure broker sandbox directory %s: %w", dir, err)
299 }220 }
300 }221 }
301- // ResolveLayout validates the root textually and stops at the first222+ // Re-walk every component now that EnsurePrivate created them, so a
302- // component that does not exist yet; now that EnsurePrivate has created the223+ // component swapped for a symlink in that window fails closed.
303- // sandbox paths, re-walk every component so a component swapped for a
304- // symlink in that window fails closed instead of being followed.
305 if err := layout.VerifyCreated(); err != nil {224 if err := layout.VerifyCreated(); err != nil {
306- return fmt.Errorf("vev: verify broker sandbox paths: %w", err)225+ return fmt.Errorf("vev: verify broker paths: %w", err)
307 }226 }
308- var config *brokerconfig.Config227+ // The broker is allowed to precede the daemon. Identity is therefore an
309- if options.production {228+ // optional existing authority here; acquisition binds it after an
310- // The broker is allowed to precede the daemon. Identity is therefore an229+ // authenticated StartIfNeeded connection when the daemon is first born.
311- // optional existing authority here; acquisition binds it after an230+ identity, err := daemonidentity.Load(platform.StateDir())
312- // authenticated StartIfNeeded connection when the daemon is first born.231+ if err != nil && !errors.Is(err, os.ErrNotExist) {
313- identity, loadErr := daemonidentity.Load(platform.StateDir())232+ return fmt.Errorf("vev: load daemon authority: %w", err)
314- if loadErr != nil && !errors.Is(loadErr, os.ErrNotExist) {
315- return fmt.Errorf("vev: load daemon authority: %w", loadErr)
316- }
317- config, err = brokerconfig.LoadProduction(layout, configPath, identity, localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
318- } else {
319- config, err = brokerconfig.LoadPath(layout, configPath)
320 }233 }
234+ config, err := brokerconfig.LoadProduction(layout, configPath, identity, localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
321 if err != nil {235 if err != nil {
322 return err236 return err
323 }237 }
@@ -337,15 +251,13 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
337 }251 }
338 defer func() { retErr = errors.Join(retErr, logCloser.Close()) }()252 defer func() { retErr = errors.Join(retErr, logCloser.Close()) }()
339 253
340- if options.production {254+ // The durable route schema delegates SSH trust to OpenSSH. Refuse route
341- // The durable route schema intentionally delegates SSH trust to OpenSSH.255+ // overrides that OpenSSH configuration must represent instead of dropping
342- // Refuse route overrides that OpenSSH configuration must represent instead of dropping256+ // security inputs during import.
343- // security inputs during production import.257+ for _, endpoint := range config.Endpoints() {
344- for _, endpoint := range config.Endpoints() {258+ registration, _ := config.Registration(endpoint)
345- registration, _ := config.Registration(endpoint)259+ if registration.Route.KnownHostsFile() != "" || registration.Route.ConnectTimeout() != 0 {
346- if registration.Route.KnownHostsFile() != "" || registration.Route.ConnectTimeout() != 0 {260+ return errors.New("vev: broker routes require SSH trust and timeouts in OpenSSH configuration")
347- return errors.New("vev: production broker routes require SSH trust and timeouts in OpenSSH configuration")
348- }
349 }261 }
350 }262 }
351 initialHosts, err := configuredBrokerHosts(config)263 initialHosts, err := configuredBrokerHosts(config)
@@ -388,53 +300,32 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
388 }300 }
389 defer startupLease.Release()301 defer startupLease.Release()
390 302
391- // The sandbox retains its explicit route/trust fixture contract. Production303+ // Membership is imported once, then only durable remote authority is used.
392- // imports membership once, then uses only durable remote authority.304+ resolver := &brokerRoutes{local: config.Resolver()}
393- dynamicRoutes := &brokerRoutes{local: config.Resolver()}305+ connector, err := brokerDynamicMuxConnector(config, resolver, log)
394- var resolver ports.BrokerRouteAuthority = config.Resolver()
395- connector, err := brokerMuxConnector(config, log)
396- if options.production {
397- resolver = dynamicRoutes
398- connector, err = brokerDynamicMuxConnector(config, dynamicRoutes, log)
399- }
400 if err != nil {306 if err != nil {
401 return err307 return err
402 }308 }
403- var localIdentityLoaders []localIdentityLoader309+ loadLocalIdentity := func() (ports.BrokerDaemonIdentity, error) {
404- if options.production {310+ identity, loadErr := daemonidentity.Load(platform.StateDir())
405- localIdentityLoaders = append(localIdentityLoaders, func() (ports.BrokerDaemonIdentity, error) {311+ if errors.Is(loadErr, os.ErrNotExist) {
406- identity, loadErr := daemonidentity.Load(platform.StateDir())312+ return "", nil
407- if errors.Is(loadErr, os.ErrNotExist) {313+ }
408- return "", nil314+ return identity, loadErr
409- }
410- return identity, loadErr
411- })
412 }315 }
413- registryConfig, err := offlineRegistryConfig(config, epoch, connector, localIdentityLoaders...)316+ registryConfig, err := offlineRegistryConfig(config, epoch, connector, loadLocalIdentity)
414 if err != nil {317 if err != nil {
415 return err318 return err
416 }319 }
417- var remoteProbe ports.BrokerHostProbe320+ remoteProbe := &brokerRemoteProbe{epoch: epoch, routes: resolver, connector: connector}
418- var concreteRemoteProbe *brokerRemoteProbe
419- if options.production || len(config.Endpoints()) != 0 {
420- concreteRemoteProbe = &brokerRemoteProbe{
421- epoch: epoch, routes: resolver, connector: connector,
422- }
423- remoteProbe = concreteRemoteProbe
424- }
425 registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig)321 registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig)
426 if err != nil {322 if err != nil {
427 return err323 return err
428 }324 }
429- dynamicRoutes.hosts = registry325+ resolver.hosts = registry
430- binder := ports.BrokerIdentityBinder(registry)326+ binder := productionIdentityBinder{delegate: registry, stateDir: platform.StateDir(), policy: localDaemonPolicy()}
431- if options.production {327+ remoteProbe.binder = binder
432- binder = productionIdentityBinder{delegate: registry, stateDir: platform.StateDir(), policy: localDaemonPolicy()}328+ remoteProbe.hosts = registry
433- }
434- if concreteRemoteProbe != nil {
435- concreteRemoteProbe.binder = binder
436- concreteRemoteProbe.hosts = registry
437- }
438 // RegisterRunner starts immediately: finish the cyclic composition before329 // RegisterRunner starts immediately: finish the cyclic composition before
439 // publishing it to any goroutine. Shutdown remains listener -> pool -> registry.330 // publishing it to any goroutine. Shutdown remains listener -> pool -> registry.
440 if err := supervisor.RegisterRunner("registry", registry); err != nil {331 if err := supervisor.RegisterRunner("registry", registry); err != nil {
@@ -445,9 +336,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
445 return err336 return err
446 }337 }
447 // Observation borrows pooled transports rather than dialing its own.338 // Observation borrows pooled transports rather than dialing its own.
448- if concreteRemoteProbe != nil {339+ remoteProbe.shared.share(pool)
449- concreteRemoteProbe.shared.share(pool)
450- }
451 if registryConfig.Local != nil {340 if registryConfig.Local != nil {
452 if local, ok := registryConfig.Local.Probe.(*localRouteProbe); ok {341 if local, ok := registryConfig.Local.Probe.(*localRouteProbe); ok {
453 local.shared.share(pool)342 local.shared.share(pool)
@@ -476,7 +365,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
476 go drainBrokerAccept(listener, log, started, drained, failed)365 go drainBrokerAccept(listener, log, started, drained, failed)
477 <-started366 <-started
478 startupLease.Release()367 startupLease.Release()
479- log.Info("broker_ready", "socket", socketPath, "endpoints", len(config.Endpoints()), "production", options.production)368+ log.Info("broker_ready", "socket", socketPath, "endpoints", len(config.Endpoints()))
480 if deps.onReady != nil {369 if deps.onReady != nil {
481 deps.onReady(socketPath)370 deps.onReady(socketPath)
482 }371 }
@@ -495,7 +384,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
495 closeErr := supervisor.Close()384 closeErr := supervisor.Close()
496 supervisorClosed = true385 supervisorClosed = true
497 <-drained386 <-drained
498- log.Info("broker_stopped", "production", options.production)387+ log.Info("broker_stopped")
499 return closeErr388 return closeErr
500 }389 }
501 390
D internal/app/broker_offline_client.go +0 -181
@@ -1,181 +0,0 @@
1-package app
2-
3-import (
4- "context"
5- "errors"
6- "io"
7- "log/slog"
8- "os"
9- "strings"
10-
11- "github.com/bnema/vev/internal/adapters/brokeripc"
12- "github.com/bnema/vev/internal/adapters/clock"
13- "github.com/bnema/vev/internal/adapters/term"
14- "github.com/bnema/vev/internal/adapters/uiterm"
15- "github.com/bnema/vev/internal/domain"
16- "github.com/bnema/vev/internal/ports"
17- "github.com/bnema/vev/internal/usecase/client"
18-)
19-
20-// brokerClientCommand is the hidden client entry point. It is deliberately
21-// separate from every ordinary attach, UI-driver, and web command: only an
22-// explicitly supplied offline root can select the broker sandbox.
23-const brokerClientCommand = "_broker-client"
24-
25-const (
26- offlineClientTerminal = "terminal"
27- offlineClientUIDriver = "ui-driver"
28-)
29-
30-type brokerClientOptions struct {
31- offlineRoot string
32- harness string
33- // picker starts the harness with no initial navigation at all, exactly like
34- // `--ui-driver --picker`: the client presents the picker and opens nothing.
35- picker bool
36-}
37-
38-func parseBrokerClientArgs(args []string) (command, error) {
39- options := brokerClientOptions{harness: offlineClientTerminal}
40- var rootSet, harnessSet, pickerSet bool
41- for index := 0; index < len(args); {
42- switch args[index] {
43- case "--offline-root":
44- if rootSet {
45- return command{}, usagef("`%s` received duplicate --offline-root", brokerClientCommand)
46- }
47- if index+1 >= len(args) || args[index+1] == "" {
48- return command{}, usagef("`--offline-root` requires a path")
49- }
50- options.offlineRoot, rootSet = args[index+1], true
51- index += 2
52- case "--harness":
53- if harnessSet {
54- return command{}, usagef("`%s` received duplicate --harness", brokerClientCommand)
55- }
56- if index+1 >= len(args) {
57- return command{}, usagef("`--harness` requires terminal or ui-driver")
58- }
59- options.harness, harnessSet = args[index+1], true
60- if options.harness != offlineClientTerminal && options.harness != offlineClientUIDriver {
61- return command{}, usagef("`--harness` requires terminal or ui-driver")
62- }
63- index += 2
64- case "--picker":
65- if pickerSet {
66- return command{}, usagef("`%s` received duplicate --picker", brokerClientCommand)
67- }
68- options.picker, pickerSet = true, true
69- index++
70- default:
71- if strings.HasPrefix(args[index], "-") {
72- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerClientCommand)
73- }
74- return command{}, usagef("`%s` does not accept positional arguments", brokerClientCommand)
75- }
76- }
77- if !rootSet {
78- return command{}, usagef("`%s` requires --offline-root", brokerClientCommand)
79- }
80- if pickerSet && options.harness != offlineClientUIDriver {
81- return command{}, usagef("`%s` --picker requires --harness ui-driver", brokerClientCommand)
82- }
83- return command{kind: kindBrokerClient, brokerClient: options}, nil
84-}
85-
86-func runBrokerClientCommand(ctx context.Context, options brokerClientOptions) error {
87- layout, err := offlineLayout(options.offlineRoot)
88- if err != nil {
89- return err
90- }
91- log := slog.New(slog.NewTextHandler(io.Discard, nil))
92- switch options.harness {
93- case offlineClientTerminal:
94- return runOfflineClient(ctx, brokeripc.SocketPath(layout.Runtime), term.New(), nil, log, nil, nil)
95- case offlineClientUIDriver:
96- terminal, err := uiterm.New(ctx, domain.Geometry{Size: domain.Size{Cols: uiDriverDefaultColumns, Rows: uiDriverDefaultRows}}, "")
97- if err != nil {
98- return err
99- }
100- defer terminal.Close()
101- ui := client.NewUI(terminal, clock.New())
102- navigation := localEphemeralNavigation()
103- if options.picker {
104- navigation = client.InitialNavigation{}
105- }
106- return runOfflineUIDriverClient(ctx, offlineUIDriverHarness{
107- socket: brokeripc.SocketPath(layout.Runtime),
108- terminal: terminal,
109- ui: ui,
110- log: log,
111- navigation: navigation,
112- stream: &stdioStream{reader: os.Stdin, writer: os.Stdout},
113- })
114- default:
115- return usagef("unknown offline client harness %q", options.harness)
116- }
117-}
118-
119-// runOfflineClient is the sandbox terminal harness over the shared broker
120-// client composition. It supplies the real broker IPC connector for one
121-// offline root and the no-argument ephemeral local creation, then delegates the
122-// whole run to runBrokerClient: the sandbox adds no picker, supervisor, dialer,
123-// or stream of its own.
124-func runOfflineClient(ctx context.Context, socket string, terminal ports.Terminal, ui *client.UI, log *slog.Logger, onState func(client.State), onFailure func(error)) error {
125- if terminal == nil {
126- return errors.New("vev: offline client requires a terminal")
127- }
128- if log != nil {
129- log.Debug("broker_offline_client", "socket", socket)
130- }
131- return runBrokerClient(ctx, brokerClientConfig{
132- Connector: brokeripc.NewConnector(socket, brokeripc.Config{}),
133- Terminal: terminal,
134- UI: ui,
135- // No-argument composition creates one ephemeral local session through
136- // the closed initial-navigation union; there is no special attach path.
137- InitialNavigation: localEphemeralNavigation(),
138- AttachmentEnvironment: terminalAttachmentEnvironment(),
139- SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
140- OnState: onState,
141- OnFailure: onFailure,
142- })
143-}
144-
145-// offlineUIDriverHarness is the sandbox harness input: one explicitly supplied
146-// offline broker socket, the virtual terminal and UI service built over it, the
147-// one-shot navigation the harness was asked to perform, and the stream the JSONL
148-// protocol is served on. The CLI's `--picker` selects the zero picker intent; the
149-// default is the same no-argument local ephemeral creation every frontend uses.
150-type offlineUIDriverHarness struct {
151- socket string
152- terminal *uiterm.Terminal
153- ui *client.UI
154- log *slog.Logger
155- navigation client.InitialNavigation
156- resolver client.InitialNavigationResolver
157- stream io.ReadWriteCloser
158-}
159-
160-// runOfflineUIDriverClient composes the sandbox UI-driver harness over the same
161-// runUIDriverClient path production uses. Its connector is the real broker IPC
162-// connector of one explicitly supplied offline root: the sandbox adds no
163-// production fallback, starts no broker of its own, and never reaches the
164-// production XDG runtime or state. No session, endpoint, or process is
165-// provisioned here: the harness reaches sessions only through the broker of its
166-// own root, exactly like the production driver.
167-func runOfflineUIDriverClient(ctx context.Context, harness offlineUIDriverHarness) error {
168- if harness.log != nil {
169- harness.log.Debug("broker_offline_ui_driver", "socket", harness.socket)
170- }
171- return runUIDriverClient(ctx, brokerClientConfig{
172- Connector: brokeripc.NewConnector(harness.socket, brokeripc.Config{}),
173- Terminal: harness.terminal,
174- Clock: clock.New(),
175- UI: harness.ui,
176- InitialNavigation: harness.navigation,
177- ResolveInitialNavigation: harness.resolver,
178- AttachmentEnvironment: terminalAttachmentEnvironment(),
179- SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
180- }, harness.stream)
181-}
@@ -1,181 +0,0 @@
1-package app
2-
3-import (
4- "context"
5- "errors"
6- "io"
7- "log/slog"
8- "os"
9- "strings"
10-
11- "github.com/bnema/vev/internal/adapters/brokeripc"
12- "github.com/bnema/vev/internal/adapters/clock"
13- "github.com/bnema/vev/internal/adapters/term"
14- "github.com/bnema/vev/internal/adapters/uiterm"
15- "github.com/bnema/vev/internal/domain"
16- "github.com/bnema/vev/internal/ports"
17- "github.com/bnema/vev/internal/usecase/client"
18-)
19-
20-// brokerClientCommand is the hidden client entry point. It is deliberately
21-// separate from every ordinary attach, UI-driver, and web command: only an
22-// explicitly supplied offline root can select the broker sandbox.
23-const brokerClientCommand = "_broker-client"
24-
25-const (
26- offlineClientTerminal = "terminal"
27- offlineClientUIDriver = "ui-driver"
28-)
29-
30-type brokerClientOptions struct {
31- offlineRoot string
32- harness string
33- // picker starts the harness with no initial navigation at all, exactly like
34- // `--ui-driver --picker`: the client presents the picker and opens nothing.
35- picker bool
36-}
37-
38-func parseBrokerClientArgs(args []string) (command, error) {
39- options := brokerClientOptions{harness: offlineClientTerminal}
40- var rootSet, harnessSet, pickerSet bool
41- for index := 0; index < len(args); {
42- switch args[index] {
43- case "--offline-root":
44- if rootSet {
45- return command{}, usagef("`%s` received duplicate --offline-root", brokerClientCommand)
46- }
47- if index+1 >= len(args) || args[index+1] == "" {
48- return command{}, usagef("`--offline-root` requires a path")
49- }
50- options.offlineRoot, rootSet = args[index+1], true
51- index += 2
52- case "--harness":
53- if harnessSet {
54- return command{}, usagef("`%s` received duplicate --harness", brokerClientCommand)
55- }
56- if index+1 >= len(args) {
57- return command{}, usagef("`--harness` requires terminal or ui-driver")
58- }
59- options.harness, harnessSet = args[index+1], true
60- if options.harness != offlineClientTerminal && options.harness != offlineClientUIDriver {
61- return command{}, usagef("`--harness` requires terminal or ui-driver")
62- }
63- index += 2
64- case "--picker":
65- if pickerSet {
66- return command{}, usagef("`%s` received duplicate --picker", brokerClientCommand)
67- }
68- options.picker, pickerSet = true, true
69- index++
70- default:
71- if strings.HasPrefix(args[index], "-") {
72- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerClientCommand)
73- }
74- return command{}, usagef("`%s` does not accept positional arguments", brokerClientCommand)
75- }
76- }
77- if !rootSet {
78- return command{}, usagef("`%s` requires --offline-root", brokerClientCommand)
79- }
80- if pickerSet && options.harness != offlineClientUIDriver {
81- return command{}, usagef("`%s` --picker requires --harness ui-driver", brokerClientCommand)
82- }
83- return command{kind: kindBrokerClient, brokerClient: options}, nil
84-}
85-
86-func runBrokerClientCommand(ctx context.Context, options brokerClientOptions) error {
87- layout, err := offlineLayout(options.offlineRoot)
88- if err != nil {
89- return err
90- }
91- log := slog.New(slog.NewTextHandler(io.Discard, nil))
92- switch options.harness {
93- case offlineClientTerminal:
94- return runOfflineClient(ctx, brokeripc.SocketPath(layout.Runtime), term.New(), nil, log, nil, nil)
95- case offlineClientUIDriver:
96- terminal, err := uiterm.New(ctx, domain.Geometry{Size: domain.Size{Cols: uiDriverDefaultColumns, Rows: uiDriverDefaultRows}}, "")
97- if err != nil {
98- return err
99- }
100- defer terminal.Close()
101- ui := client.NewUI(terminal, clock.New())
102- navigation := localEphemeralNavigation()
103- if options.picker {
104- navigation = client.InitialNavigation{}
105- }
106- return runOfflineUIDriverClient(ctx, offlineUIDriverHarness{
107- socket: brokeripc.SocketPath(layout.Runtime),
108- terminal: terminal,
109- ui: ui,
110- log: log,
111- navigation: navigation,
112- stream: &stdioStream{reader: os.Stdin, writer: os.Stdout},
113- })
114- default:
115- return usagef("unknown offline client harness %q", options.harness)
116- }
117-}
118-
119-// runOfflineClient is the sandbox terminal harness over the shared broker
120-// client composition. It supplies the real broker IPC connector for one
121-// offline root and the no-argument ephemeral local creation, then delegates the
122-// whole run to runBrokerClient: the sandbox adds no picker, supervisor, dialer,
123-// or stream of its own.
124-func runOfflineClient(ctx context.Context, socket string, terminal ports.Terminal, ui *client.UI, log *slog.Logger, onState func(client.State), onFailure func(error)) error {
125- if terminal == nil {
126- return errors.New("vev: offline client requires a terminal")
127- }
128- if log != nil {
129- log.Debug("broker_offline_client", "socket", socket)
130- }
131- return runBrokerClient(ctx, brokerClientConfig{
132- Connector: brokeripc.NewConnector(socket, brokeripc.Config{}),
133- Terminal: terminal,
134- UI: ui,
135- // No-argument composition creates one ephemeral local session through
136- // the closed initial-navigation union; there is no special attach path.
137- InitialNavigation: localEphemeralNavigation(),
138- AttachmentEnvironment: terminalAttachmentEnvironment(),
139- SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
140- OnState: onState,
141- OnFailure: onFailure,
142- })
143-}
144-
145-// offlineUIDriverHarness is the sandbox harness input: one explicitly supplied
146-// offline broker socket, the virtual terminal and UI service built over it, the
147-// one-shot navigation the harness was asked to perform, and the stream the JSONL
148-// protocol is served on. The CLI's `--picker` selects the zero picker intent; the
149-// default is the same no-argument local ephemeral creation every frontend uses.
150-type offlineUIDriverHarness struct {
151- socket string
152- terminal *uiterm.Terminal
153- ui *client.UI
154- log *slog.Logger
155- navigation client.InitialNavigation
156- resolver client.InitialNavigationResolver
157- stream io.ReadWriteCloser
158-}
159-
160-// runOfflineUIDriverClient composes the sandbox UI-driver harness over the same
161-// runUIDriverClient path production uses. Its connector is the real broker IPC
162-// connector of one explicitly supplied offline root: the sandbox adds no
163-// production fallback, starts no broker of its own, and never reaches the
164-// production XDG runtime or state. No session, endpoint, or process is
165-// provisioned here: the harness reaches sessions only through the broker of its
166-// own root, exactly like the production driver.
167-func runOfflineUIDriverClient(ctx context.Context, harness offlineUIDriverHarness) error {
168- if harness.log != nil {
169- harness.log.Debug("broker_offline_ui_driver", "socket", harness.socket)
170- }
171- return runUIDriverClient(ctx, brokerClientConfig{
172- Connector: brokeripc.NewConnector(harness.socket, brokeripc.Config{}),
173- Terminal: harness.terminal,
174- Clock: clock.New(),
175- UI: harness.ui,
176- InitialNavigation: harness.navigation,
177- ResolveInitialNavigation: harness.resolver,
178- AttachmentEnvironment: terminalAttachmentEnvironment(),
179- SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
180- }, harness.stream)
181-}
M internal/app/broker_offline_client_test.go +1 -1
@@ -138,7 +138,7 @@ func startPersistentOfflineClientFixture(t *testing.T, shell string, shellArgs [
138138 // a manual clock rooted at Unix zero would correctly make the observation
139139 // stale to the real client clock before initial navigation resolves.
140140 deps, ready := testBrokerServeDeps(clock.New())
141- brokerDone := runSandbox(ctx, brokerServeOptions{production: true, idleGrace: time.Hour}, deps)
141+ brokerDone := runSandbox(ctx, brokerServeOptions{idleGrace: time.Hour}, deps)
142142 var socket string
143143 select {
144144 case socket = <-ready:
@@ -138,7 +138,7 @@ func startPersistentOfflineClientFixture(t *testing.T, shell string, shellArgs [
138 // a manual clock rooted at Unix zero would correctly make the observation138 // a manual clock rooted at Unix zero would correctly make the observation
139 // stale to the real client clock before initial navigation resolves.139 // stale to the real client clock before initial navigation resolves.
140 deps, ready := testBrokerServeDeps(clock.New())140 deps, ready := testBrokerServeDeps(clock.New())
141- brokerDone := runSandbox(ctx, brokerServeOptions{production: true, idleGrace: time.Hour}, deps)141+ brokerDone := runSandbox(ctx, brokerServeOptions{idleGrace: time.Hour}, deps)
142 var socket string142 var socket string
143 select {143 select {
144 case socket = <-ready:144 case socket = <-ready:
A internal/app/broker_offline_client_test_helpers_test.go +78 -0
@@ -0,0 +1,78 @@
1+package app
2+
3+import (
4+ "context"
5+ "errors"
6+ "io"
7+ "log/slog"
8+
9+ "github.com/bnema/vev/internal/adapters/brokeripc"
10+ "github.com/bnema/vev/internal/adapters/clock"
11+ "github.com/bnema/vev/internal/adapters/uiterm"
12+ "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/usecase/client"
14+)
15+
16+// runOfflineClient is the sandbox terminal harness over the shared broker
17+// client composition. It supplies the real broker IPC connector for one
18+// offline root and the no-argument ephemeral local creation, then delegates the
19+// whole run to runBrokerClient: the sandbox adds no picker, supervisor, dialer,
20+// or stream of its own.
21+func runOfflineClient(ctx context.Context, socket string, terminal ports.Terminal, ui *client.UI, log *slog.Logger, onState func(client.State), onFailure func(error)) error {
22+ if terminal == nil {
23+ return errors.New("vev: offline client requires a terminal")
24+ }
25+ if log != nil {
26+ log.Debug("broker_offline_client", "socket", socket)
27+ }
28+ return runBrokerClient(ctx, brokerClientConfig{
29+ Connector: brokeripc.NewConnector(socket, brokeripc.Config{}),
30+ Terminal: terminal,
31+ UI: ui,
32+ // No-argument composition creates one ephemeral local session through
33+ // the closed initial-navigation union; there is no special attach path.
34+ InitialNavigation: localEphemeralNavigation(),
35+ AttachmentEnvironment: terminalAttachmentEnvironment(),
36+ SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
37+ OnState: onState,
38+ OnFailure: onFailure,
39+ })
40+}
41+
42+// offlineUIDriverHarness is the sandbox harness input: one explicitly supplied
43+// offline broker socket, the virtual terminal and UI service built over it, the
44+// one-shot navigation the harness was asked to perform, and the stream the JSONL
45+// protocol is served on. The CLI's `--picker` selects the zero picker intent; the
46+// default is the same no-argument local ephemeral creation every frontend uses.
47+type offlineUIDriverHarness struct {
48+ socket string
49+ terminal *uiterm.Terminal
50+ ui *client.UI
51+ log *slog.Logger
52+ navigation client.InitialNavigation
53+ resolver client.InitialNavigationResolver
54+ stream io.ReadWriteCloser
55+}
56+
57+// runOfflineUIDriverClient composes the sandbox UI-driver harness over the same
58+// runUIDriverClient path production uses. Its connector is the real broker IPC
59+// connector of one explicitly supplied offline root: the sandbox adds no
60+// production fallback, starts no broker of its own, and never reaches the
61+// production XDG runtime or state. No session, endpoint, or process is
62+// provisioned here: the harness reaches sessions only through the broker of its
63+// own root, exactly like the production driver.
64+func runOfflineUIDriverClient(ctx context.Context, harness offlineUIDriverHarness) error {
65+ if harness.log != nil {
66+ harness.log.Debug("broker_offline_ui_driver", "socket", harness.socket)
67+ }
68+ return runUIDriverClient(ctx, brokerClientConfig{
69+ Connector: brokeripc.NewConnector(harness.socket, brokeripc.Config{}),
70+ Terminal: harness.terminal,
71+ Clock: clock.New(),
72+ UI: harness.ui,
73+ InitialNavigation: harness.navigation,
74+ ResolveInitialNavigation: harness.resolver,
75+ AttachmentEnvironment: terminalAttachmentEnvironment(),
76+ SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
77+ }, harness.stream)
78+}
@@ -0,0 +1,78 @@
1+package app
2+
3+import (
4+ "context"
5+ "errors"
6+ "io"
7+ "log/slog"
8+
9+ "github.com/bnema/vev/internal/adapters/brokeripc"
10+ "github.com/bnema/vev/internal/adapters/clock"
11+ "github.com/bnema/vev/internal/adapters/uiterm"
12+ "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/usecase/client"
14+)
15+
16+// runOfflineClient is the sandbox terminal harness over the shared broker
17+// client composition. It supplies the real broker IPC connector for one
18+// offline root and the no-argument ephemeral local creation, then delegates the
19+// whole run to runBrokerClient: the sandbox adds no picker, supervisor, dialer,
20+// or stream of its own.
21+func runOfflineClient(ctx context.Context, socket string, terminal ports.Terminal, ui *client.UI, log *slog.Logger, onState func(client.State), onFailure func(error)) error {
22+ if terminal == nil {
23+ return errors.New("vev: offline client requires a terminal")
24+ }
25+ if log != nil {
26+ log.Debug("broker_offline_client", "socket", socket)
27+ }
28+ return runBrokerClient(ctx, brokerClientConfig{
29+ Connector: brokeripc.NewConnector(socket, brokeripc.Config{}),
30+ Terminal: terminal,
31+ UI: ui,
32+ // No-argument composition creates one ephemeral local session through
33+ // the closed initial-navigation union; there is no special attach path.
34+ InitialNavigation: localEphemeralNavigation(),
35+ AttachmentEnvironment: terminalAttachmentEnvironment(),
36+ SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
37+ OnState: onState,
38+ OnFailure: onFailure,
39+ })
40+}
41+
42+// offlineUIDriverHarness is the sandbox harness input: one explicitly supplied
43+// offline broker socket, the virtual terminal and UI service built over it, the
44+// one-shot navigation the harness was asked to perform, and the stream the JSONL
45+// protocol is served on. The CLI's `--picker` selects the zero picker intent; the
46+// default is the same no-argument local ephemeral creation every frontend uses.
47+type offlineUIDriverHarness struct {
48+ socket string
49+ terminal *uiterm.Terminal
50+ ui *client.UI
51+ log *slog.Logger
52+ navigation client.InitialNavigation
53+ resolver client.InitialNavigationResolver
54+ stream io.ReadWriteCloser
55+}
56+
57+// runOfflineUIDriverClient composes the sandbox UI-driver harness over the same
58+// runUIDriverClient path production uses. Its connector is the real broker IPC
59+// connector of one explicitly supplied offline root: the sandbox adds no
60+// production fallback, starts no broker of its own, and never reaches the
61+// production XDG runtime or state. No session, endpoint, or process is
62+// provisioned here: the harness reaches sessions only through the broker of its
63+// own root, exactly like the production driver.
64+func runOfflineUIDriverClient(ctx context.Context, harness offlineUIDriverHarness) error {
65+ if harness.log != nil {
66+ harness.log.Debug("broker_offline_ui_driver", "socket", harness.socket)
67+ }
68+ return runUIDriverClient(ctx, brokerClientConfig{
69+ Connector: brokeripc.NewConnector(harness.socket, brokeripc.Config{}),
70+ Terminal: harness.terminal,
71+ Clock: clock.New(),
72+ UI: harness.ui,
73+ InitialNavigation: harness.navigation,
74+ ResolveInitialNavigation: harness.resolver,
75+ AttachmentEnvironment: terminalAttachmentEnvironment(),
76+ SessionEnvironment: client.SessionEnvironment{Provenance: client.SessionEnvironmentLocalCLI},
77+ }, harness.stream)
78+}
M internal/app/broker_offline_mux.go +15 -83
@@ -101,46 +101,36 @@ const brokerMuxWaitDelay = 2 * time.Second
101101 // brokerDaemonStartArgvFlag's exact spelling, so the two sides can never drift.
102102 const brokerDaemonStartArg = "--daemon-start"
103103
104+// brokerScopeArg is the scope flag carried by every durable remote route argv
105+// (see ports.BrokerRouteForTransport). Stored routes keep it verbatim.
106+const brokerScopeArg = "--production"
107+
104108 // brokerMuxOptions is the parsed hidden mux helper invocation.
105109 type brokerMuxOptions struct {
106- production bool
107- offlineRoot string
108110 // startMode is the daemon-start authorization the broker propagated. An
109111 // absent flag is the safe default: the helper observes only what is already
110112 // running and never starts a daemon, a broker, or a recursive helper.
111113 startMode ports.BrokerDaemonStartMode
112114 }
113115
114-// parseBrokerMuxArgs strictly parses `<hidden> (--offline-root ABS | --production)
115-// [--daemon-start existing-only|if-needed]`. Unknown or duplicate flags, a
116-// missing or unknown value, and positionals are refused, exactly like the
117-// foreground sandbox entry point. An absent --daemon-start defaults to
116+// parseBrokerMuxArgs strictly parses `<hidden> [--daemon-start
117+// existing-only|if-needed]`. Unknown or duplicate flags, a missing or unknown
118+// value, and positionals are refused. An absent --daemon-start defaults to
118119 // existing-only, and an unrecognized value is never mapped onto a permissive
119120 // authorization.
120121 func parseBrokerMuxArgs(name string, kind cmdKind, args []string) (command, error) {
121122 options := brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}
122- rootSet, startSet := false, false
123+ scopeSet, startSet := false, false
123124 for index := 0; index < len(args); {
124125 switch args[index] {
125- case "--production":
126- if options.production || rootSet {
127- return command{}, usagef("`%s` received duplicate or conflicting scope", name)
126+ case brokerScopeArg:
127+ // The scope flag is part of every durable remote route argv, so it
128+ // stays accepted; the helper always serves the per-user scope.
129+ if scopeSet {
130+ return command{}, usagef("`%s` received duplicate %s", name, brokerScopeArg)
128131 }
129- options.production = true
132+ scopeSet = true
130133 index++
131- case "--offline-root":
132- if options.production {
133- return command{}, usagef("`%s` received conflicting scope", name)
134- }
135- if rootSet {
136- return command{}, usagef("`%s` received duplicate --offline-root", name)
137- }
138- if index+1 >= len(args) || args[index+1] == "" {
139- return command{}, usagef("`--offline-root` requires a path")
140- }
141- options.offlineRoot = args[index+1]
142- rootSet = true
143- index += 2
144134 case brokerDaemonStartArg:
145135 if startSet {
146136 return command{}, usagef("`%s` received duplicate %s", name, brokerDaemonStartArg)
@@ -162,48 +152,9 @@ func parseBrokerMuxArgs(name string, kind cmdKind, args []string) (command, erro
162152 return command{}, usagef("`%s` does not accept positional arguments", name)
163153 }
164154 }
165- if !rootSet && !options.production {
166- return command{}, usagef("`%s` requires --offline-root or --production", name)
167- }
168155 return command{kind: kind, brokerMux: options}, nil
169156 }
170157
171-// loadMuxHelperConfig validates one offline root and loads its strict sandbox
172-// configuration. A helper never creates sandbox directories: the root and the
173-// config marker must already exist, so a helper can never provision state.
174-func loadMuxHelperConfig(offlineRoot string) (*brokerconfig.Config, error) {
175- layout, err := offlineLayout(offlineRoot)
176- if err != nil {
177- return nil, err
178- }
179- return brokerconfig.Load(layout)
180-}
181-
182-// brokerMuxConnector builds the transport-selecting daemonmux endpoint
183-// connector over one immutable configuration. The dial function receives the
184-// whole resolved target the pool will authenticate against and refuses any
185-// address the configuration did not produce, so a request can never select a
186-// transport, an address, or a start authorization of its own. It re-checks that
187-// the route's provisioned authority still matches the resolved policy and fence
188-// before dialing, so the transport is only ever reached under the authority the
189-// resolver published.
190-func brokerMuxConnector(config *brokerconfig.Config, log *slog.Logger) (*daemonmux.EndpointConnector, error) {
191- if config == nil {
192- return nil, errors.New("vev: broker mux connector requires a configuration")
193- }
194- dial := func(ctx context.Context, target ports.BrokerDialTarget) (daemonmux.RawFramedTransport, error) {
195- route, ok := config.RouteByAddress(target.Address)
196- if !ok {
197- return nil, fmt.Errorf("vev: broker mux: unknown route address")
198- }
199- if err := brokerRouteAuthorityMatches(config, target); err != nil {
200- return nil, err
201- }
202- return dialBrokerRoute(ctx, route, target, log)
203- }
204- return daemonmux.NewEndpointConnector(dial, daemonmux.DefaultMuxCeilings())
205-}
206-
207158 // brokerDynamicMuxConnector keeps the configured local carriage, but remote
208159 // targets must still match committed registry authority at dial time.
209160 func brokerDynamicMuxConnector(config *brokerconfig.Config, routes *brokerRoutes, log *slog.Logger) (*daemonmux.EndpointConnector, error) {
@@ -530,13 +481,6 @@ func runBrokerMuxStdioCommand(ctx context.Context, options brokerMuxOptions) err
530481 // proxy owns the carriage lifetime in its own session and survives this short-
531482 // lived bootstrap, exactly like the ordinary QUIC bootstrap.
532483 func runBrokerMuxQUICBootstrapCommand(ctx context.Context, options brokerMuxOptions) error {
533- // Validate the sandbox before spawning anything, so a bad root or config
534- // fails without a readiness line and without a child.
535- if !options.production {
536- if _, err := loadMuxHelperConfig(options.offlineRoot); err != nil {
537- return err
538- }
539- }
540484 exe, err := os.Executable()
541485 if err != nil {
542486 return err
@@ -553,12 +497,7 @@ func runBrokerMuxQUICBootstrapCommand(ctx context.Context, options brokerMuxOpti
553497 }
554498 defer func() { _ = devNull.Close() }()
555499
556- args := []string{brokerMuxQUICProxyCommand}
557- if options.production {
558- args = append(args, "--production")
559- } else {
560- args = append(args, "--offline-root", options.offlineRoot)
561- }
500+ args := []string{brokerMuxQUICProxyCommand, brokerScopeArg}
562501 flag, err := brokerDaemonStartArgvFlag(options.startMode)
563502 if err != nil {
564503 _ = writer.Close()
@@ -652,13 +591,6 @@ func runBrokerMuxQUICProxyCommand(ctx context.Context, options brokerMuxOptions)
652591 // dialBrokerMuxHelper is a remote-side broker carriage helper, not a client
653592 // daemon dial. Production authority is derived locally, never from SSH argv.
654593 func dialBrokerMuxHelper(ctx context.Context, options brokerMuxOptions) (daemonmux.RawFramedTransport, error) {
655- if !options.production {
656- config, err := loadMuxHelperConfig(options.offlineRoot)
657- if err != nil {
658- return nil, err
659- }
660- return dialMuxHelperCarriage(ctx, config, options.startMode)
661- }
662594 route, err := brokerconfig.RouteFromSpec(ports.BrokerRouteSpec{Kind: ports.BrokerRouteUnix, Path: daemonmux.SocketPath(ipc.SocketDir())})
663595 if err != nil {
664596 return nil, err
@@ -101,46 +101,36 @@ const brokerMuxWaitDelay = 2 * time.Second
101 // brokerDaemonStartArgvFlag's exact spelling, so the two sides can never drift.101 // brokerDaemonStartArgvFlag's exact spelling, so the two sides can never drift.
102 const brokerDaemonStartArg = "--daemon-start"102 const brokerDaemonStartArg = "--daemon-start"
103 103
104+// brokerScopeArg is the scope flag carried by every durable remote route argv
105+// (see ports.BrokerRouteForTransport). Stored routes keep it verbatim.
106+const brokerScopeArg = "--production"
107+
104 // brokerMuxOptions is the parsed hidden mux helper invocation.108 // brokerMuxOptions is the parsed hidden mux helper invocation.
105 type brokerMuxOptions struct {109 type brokerMuxOptions struct {
106- production bool
107- offlineRoot string
108 // startMode is the daemon-start authorization the broker propagated. An110 // startMode is the daemon-start authorization the broker propagated. An
109 // absent flag is the safe default: the helper observes only what is already111 // absent flag is the safe default: the helper observes only what is already
110 // running and never starts a daemon, a broker, or a recursive helper.112 // running and never starts a daemon, a broker, or a recursive helper.
111 startMode ports.BrokerDaemonStartMode113 startMode ports.BrokerDaemonStartMode
112 }114 }
113 115
114-// parseBrokerMuxArgs strictly parses `<hidden> (--offline-root ABS | --production)116+// parseBrokerMuxArgs strictly parses `<hidden> [--daemon-start
115-// [--daemon-start existing-only|if-needed]`. Unknown or duplicate flags, a117+// existing-only|if-needed]`. Unknown or duplicate flags, a missing or unknown
116-// missing or unknown value, and positionals are refused, exactly like the118+// value, and positionals are refused. An absent --daemon-start defaults to
117-// foreground sandbox entry point. An absent --daemon-start defaults to
118 // existing-only, and an unrecognized value is never mapped onto a permissive119 // existing-only, and an unrecognized value is never mapped onto a permissive
119 // authorization.120 // authorization.
120 func parseBrokerMuxArgs(name string, kind cmdKind, args []string) (command, error) {121 func parseBrokerMuxArgs(name string, kind cmdKind, args []string) (command, error) {
121 options := brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}122 options := brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}
122- rootSet, startSet := false, false123+ scopeSet, startSet := false, false
123 for index := 0; index < len(args); {124 for index := 0; index < len(args); {
124 switch args[index] {125 switch args[index] {
125- case "--production":126+ case brokerScopeArg:
126- if options.production || rootSet {127+ // The scope flag is part of every durable remote route argv, so it
127- return command{}, usagef("`%s` received duplicate or conflicting scope", name)128+ // stays accepted; the helper always serves the per-user scope.
129+ if scopeSet {
130+ return command{}, usagef("`%s` received duplicate %s", name, brokerScopeArg)
128 }131 }
129- options.production = true132+ scopeSet = true
130 index++133 index++
131- case "--offline-root":
132- if options.production {
133- return command{}, usagef("`%s` received conflicting scope", name)
134- }
135- if rootSet {
136- return command{}, usagef("`%s` received duplicate --offline-root", name)
137- }
138- if index+1 >= len(args) || args[index+1] == "" {
139- return command{}, usagef("`--offline-root` requires a path")
140- }
141- options.offlineRoot = args[index+1]
142- rootSet = true
143- index += 2
144 case brokerDaemonStartArg:134 case brokerDaemonStartArg:
145 if startSet {135 if startSet {
146 return command{}, usagef("`%s` received duplicate %s", name, brokerDaemonStartArg)136 return command{}, usagef("`%s` received duplicate %s", name, brokerDaemonStartArg)
@@ -162,48 +152,9 @@ func parseBrokerMuxArgs(name string, kind cmdKind, args []string) (command, erro
162 return command{}, usagef("`%s` does not accept positional arguments", name)152 return command{}, usagef("`%s` does not accept positional arguments", name)
163 }153 }
164 }154 }
165- if !rootSet && !options.production {
166- return command{}, usagef("`%s` requires --offline-root or --production", name)
167- }
168 return command{kind: kind, brokerMux: options}, nil155 return command{kind: kind, brokerMux: options}, nil
169 }156 }
170 157
171-// loadMuxHelperConfig validates one offline root and loads its strict sandbox
172-// configuration. A helper never creates sandbox directories: the root and the
173-// config marker must already exist, so a helper can never provision state.
174-func loadMuxHelperConfig(offlineRoot string) (*brokerconfig.Config, error) {
175- layout, err := offlineLayout(offlineRoot)
176- if err != nil {
177- return nil, err
178- }
179- return brokerconfig.Load(layout)
180-}
181-
182-// brokerMuxConnector builds the transport-selecting daemonmux endpoint
183-// connector over one immutable configuration. The dial function receives the
184-// whole resolved target the pool will authenticate against and refuses any
185-// address the configuration did not produce, so a request can never select a
186-// transport, an address, or a start authorization of its own. It re-checks that
187-// the route's provisioned authority still matches the resolved policy and fence
188-// before dialing, so the transport is only ever reached under the authority the
189-// resolver published.
190-func brokerMuxConnector(config *brokerconfig.Config, log *slog.Logger) (*daemonmux.EndpointConnector, error) {
191- if config == nil {
192- return nil, errors.New("vev: broker mux connector requires a configuration")
193- }
194- dial := func(ctx context.Context, target ports.BrokerDialTarget) (daemonmux.RawFramedTransport, error) {
195- route, ok := config.RouteByAddress(target.Address)
196- if !ok {
197- return nil, fmt.Errorf("vev: broker mux: unknown route address")
198- }
199- if err := brokerRouteAuthorityMatches(config, target); err != nil {
200- return nil, err
201- }
202- return dialBrokerRoute(ctx, route, target, log)
203- }
204- return daemonmux.NewEndpointConnector(dial, daemonmux.DefaultMuxCeilings())
205-}
206-
207 // brokerDynamicMuxConnector keeps the configured local carriage, but remote158 // brokerDynamicMuxConnector keeps the configured local carriage, but remote
208 // targets must still match committed registry authority at dial time.159 // targets must still match committed registry authority at dial time.
209 func brokerDynamicMuxConnector(config *brokerconfig.Config, routes *brokerRoutes, log *slog.Logger) (*daemonmux.EndpointConnector, error) {160 func brokerDynamicMuxConnector(config *brokerconfig.Config, routes *brokerRoutes, log *slog.Logger) (*daemonmux.EndpointConnector, error) {
@@ -530,13 +481,6 @@ func runBrokerMuxStdioCommand(ctx context.Context, options brokerMuxOptions) err
530 // proxy owns the carriage lifetime in its own session and survives this short-481 // proxy owns the carriage lifetime in its own session and survives this short-
531 // lived bootstrap, exactly like the ordinary QUIC bootstrap.482 // lived bootstrap, exactly like the ordinary QUIC bootstrap.
532 func runBrokerMuxQUICBootstrapCommand(ctx context.Context, options brokerMuxOptions) error {483 func runBrokerMuxQUICBootstrapCommand(ctx context.Context, options brokerMuxOptions) error {
533- // Validate the sandbox before spawning anything, so a bad root or config
534- // fails without a readiness line and without a child.
535- if !options.production {
536- if _, err := loadMuxHelperConfig(options.offlineRoot); err != nil {
537- return err
538- }
539- }
540 exe, err := os.Executable()484 exe, err := os.Executable()
541 if err != nil {485 if err != nil {
542 return err486 return err
@@ -553,12 +497,7 @@ func runBrokerMuxQUICBootstrapCommand(ctx context.Context, options brokerMuxOpti
553 }497 }
554 defer func() { _ = devNull.Close() }()498 defer func() { _ = devNull.Close() }()
555 499
556- args := []string{brokerMuxQUICProxyCommand}500+ args := []string{brokerMuxQUICProxyCommand, brokerScopeArg}
557- if options.production {
558- args = append(args, "--production")
559- } else {
560- args = append(args, "--offline-root", options.offlineRoot)
561- }
562 flag, err := brokerDaemonStartArgvFlag(options.startMode)501 flag, err := brokerDaemonStartArgvFlag(options.startMode)
563 if err != nil {502 if err != nil {
564 _ = writer.Close()503 _ = writer.Close()
@@ -652,13 +591,6 @@ func runBrokerMuxQUICProxyCommand(ctx context.Context, options brokerMuxOptions)
652 // dialBrokerMuxHelper is a remote-side broker carriage helper, not a client591 // dialBrokerMuxHelper is a remote-side broker carriage helper, not a client
653 // daemon dial. Production authority is derived locally, never from SSH argv.592 // daemon dial. Production authority is derived locally, never from SSH argv.
654 func dialBrokerMuxHelper(ctx context.Context, options brokerMuxOptions) (daemonmux.RawFramedTransport, error) {593 func dialBrokerMuxHelper(ctx context.Context, options brokerMuxOptions) (daemonmux.RawFramedTransport, error) {
655- if !options.production {
656- config, err := loadMuxHelperConfig(options.offlineRoot)
657- if err != nil {
658- return nil, err
659- }
660- return dialMuxHelperCarriage(ctx, config, options.startMode)
661- }
662 route, err := brokerconfig.RouteFromSpec(ports.BrokerRouteSpec{Kind: ports.BrokerRouteUnix, Path: daemonmux.SocketPath(ipc.SocketDir())})594 route, err := brokerconfig.RouteFromSpec(ports.BrokerRouteSpec{Kind: ports.BrokerRouteUnix, Path: daemonmux.SocketPath(ipc.SocketDir())})
663 if err != nil {595 if err != nil {
664 return nil, err596 return nil, err
M internal/app/broker_offline_mux_test.go +4 -22
@@ -249,9 +249,9 @@ func TestParseBrokerMuxArgs(t *testing.T) {
249249 wantErr string
250250 want brokerMuxOptions
251251 }{
252- {name: "valid", args: []string{"--production"}, want: brokerMuxOptions{production: true, startMode: ports.BrokerDaemonExistingOnly}},
253- {name: "explicit existing-only", args: []string{"--production", brokerDaemonStartArg, "existing-only"}, want: brokerMuxOptions{production: true, startMode: ports.BrokerDaemonExistingOnly}},
254- {name: "explicit if-needed", args: []string{"--production", brokerDaemonStartArg, "if-needed"}, want: brokerMuxOptions{production: true, startMode: ports.BrokerDaemonStartIfNeeded}},
252+ {name: "valid", args: []string{"--production"}, want: brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}},
253+ {name: "explicit existing-only", args: []string{"--production", brokerDaemonStartArg, "existing-only"}, want: brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}},
254+ {name: "explicit if-needed", args: []string{"--production", brokerDaemonStartArg, "if-needed"}, want: brokerMuxOptions{startMode: ports.BrokerDaemonStartIfNeeded}},
255255 {name: "unknown flag", args: []string{"--production", "--idle-grace", "1m"}, wantErr: "unknown flag"},
256256 {name: "positional", args: []string{"--production", "extra"}, wantErr: "positional"},
257257 {name: "missing daemon-start value", args: []string{"--production", brokerDaemonStartArg}, wantErr: "requires a value"},
@@ -287,7 +287,6 @@ func TestBrokerMuxHelpersHiddenFromPublicHelp(t *testing.T) {
287287 command, err := parseArgs([]string{tt.name, "--production"})
288288 require.NoError(t, err)
289289 require.Equal(t, tt.kind, command.kind)
290- require.True(t, command.brokerMux.production)
291290 }
292291 // Ordinary commands are untouched by the new hidden entries.
293292 attach, err := parseArgs(nil)
@@ -412,23 +411,6 @@ func TestBrokerMuxHelperReturnsTypedNoDaemonExit(t *testing.T) {
412411 require.Equal(t, sshstdio.MuxExitNoDaemon, coded.code)
413412 }
414413
415-// TestBrokerMuxConnectorRejectsUnknownAddress proves the transport selector
416-// refuses any address the immutable configuration did not produce.
417-func TestBrokerMuxConnectorRejectsUnknownAddress(t *testing.T) {
418- emptyProductionBrokerLayout(t, "")
419- layout := writeMuxRoot(t, map[string]any{"kind": "unix", "path": "/tmp/unused-mux.sock"})
420- config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
421- require.NoError(t, err)
422- connector, err := brokerMuxConnector(config, slog.New(slog.NewTextHandler(io.Discard, nil)))
423- require.NoError(t, err)
424-
425- _, err = connector.Connect(context.Background(), ports.BrokerDialTarget{Fence: ports.BrokerEndpointFence{Local: true}, Policy: brokerTestPolicy(), Address: "offline-route-00000000000000000000000000000000", StartMode: ports.BrokerDaemonStartIfNeeded, ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: brokerTestIdentity, Bound: true}})
426- require.Error(t, err)
427- var typed ports.BrokerError
428- require.ErrorAs(t, err, &typed)
429- require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
430-}
431-
432414 // TestBrokerServeSSHStdioRouteRoundTrip drives the full broker path over an SSH
433415 // stdio route: the broker dials the fake ssh child, the hidden helper bridges
434416 // its stdio to a real unix daemonmux fixture, and a typed Ping returns a Pong.
@@ -659,7 +641,7 @@ func startMuxBrokerServe(t *testing.T) (string, func()) {
659641 t.Helper()
660642 deps, ready := testBrokerServeDeps(newSandboxClock())
661643 ctx, cancel := context.WithCancel(context.Background())
662- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
644+ done := runSandbox(ctx, brokerServeOptions{}, deps)
663645 socketPath := awaitSandboxReady(t, ready, done)
664646 var once bool
665647 stop := func() {
@@ -249,9 +249,9 @@ func TestParseBrokerMuxArgs(t *testing.T) {
249 wantErr string249 wantErr string
250 want brokerMuxOptions250 want brokerMuxOptions
251 }{251 }{
252- {name: "valid", args: []string{"--production"}, want: brokerMuxOptions{production: true, startMode: ports.BrokerDaemonExistingOnly}},252+ {name: "valid", args: []string{"--production"}, want: brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}},
253- {name: "explicit existing-only", args: []string{"--production", brokerDaemonStartArg, "existing-only"}, want: brokerMuxOptions{production: true, startMode: ports.BrokerDaemonExistingOnly}},253+ {name: "explicit existing-only", args: []string{"--production", brokerDaemonStartArg, "existing-only"}, want: brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly}},
254- {name: "explicit if-needed", args: []string{"--production", brokerDaemonStartArg, "if-needed"}, want: brokerMuxOptions{production: true, startMode: ports.BrokerDaemonStartIfNeeded}},254+ {name: "explicit if-needed", args: []string{"--production", brokerDaemonStartArg, "if-needed"}, want: brokerMuxOptions{startMode: ports.BrokerDaemonStartIfNeeded}},
255 {name: "unknown flag", args: []string{"--production", "--idle-grace", "1m"}, wantErr: "unknown flag"},255 {name: "unknown flag", args: []string{"--production", "--idle-grace", "1m"}, wantErr: "unknown flag"},
256 {name: "positional", args: []string{"--production", "extra"}, wantErr: "positional"},256 {name: "positional", args: []string{"--production", "extra"}, wantErr: "positional"},
257 {name: "missing daemon-start value", args: []string{"--production", brokerDaemonStartArg}, wantErr: "requires a value"},257 {name: "missing daemon-start value", args: []string{"--production", brokerDaemonStartArg}, wantErr: "requires a value"},
@@ -287,7 +287,6 @@ func TestBrokerMuxHelpersHiddenFromPublicHelp(t *testing.T) {
287 command, err := parseArgs([]string{tt.name, "--production"})287 command, err := parseArgs([]string{tt.name, "--production"})
288 require.NoError(t, err)288 require.NoError(t, err)
289 require.Equal(t, tt.kind, command.kind)289 require.Equal(t, tt.kind, command.kind)
290- require.True(t, command.brokerMux.production)
291 }290 }
292 // Ordinary commands are untouched by the new hidden entries.291 // Ordinary commands are untouched by the new hidden entries.
293 attach, err := parseArgs(nil)292 attach, err := parseArgs(nil)
@@ -412,23 +411,6 @@ func TestBrokerMuxHelperReturnsTypedNoDaemonExit(t *testing.T) {
412 require.Equal(t, sshstdio.MuxExitNoDaemon, coded.code)411 require.Equal(t, sshstdio.MuxExitNoDaemon, coded.code)
413 }412 }
414 413
415-// TestBrokerMuxConnectorRejectsUnknownAddress proves the transport selector
416-// refuses any address the immutable configuration did not produce.
417-func TestBrokerMuxConnectorRejectsUnknownAddress(t *testing.T) {
418- emptyProductionBrokerLayout(t, "")
419- layout := writeMuxRoot(t, map[string]any{"kind": "unix", "path": "/tmp/unused-mux.sock"})
420- config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
421- require.NoError(t, err)
422- connector, err := brokerMuxConnector(config, slog.New(slog.NewTextHandler(io.Discard, nil)))
423- require.NoError(t, err)
424-
425- _, err = connector.Connect(context.Background(), ports.BrokerDialTarget{Fence: ports.BrokerEndpointFence{Local: true}, Policy: brokerTestPolicy(), Address: "offline-route-00000000000000000000000000000000", StartMode: ports.BrokerDaemonStartIfNeeded, ExpectedIdentity: ports.BrokerExpectedIdentity{Identity: brokerTestIdentity, Bound: true}})
426- require.Error(t, err)
427- var typed ports.BrokerError
428- require.ErrorAs(t, err, &typed)
429- require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
430-}
431-
432 // TestBrokerServeSSHStdioRouteRoundTrip drives the full broker path over an SSH414 // TestBrokerServeSSHStdioRouteRoundTrip drives the full broker path over an SSH
433 // stdio route: the broker dials the fake ssh child, the hidden helper bridges415 // stdio route: the broker dials the fake ssh child, the hidden helper bridges
434 // its stdio to a real unix daemonmux fixture, and a typed Ping returns a Pong.416 // its stdio to a real unix daemonmux fixture, and a typed Ping returns a Pong.
@@ -659,7 +641,7 @@ func startMuxBrokerServe(t *testing.T) (string, func()) {
659 t.Helper()641 t.Helper()
660 deps, ready := testBrokerServeDeps(newSandboxClock())642 deps, ready := testBrokerServeDeps(newSandboxClock())
661 ctx, cancel := context.WithCancel(context.Background())643 ctx, cancel := context.WithCancel(context.Background())
662- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)644+ done := runSandbox(ctx, brokerServeOptions{}, deps)
663 socketPath := awaitSandboxReady(t, ready, done)645 socketPath := awaitSandboxReady(t, ready, done)
664 var once bool646 var once bool
665 stop := func() {647 stop := func() {
M internal/app/broker_offline_status.go +8 -264
@@ -3,7 +3,6 @@ package app
33 import (
44 "bytes"
55 "context"
6- "encoding/json"
76 "errors"
87 "fmt"
98 "io"
@@ -11,9 +10,7 @@ import (
1110 "net"
1211 "os"
1312 "os/exec"
14- "os/signal"
1513 "path/filepath"
16- "strings"
1714 "syscall"
1815 "time"
1916
@@ -57,14 +54,9 @@ import (
5754 // absolute overall deadline expires. A live-but-incompatible endpoint and a
5855 // stalled handshake are diagnosed without spawning.
5956
60-const (
61- // brokerLauncherCommand is the hidden intermediate that starts one detached
62- // `_broker-serve` and exits.
63- brokerLauncherCommand = "_broker-launcher"
64- productionBrokerLauncherCommand = "_broker-production-launcher"
65- // brokerStatusCommand is the hidden broker status probe.
66- brokerStatusCommand = "_broker-status"
67-)
57+// productionBrokerLauncherCommand is the hidden intermediate that starts one
58+// detached broker serve process and exits.
59+const productionBrokerLauncherCommand = "_broker-production-launcher"
6860
6961 // Bounds for the detached connect-or-spawn path. Every wait is bounded by the
7062 // one absolute overall deadline, and the first dial is bounded on its own so a
@@ -74,7 +66,6 @@ const (
7466 brokerStatusProbeBudget = 2 * time.Second
7567 brokerStatusInitialBackoff = 5 * time.Millisecond
7668 brokerStatusMaxBackoff = 50 * time.Millisecond
77- brokerStatusMaxReportBytes = 4 << 10
7869 )
7970
8071 var (
@@ -92,19 +83,6 @@ var (
9283 errBrokerNotReady = errors.New("vev: broker did not become ready")
9384 )
9485
95-// brokerLauncherOptions is the parsed `_broker-launcher` invocation.
96-type brokerLauncherOptions struct {
97- offlineRoot string
98- idleGrace time.Duration
99-}
100-
101-// brokerStatusOptions is the parsed `_broker-status` invocation.
102-type brokerStatusOptions struct {
103- offlineRoot string
104- ensure bool
105- timeout time.Duration
106-}
107-
10886 // brokerStatusReport is the bounded JSON status document. It carries exactly
10987 // the ready/offline state, the broker endpoint, and the broker's epoch,
11088 // revision, and host count. Epoch, revision, and host count are zero when the
@@ -154,14 +132,14 @@ func defaultBrokerStatusDeps() brokerStatusDeps {
154132 }
155133 return false, owner.Release()
156134 },
157- spawn: spawnBrokerLauncher,
135+ spawn: func(ctx context.Context, _ string, _ time.Duration, _ bool) error {
136+ return spawnProductionBrokerLauncher(ctx)
137+ },
158138 stdout: os.Stdout,
159139 }
160140 }
161141
162-// parseBrokerLauncherArgs strictly parses
163-// `_broker-launcher --offline-root ABS [--idle-grace DURATION]`. Unknown flags,
164-// duplicate flags, positionals, and non-positive durations are refused.
142+// parseProductionBrokerLauncherArgs parses the argument-free hidden launcher entry.
165143 func parseProductionBrokerLauncherArgs(args []string) (command, error) {
166144 if len(args) != 0 {
167145 return command{}, usagef("`%s` does not accept arguments", productionBrokerLauncherCommand)
@@ -169,141 +147,11 @@ func parseProductionBrokerLauncherArgs(args []string) (command, error) {
169147 return command{kind: kindProductionBrokerLauncher}, nil
170148 }
171149
172-func parseBrokerLauncherArgs(args []string) (command, error) {
173- var options brokerLauncherOptions
174- var seenRoot, seenGrace bool
175- for index := 0; index < len(args); index++ {
176- switch args[index] {
177- case "--offline-root":
178- if seenRoot {
179- return command{}, usagef("`%s` received duplicate --offline-root", brokerLauncherCommand)
180- }
181- if index+1 >= len(args) || args[index+1] == "" {
182- return command{}, usagef("`--offline-root` requires a path")
183- }
184- options.offlineRoot = args[index+1]
185- seenRoot = true
186- index++
187- case "--idle-grace":
188- if seenGrace {
189- return command{}, usagef("`%s` received duplicate --idle-grace", brokerLauncherCommand)
190- }
191- if index+1 >= len(args) || args[index+1] == "" {
192- return command{}, usagef("`--idle-grace` requires a duration")
193- }
194- grace, err := time.ParseDuration(args[index+1])
195- if err != nil {
196- return command{}, usagef("`--idle-grace` %q is not a duration", args[index+1])
197- }
198- if grace <= 0 {
199- return command{}, usagef("`--idle-grace` must be positive")
200- }
201- options.idleGrace = grace
202- seenGrace = true
203- index++
204- default:
205- if strings.HasPrefix(args[index], "-") {
206- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerLauncherCommand)
207- }
208- return command{}, usagef("`%s` does not accept positional arguments", brokerLauncherCommand)
209- }
210- }
211- if options.offlineRoot == "" {
212- return command{}, usagef("`%s` requires --offline-root", brokerLauncherCommand)
213- }
214- return command{kind: kindBrokerLauncher, brokerLauncher: options}, nil
215-}
216-
217-// parseBrokerStatusArgs strictly parses
218-// `_broker-status --offline-root ABS [--ensure] [--timeout DURATION]`. Unknown
219-// flags, duplicate flags, positionals, and non-positive timeouts are refused.
220-func parseBrokerStatusArgs(args []string) (command, error) {
221- options := brokerStatusOptions{timeout: brokerStatusDefaultTimeout}
222- var seenRoot, seenEnsure, seenTimeout bool
223- for index := 0; index < len(args); index++ {
224- switch args[index] {
225- case "--offline-root":
226- if seenRoot {
227- return command{}, usagef("`%s` received duplicate --offline-root", brokerStatusCommand)
228- }
229- if index+1 >= len(args) || args[index+1] == "" {
230- return command{}, usagef("`--offline-root` requires a path")
231- }
232- options.offlineRoot = args[index+1]
233- seenRoot = true
234- index++
235- case "--ensure":
236- if seenEnsure {
237- return command{}, usagef("`%s` received duplicate --ensure", brokerStatusCommand)
238- }
239- options.ensure = true
240- seenEnsure = true
241- case "--timeout":
242- if seenTimeout {
243- return command{}, usagef("`%s` received duplicate --timeout", brokerStatusCommand)
244- }
245- if index+1 >= len(args) || args[index+1] == "" {
246- return command{}, usagef("`--timeout` requires a duration")
247- }
248- timeout, err := time.ParseDuration(args[index+1])
249- if err != nil {
250- return command{}, usagef("`--timeout` %q is not a duration", args[index+1])
251- }
252- if timeout <= 0 {
253- return command{}, usagef("`--timeout` must be positive")
254- }
255- options.timeout = timeout
256- seenTimeout = true
257- index++
258- default:
259- if strings.HasPrefix(args[index], "-") {
260- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerStatusCommand)
261- }
262- return command{}, usagef("`%s` does not accept positional arguments", brokerStatusCommand)
263- }
264- }
265- if options.offlineRoot == "" {
266- return command{}, usagef("`%s` requires --offline-root", brokerStatusCommand)
267- }
268- return command{kind: kindBrokerStatus, brokerStatus: options}, nil
269-}
270-
271-// runBrokerLauncherCommand runs the hidden launcher. It changes no ordinary
272-// command and never runs in production flows.
150+// runProductionBrokerLauncherCommand starts one detached broker serve process.
273151 func runProductionBrokerLauncherCommand(context.Context) error {
274152 return startDetachedBrokerServe([]string{productionBrokerServeCommand}, filepath.Join(productionBrokerLayout().Log, "vev-broker-crash.log"))
275153 }
276154
277-func runBrokerLauncherCommand(_ context.Context, options brokerLauncherOptions) error {
278- return runBrokerLauncher(options)
279-}
280-
281-// runBrokerLauncher validates the sandbox configuration, diagnoses an explicit
282-// idle grace that conflicts with the provisioned one, then starts one detached
283-// `_broker-serve` in a new session and returns.
284-func runBrokerLauncher(options brokerLauncherOptions) error {
285- layout, err := offlineLayout(options.offlineRoot)
286- if err != nil {
287- return err
288- }
289- if err := safedir.EnsurePrivate(layout.Root); err != nil {
290- return fmt.Errorf("vev: secure broker sandbox root: %w", err)
291- }
292- config, err := brokerconfig.Load(layout)
293- if err != nil {
294- return err
295- }
296- grace, err := effectiveIdleGrace(config, options.idleGrace)
297- if err != nil {
298- return err
299- }
300- args := []string{brokerServeCommand, "--offline-root", options.offlineRoot}
301- if grace > 0 {
302- args = append(args, "--idle-grace", grace.String())
303- }
304- return startDetachedBrokerServe(args, filepath.Join(layout.Log, "vev-broker-crash.log"))
305-}
306-
307155 // startDetachedBrokerServe starts one `_broker-serve` in a new session and
308156 // releases it, so the broker outlives the short-lived launcher. It mirrors the
309157 // daemon launcher's double-fork boundary: no ambient variable is dropped beyond
@@ -358,14 +206,6 @@ func spawnProductionBrokerLauncher(ctx context.Context) error {
358206 return runBrokerLauncherProcess(ctx, []string{productionBrokerLauncherCommand})
359207 }
360208
361-func spawnBrokerLauncher(ctx context.Context, root string, grace time.Duration, graceSet bool) error {
362- args := []string{brokerLauncherCommand, "--offline-root", root}
363- if graceSet {
364- args = append(args, "--idle-grace", grace.String())
365- }
366- return runBrokerLauncherProcess(ctx, args)
367-}
368-
369209 func runBrokerLauncherProcess(ctx context.Context, args []string) error {
370210 exePath, err := selfExePath()
371211 if err != nil {
@@ -393,87 +233,6 @@ func runBrokerLauncherProcess(ctx context.Context, args []string) error {
393233 return nil
394234 }
395235
396-// runBrokerStatusCommand runs the hidden status probe, bounded by a signal-aware
397-// parent context.
398-func runBrokerStatusCommand(ctx context.Context, options brokerStatusOptions) error {
399- ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
400- defer stop()
401- return runBrokerStatus(ctx, options, defaultBrokerStatusDeps())
402-}
403-
404-// runBrokerStatus renders the broker status as one bounded JSON document.
405-//
406-// Without --ensure it is dial-only: it performs one probe bounded by
407-// --timeout and always succeeds, reporting ready or offline, so the JSON is the
408-// sole result and a caller never has to interpret an exit code to read a state.
409-// The timeout is the single absolute probe bound; it is never silently replaced
410-// by the ensure probe budget, so a caller can tighten it (or rely on the
411-// default) and observe the same bound. With --ensure it uses one absolute
412-// overall deadline; on success it prints the ready report and
413-// returns nil, and on any layout, configuration, or startup failure it still
414-// prints the offline report but returns a code-3 error so the caller always
415-// observes one offline JSON document and a distinct exit code.
416-func runBrokerStatus(ctx context.Context, options brokerStatusOptions, deps brokerStatusDeps) error {
417- layout, err := offlineLayout(options.offlineRoot)
418- if err != nil {
419- if options.ensure {
420- return ensureOfflineFailure(deps.stdout, brokerStatusReport{Status: "offline"}, err)
421- }
422- return err
423- }
424- socketPath := brokeripc.SocketPath(layout.Runtime)
425- offline := brokerStatusReport{Status: "offline", Endpoint: socketPath}
426-
427- if !options.ensure {
428- probeCtx, cancel := context.WithTimeout(ctx, options.timeout)
429- defer cancel()
430- report, err := deps.probe(probeCtx, socketPath)
431- if err != nil {
432- // Dial-only renders the observable state: absence and an
433- // incompatible live endpoint both read as offline.
434- return writeBrokerStatusReport(deps.stdout, offline)
435- }
436- return writeBrokerStatusReport(deps.stdout, report)
437- }
438-
439- // Validate and secure the sandbox root exactly as the launcher does before
440- // the spawn directory is created or any lock is taken, so status --ensure can
441- // never operate on paths the launcher would refuse.
442- if err := safedir.EnsurePrivate(layout.Root); err != nil {
443- return ensureOfflineFailure(deps.stdout, offline, fmt.Errorf("vev: secure broker sandbox root: %w", err))
444- }
445- config, err := brokerconfig.Load(layout)
446- if err != nil {
447- return ensureOfflineFailure(deps.stdout, offline, err)
448- }
449- grace, graceSet := config.IdleGrace()
450-
451- deadline := deps.now().Add(options.timeout)
452- report, err := ensureBrokerReady(ctx, brokerStatusRequest{
453- layout: layout,
454- socketPath: socketPath,
455- deadline: deadline,
456- timeout: options.timeout,
457- root: options.offlineRoot,
458- grace: grace,
459- graceSet: graceSet,
460- }, deps)
461- if err != nil {
462- return ensureOfflineFailure(deps.stdout, offline, err)
463- }
464- return writeBrokerStatusReport(deps.stdout, report)
465-}
466-
467-// ensureOfflineFailure renders one offline report and returns the ensure failure
468-// wrapped in exit code 3, so a layout, configuration, or startup failure always
469-// leaves the caller with the offline JSON document and a distinct exit code.
470-func ensureOfflineFailure(stdout io.Writer, offline brokerStatusReport, err error) error {
471- if writeErr := writeBrokerStatusReport(stdout, offline); writeErr != nil {
472- return errors.Join(err, writeErr)
473- }
474- return &exitCoded{code: 3, err: err}
475-}
476-
477236 // brokerStatusRequest carries one ensure run's fixed inputs.
478237 type brokerStatusRequest struct {
479238 layout brokerconfig.Layout
@@ -710,18 +469,3 @@ func absentError(socketPath string) error {
710469 func incompatibleError(socketPath string, cause error) error {
711470 return fmt.Errorf("%w: %s: %w", errBrokerIncompatible, socketPath, cause)
712471 }
713-
714-// writeBrokerStatusReport writes exactly one bounded JSON document.
715-func writeBrokerStatusReport(w io.Writer, report brokerStatusReport) error {
716- data, err := json.Marshal(report)
717- if err != nil {
718- return fmt.Errorf("vev: encode broker status: %w", err)
719- }
720- if len(data) > brokerStatusMaxReportBytes {
721- return errors.New("vev: broker status report exceeds the bound")
722- }
723- if _, err := w.Write(append(data, '\n')); err != nil {
724- return fmt.Errorf("vev: write broker status: %w", err)
725- }
726- return nil
727-}
@@ -3,7 +3,6 @@ package app
3 import (3 import (
4 "bytes"4 "bytes"
5 "context"5 "context"
6- "encoding/json"
7 "errors"6 "errors"
8 "fmt"7 "fmt"
9 "io"8 "io"
@@ -11,9 +10,7 @@ import (
11 "net"10 "net"
12 "os"11 "os"
13 "os/exec"12 "os/exec"
14- "os/signal"
15 "path/filepath"13 "path/filepath"
16- "strings"
17 "syscall"14 "syscall"
18 "time"15 "time"
19 16
@@ -57,14 +54,9 @@ import (
57 // absolute overall deadline expires. A live-but-incompatible endpoint and a54 // absolute overall deadline expires. A live-but-incompatible endpoint and a
58 // stalled handshake are diagnosed without spawning.55 // stalled handshake are diagnosed without spawning.
59 56
60-const (57+// productionBrokerLauncherCommand is the hidden intermediate that starts one
61- // brokerLauncherCommand is the hidden intermediate that starts one detached58+// detached broker serve process and exits.
62- // `_broker-serve` and exits.59+const productionBrokerLauncherCommand = "_broker-production-launcher"
63- brokerLauncherCommand = "_broker-launcher"
64- productionBrokerLauncherCommand = "_broker-production-launcher"
65- // brokerStatusCommand is the hidden broker status probe.
66- brokerStatusCommand = "_broker-status"
67-)
68 60
69 // Bounds for the detached connect-or-spawn path. Every wait is bounded by the61 // Bounds for the detached connect-or-spawn path. Every wait is bounded by the
70 // one absolute overall deadline, and the first dial is bounded on its own so a62 // one absolute overall deadline, and the first dial is bounded on its own so a
@@ -74,7 +66,6 @@ const (
74 brokerStatusProbeBudget = 2 * time.Second66 brokerStatusProbeBudget = 2 * time.Second
75 brokerStatusInitialBackoff = 5 * time.Millisecond67 brokerStatusInitialBackoff = 5 * time.Millisecond
76 brokerStatusMaxBackoff = 50 * time.Millisecond68 brokerStatusMaxBackoff = 50 * time.Millisecond
77- brokerStatusMaxReportBytes = 4 << 10
78 )69 )
79 70
80 var (71 var (
@@ -92,19 +83,6 @@ var (
92 errBrokerNotReady = errors.New("vev: broker did not become ready")83 errBrokerNotReady = errors.New("vev: broker did not become ready")
93 )84 )
94 85
95-// brokerLauncherOptions is the parsed `_broker-launcher` invocation.
96-type brokerLauncherOptions struct {
97- offlineRoot string
98- idleGrace time.Duration
99-}
100-
101-// brokerStatusOptions is the parsed `_broker-status` invocation.
102-type brokerStatusOptions struct {
103- offlineRoot string
104- ensure bool
105- timeout time.Duration
106-}
107-
108 // brokerStatusReport is the bounded JSON status document. It carries exactly86 // brokerStatusReport is the bounded JSON status document. It carries exactly
109 // the ready/offline state, the broker endpoint, and the broker's epoch,87 // the ready/offline state, the broker endpoint, and the broker's epoch,
110 // revision, and host count. Epoch, revision, and host count are zero when the88 // revision, and host count. Epoch, revision, and host count are zero when the
@@ -154,14 +132,14 @@ func defaultBrokerStatusDeps() brokerStatusDeps {
154 }132 }
155 return false, owner.Release()133 return false, owner.Release()
156 },134 },
157- spawn: spawnBrokerLauncher,135+ spawn: func(ctx context.Context, _ string, _ time.Duration, _ bool) error {
136+ return spawnProductionBrokerLauncher(ctx)
137+ },
158 stdout: os.Stdout,138 stdout: os.Stdout,
159 }139 }
160 }140 }
161 141
162-// parseBrokerLauncherArgs strictly parses142+// parseProductionBrokerLauncherArgs parses the argument-free hidden launcher entry.
163-// `_broker-launcher --offline-root ABS [--idle-grace DURATION]`. Unknown flags,
164-// duplicate flags, positionals, and non-positive durations are refused.
165 func parseProductionBrokerLauncherArgs(args []string) (command, error) {143 func parseProductionBrokerLauncherArgs(args []string) (command, error) {
166 if len(args) != 0 {144 if len(args) != 0 {
167 return command{}, usagef("`%s` does not accept arguments", productionBrokerLauncherCommand)145 return command{}, usagef("`%s` does not accept arguments", productionBrokerLauncherCommand)
@@ -169,141 +147,11 @@ func parseProductionBrokerLauncherArgs(args []string) (command, error) {
169 return command{kind: kindProductionBrokerLauncher}, nil147 return command{kind: kindProductionBrokerLauncher}, nil
170 }148 }
171 149
172-func parseBrokerLauncherArgs(args []string) (command, error) {150+// runProductionBrokerLauncherCommand starts one detached broker serve process.
173- var options brokerLauncherOptions
174- var seenRoot, seenGrace bool
175- for index := 0; index < len(args); index++ {
176- switch args[index] {
177- case "--offline-root":
178- if seenRoot {
179- return command{}, usagef("`%s` received duplicate --offline-root", brokerLauncherCommand)
180- }
181- if index+1 >= len(args) || args[index+1] == "" {
182- return command{}, usagef("`--offline-root` requires a path")
183- }
184- options.offlineRoot = args[index+1]
185- seenRoot = true
186- index++
187- case "--idle-grace":
188- if seenGrace {
189- return command{}, usagef("`%s` received duplicate --idle-grace", brokerLauncherCommand)
190- }
191- if index+1 >= len(args) || args[index+1] == "" {
192- return command{}, usagef("`--idle-grace` requires a duration")
193- }
194- grace, err := time.ParseDuration(args[index+1])
195- if err != nil {
196- return command{}, usagef("`--idle-grace` %q is not a duration", args[index+1])
197- }
198- if grace <= 0 {
199- return command{}, usagef("`--idle-grace` must be positive")
200- }
201- options.idleGrace = grace
202- seenGrace = true
203- index++
204- default:
205- if strings.HasPrefix(args[index], "-") {
206- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerLauncherCommand)
207- }
208- return command{}, usagef("`%s` does not accept positional arguments", brokerLauncherCommand)
209- }
210- }
211- if options.offlineRoot == "" {
212- return command{}, usagef("`%s` requires --offline-root", brokerLauncherCommand)
213- }
214- return command{kind: kindBrokerLauncher, brokerLauncher: options}, nil
215-}
216-
217-// parseBrokerStatusArgs strictly parses
218-// `_broker-status --offline-root ABS [--ensure] [--timeout DURATION]`. Unknown
219-// flags, duplicate flags, positionals, and non-positive timeouts are refused.
220-func parseBrokerStatusArgs(args []string) (command, error) {
221- options := brokerStatusOptions{timeout: brokerStatusDefaultTimeout}
222- var seenRoot, seenEnsure, seenTimeout bool
223- for index := 0; index < len(args); index++ {
224- switch args[index] {
225- case "--offline-root":
226- if seenRoot {
227- return command{}, usagef("`%s` received duplicate --offline-root", brokerStatusCommand)
228- }
229- if index+1 >= len(args) || args[index+1] == "" {
230- return command{}, usagef("`--offline-root` requires a path")
231- }
232- options.offlineRoot = args[index+1]
233- seenRoot = true
234- index++
235- case "--ensure":
236- if seenEnsure {
237- return command{}, usagef("`%s` received duplicate --ensure", brokerStatusCommand)
238- }
239- options.ensure = true
240- seenEnsure = true
241- case "--timeout":
242- if seenTimeout {
243- return command{}, usagef("`%s` received duplicate --timeout", brokerStatusCommand)
244- }
245- if index+1 >= len(args) || args[index+1] == "" {
246- return command{}, usagef("`--timeout` requires a duration")
247- }
248- timeout, err := time.ParseDuration(args[index+1])
249- if err != nil {
250- return command{}, usagef("`--timeout` %q is not a duration", args[index+1])
251- }
252- if timeout <= 0 {
253- return command{}, usagef("`--timeout` must be positive")
254- }
255- options.timeout = timeout
256- seenTimeout = true
257- index++
258- default:
259- if strings.HasPrefix(args[index], "-") {
260- return command{}, usagef("unknown flag %q for `%s`", args[index], brokerStatusCommand)
261- }
262- return command{}, usagef("`%s` does not accept positional arguments", brokerStatusCommand)
263- }
264- }
265- if options.offlineRoot == "" {
266- return command{}, usagef("`%s` requires --offline-root", brokerStatusCommand)
267- }
268- return command{kind: kindBrokerStatus, brokerStatus: options}, nil
269-}
270-
271-// runBrokerLauncherCommand runs the hidden launcher. It changes no ordinary
272-// command and never runs in production flows.
273 func runProductionBrokerLauncherCommand(context.Context) error {151 func runProductionBrokerLauncherCommand(context.Context) error {
274 return startDetachedBrokerServe([]string{productionBrokerServeCommand}, filepath.Join(productionBrokerLayout().Log, "vev-broker-crash.log"))152 return startDetachedBrokerServe([]string{productionBrokerServeCommand}, filepath.Join(productionBrokerLayout().Log, "vev-broker-crash.log"))
275 }153 }
276 154
277-func runBrokerLauncherCommand(_ context.Context, options brokerLauncherOptions) error {
278- return runBrokerLauncher(options)
279-}
280-
281-// runBrokerLauncher validates the sandbox configuration, diagnoses an explicit
282-// idle grace that conflicts with the provisioned one, then starts one detached
283-// `_broker-serve` in a new session and returns.
284-func runBrokerLauncher(options brokerLauncherOptions) error {
285- layout, err := offlineLayout(options.offlineRoot)
286- if err != nil {
287- return err
288- }
289- if err := safedir.EnsurePrivate(layout.Root); err != nil {
290- return fmt.Errorf("vev: secure broker sandbox root: %w", err)
291- }
292- config, err := brokerconfig.Load(layout)
293- if err != nil {
294- return err
295- }
296- grace, err := effectiveIdleGrace(config, options.idleGrace)
297- if err != nil {
298- return err
299- }
300- args := []string{brokerServeCommand, "--offline-root", options.offlineRoot}
301- if grace > 0 {
302- args = append(args, "--idle-grace", grace.String())
303- }
304- return startDetachedBrokerServe(args, filepath.Join(layout.Log, "vev-broker-crash.log"))
305-}
306-
307 // startDetachedBrokerServe starts one `_broker-serve` in a new session and155 // startDetachedBrokerServe starts one `_broker-serve` in a new session and
308 // releases it, so the broker outlives the short-lived launcher. It mirrors the156 // releases it, so the broker outlives the short-lived launcher. It mirrors the
309 // daemon launcher's double-fork boundary: no ambient variable is dropped beyond157 // daemon launcher's double-fork boundary: no ambient variable is dropped beyond
@@ -358,14 +206,6 @@ func spawnProductionBrokerLauncher(ctx context.Context) error {
358 return runBrokerLauncherProcess(ctx, []string{productionBrokerLauncherCommand})206 return runBrokerLauncherProcess(ctx, []string{productionBrokerLauncherCommand})
359 }207 }
360 208
361-func spawnBrokerLauncher(ctx context.Context, root string, grace time.Duration, graceSet bool) error {
362- args := []string{brokerLauncherCommand, "--offline-root", root}
363- if graceSet {
364- args = append(args, "--idle-grace", grace.String())
365- }
366- return runBrokerLauncherProcess(ctx, args)
367-}
368-
369 func runBrokerLauncherProcess(ctx context.Context, args []string) error {209 func runBrokerLauncherProcess(ctx context.Context, args []string) error {
370 exePath, err := selfExePath()210 exePath, err := selfExePath()
371 if err != nil {211 if err != nil {
@@ -393,87 +233,6 @@ func runBrokerLauncherProcess(ctx context.Context, args []string) error {
393 return nil233 return nil
394 }234 }
395 235
396-// runBrokerStatusCommand runs the hidden status probe, bounded by a signal-aware
397-// parent context.
398-func runBrokerStatusCommand(ctx context.Context, options brokerStatusOptions) error {
399- ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
400- defer stop()
401- return runBrokerStatus(ctx, options, defaultBrokerStatusDeps())
402-}
403-
404-// runBrokerStatus renders the broker status as one bounded JSON document.
405-//
406-// Without --ensure it is dial-only: it performs one probe bounded by
407-// --timeout and always succeeds, reporting ready or offline, so the JSON is the
408-// sole result and a caller never has to interpret an exit code to read a state.
409-// The timeout is the single absolute probe bound; it is never silently replaced
410-// by the ensure probe budget, so a caller can tighten it (or rely on the
411-// default) and observe the same bound. With --ensure it uses one absolute
412-// overall deadline; on success it prints the ready report and
413-// returns nil, and on any layout, configuration, or startup failure it still
414-// prints the offline report but returns a code-3 error so the caller always
415-// observes one offline JSON document and a distinct exit code.
416-func runBrokerStatus(ctx context.Context, options brokerStatusOptions, deps brokerStatusDeps) error {
417- layout, err := offlineLayout(options.offlineRoot)
418- if err != nil {
419- if options.ensure {
420- return ensureOfflineFailure(deps.stdout, brokerStatusReport{Status: "offline"}, err)
421- }
422- return err
423- }
424- socketPath := brokeripc.SocketPath(layout.Runtime)
425- offline := brokerStatusReport{Status: "offline", Endpoint: socketPath}
426-
427- if !options.ensure {
428- probeCtx, cancel := context.WithTimeout(ctx, options.timeout)
429- defer cancel()
430- report, err := deps.probe(probeCtx, socketPath)
431- if err != nil {
432- // Dial-only renders the observable state: absence and an
433- // incompatible live endpoint both read as offline.
434- return writeBrokerStatusReport(deps.stdout, offline)
435- }
436- return writeBrokerStatusReport(deps.stdout, report)
437- }
438-
439- // Validate and secure the sandbox root exactly as the launcher does before
440- // the spawn directory is created or any lock is taken, so status --ensure can
441- // never operate on paths the launcher would refuse.
442- if err := safedir.EnsurePrivate(layout.Root); err != nil {
443- return ensureOfflineFailure(deps.stdout, offline, fmt.Errorf("vev: secure broker sandbox root: %w", err))
444- }
445- config, err := brokerconfig.Load(layout)
446- if err != nil {
447- return ensureOfflineFailure(deps.stdout, offline, err)
448- }
449- grace, graceSet := config.IdleGrace()
450-
451- deadline := deps.now().Add(options.timeout)
452- report, err := ensureBrokerReady(ctx, brokerStatusRequest{
453- layout: layout,
454- socketPath: socketPath,
455- deadline: deadline,
456- timeout: options.timeout,
457- root: options.offlineRoot,
458- grace: grace,
459- graceSet: graceSet,
460- }, deps)
461- if err != nil {
462- return ensureOfflineFailure(deps.stdout, offline, err)
463- }
464- return writeBrokerStatusReport(deps.stdout, report)
465-}
466-
467-// ensureOfflineFailure renders one offline report and returns the ensure failure
468-// wrapped in exit code 3, so a layout, configuration, or startup failure always
469-// leaves the caller with the offline JSON document and a distinct exit code.
470-func ensureOfflineFailure(stdout io.Writer, offline brokerStatusReport, err error) error {
471- if writeErr := writeBrokerStatusReport(stdout, offline); writeErr != nil {
472- return errors.Join(err, writeErr)
473- }
474- return &exitCoded{code: 3, err: err}
475-}
476-
477 // brokerStatusRequest carries one ensure run's fixed inputs.236 // brokerStatusRequest carries one ensure run's fixed inputs.
478 type brokerStatusRequest struct {237 type brokerStatusRequest struct {
479 layout brokerconfig.Layout238 layout brokerconfig.Layout
@@ -710,18 +469,3 @@ func absentError(socketPath string) error {
710 func incompatibleError(socketPath string, cause error) error {469 func incompatibleError(socketPath string, cause error) error {
711 return fmt.Errorf("%w: %s: %w", errBrokerIncompatible, socketPath, cause)470 return fmt.Errorf("%w: %s: %w", errBrokerIncompatible, socketPath, cause)
712 }471 }
713-
714-// writeBrokerStatusReport writes exactly one bounded JSON document.
715-func writeBrokerStatusReport(w io.Writer, report brokerStatusReport) error {
716- data, err := json.Marshal(report)
717- if err != nil {
718- return fmt.Errorf("vev: encode broker status: %w", err)
719- }
720- if len(data) > brokerStatusMaxReportBytes {
721- return errors.New("vev: broker status report exceeds the bound")
722- }
723- if _, err := w.Write(append(data, '\n')); err != nil {
724- return fmt.Errorf("vev: write broker status: %w", err)
725- }
726- return nil
727-}
M internal/app/broker_offline_status_test.go +2 -14
@@ -7,7 +7,6 @@ import (
77 "net"
88 "os"
99 "path/filepath"
10- "strings"
1110 "sync/atomic"
1211 "testing"
1312 "time"
@@ -119,17 +118,6 @@ func (r *statusRecorder) setProbe(deps *brokerStatusDeps, probe func(attempt int
119118 }
120119 }
121120
122-// decodeOneReport remains shared with the subprocess broker tests until those
123-// are converted to the production launcher.
124-func decodeOneReport(t *testing.T, raw string) brokerStatusReport {
125- t.Helper()
126- trimmed := strings.TrimSpace(raw)
127- require.Equal(t, 1, strings.Count(trimmed, "\n")+1)
128- var report brokerStatusReport
129- require.NoError(t, json.Unmarshal([]byte(trimmed), &report))
130- return report
131-}
132-
133121 func TestEffectiveIdleGrace(t *testing.T) {
134122 layout := emptyProductionBrokerLayout(t, "90s")
135123 config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
@@ -170,7 +158,7 @@ func TestBrokerServeRefusesConflictingIdleGrace(t *testing.T) {
170158 ctx, cancel := context.WithCancel(context.Background())
171159 defer cancel()
172160 deps, _ := testBrokerServeDeps(newSandboxClock())
173- err := runBrokerServe(ctx, brokerServeOptions{production: true, idleGrace: time.Minute}, deps)
161+ err := runBrokerServe(ctx, brokerServeOptions{idleGrace: time.Minute}, deps)
174162 require.Error(t, err)
175163 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")
176164 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"), "a refused serve must not take the lifetime lock")
@@ -310,7 +298,7 @@ func TestBrokerEnsureProductionConfigFailure(t *testing.T) {
310298 layout := emptyProductionBrokerLayout(t, "")
311299 require.NoError(t, os.WriteFile(productionBrokerConfigPath(), []byte("not json"), 0o600))
312300 deps, _ := testBrokerServeDeps(newSandboxClock())
313- err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps)
301+ err := runBrokerServe(context.Background(), brokerServeOptions{}, deps)
314302 require.Error(t, err)
315303 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))
316304 }
@@ -7,7 +7,6 @@ import (
7 "net"7 "net"
8 "os"8 "os"
9 "path/filepath"9 "path/filepath"
10- "strings"
11 "sync/atomic"10 "sync/atomic"
12 "testing"11 "testing"
13 "time"12 "time"
@@ -119,17 +118,6 @@ func (r *statusRecorder) setProbe(deps *brokerStatusDeps, probe func(attempt int
119 }118 }
120 }119 }
121 120
122-// decodeOneReport remains shared with the subprocess broker tests until those
123-// are converted to the production launcher.
124-func decodeOneReport(t *testing.T, raw string) brokerStatusReport {
125- t.Helper()
126- trimmed := strings.TrimSpace(raw)
127- require.Equal(t, 1, strings.Count(trimmed, "\n")+1)
128- var report brokerStatusReport
129- require.NoError(t, json.Unmarshal([]byte(trimmed), &report))
130- return report
131-}
132-
133 func TestEffectiveIdleGrace(t *testing.T) {121 func TestEffectiveIdleGrace(t *testing.T) {
134 layout := emptyProductionBrokerLayout(t, "90s")122 layout := emptyProductionBrokerLayout(t, "90s")
135 config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))123 config, err := brokerconfig.LoadProduction(layout, productionBrokerConfigPath(), "", localDaemonPolicy(), daemonmux.SocketPath(ipc.SocketDir()))
@@ -170,7 +158,7 @@ func TestBrokerServeRefusesConflictingIdleGrace(t *testing.T) {
170 ctx, cancel := context.WithCancel(context.Background())158 ctx, cancel := context.WithCancel(context.Background())
171 defer cancel()159 defer cancel()
172 deps, _ := testBrokerServeDeps(newSandboxClock())160 deps, _ := testBrokerServeDeps(newSandboxClock())
173- err := runBrokerServe(ctx, brokerServeOptions{production: true, idleGrace: time.Minute}, deps)161+ err := runBrokerServe(ctx, brokerServeOptions{idleGrace: time.Minute}, deps)
174 require.Error(t, err)162 require.Error(t, err)
175 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")163 require.Contains(t, err.Error(), "conflicts with the provisioned idle grace")
176 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"), "a refused serve must not take the lifetime lock")164 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"), "a refused serve must not take the lifetime lock")
@@ -310,7 +298,7 @@ func TestBrokerEnsureProductionConfigFailure(t *testing.T) {
310 layout := emptyProductionBrokerLayout(t, "")298 layout := emptyProductionBrokerLayout(t, "")
311 require.NoError(t, os.WriteFile(productionBrokerConfigPath(), []byte("not json"), 0o600))299 require.NoError(t, os.WriteFile(productionBrokerConfigPath(), []byte("not json"), 0o600))
312 deps, _ := testBrokerServeDeps(newSandboxClock())300 deps, _ := testBrokerServeDeps(newSandboxClock())
313- err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps)301+ err := runBrokerServe(context.Background(), brokerServeOptions{}, deps)
314 require.Error(t, err)302 require.Error(t, err)
315 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))303 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))
316 }304 }
M internal/app/broker_offline_test.go +7 -26
@@ -86,14 +86,6 @@ func sandboxRegistrationDocument(route any) map[string]any {
8686 }
8787 }
8888
89-// writeSandboxConfig marshals one document into root/config.json owner-only.
90-func writeSandboxConfig(t *testing.T, root string, document any) {
91- t.Helper()
92- raw, err := json.Marshal(document)
93- require.NoError(t, err)
94- require.NoError(t, os.WriteFile(filepath.Join(root, brokerconfig.ConfigFileName), raw, 0o600))
95-}
96-
9789 // isolateSandboxEnv redirects production XDG roots into private temporary
9890 // directories and returns one fresh offline root beside them, so the sandbox
9991 // can never touch the real runtime or state paths.
@@ -109,17 +101,6 @@ func isolateSandboxEnv(t *testing.T) (root, prodRuntime, prodState string) {
109101 return root, prodRuntime, prodState
110102 }
111103
112-// requireProductionUntouched asserts no production runtime or state entry was
113-// created.
114-func requireProductionUntouched(t *testing.T, prodRuntime, prodState string) {
115- t.Helper()
116- for _, dir := range []string{prodRuntime, prodState} {
117- entries, err := os.ReadDir(dir)
118- require.NoError(t, err)
119- require.Empty(t, entries, "production directory %s was touched", dir)
120- }
121-}
122-
123104 // testBrokerServeDeps builds sandbox deps with a deterministic clock and a
124105 // discard logger, and reports readiness on the returned channel.
125106 func testBrokerServeDeps(clk ports.Clock) (brokerServeDeps, <-chan string) {
@@ -219,7 +200,7 @@ func TestProductionBrokerServeRejectsUnsafeRoot(t *testing.T) {
219200 layout := emptyProductionBrokerLayout(t, "")
220201 require.NoError(t, os.MkdirAll(layout.Root, 0o755))
221202 deps, _ := testBrokerServeDeps(newSandboxClock())
222- err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps)
203+ err := runBrokerServe(context.Background(), brokerServeOptions{}, deps)
223204 require.ErrorContains(t, err, "0755")
224205 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))
225206 }
@@ -234,7 +215,7 @@ func TestBrokerServeRoundTrip(t *testing.T) {
234215 deps, ready := testBrokerServeDeps(clk)
235216 ctx, cancel := context.WithCancel(context.Background())
236217 defer cancel()
237- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
218+ done := runSandbox(ctx, brokerServeOptions{}, deps)
238219
239220 socketPath := awaitSandboxReady(t, ready, done)
240221
@@ -340,7 +321,7 @@ func TestBrokerServeIdleShutdownOnEmptyConfig(t *testing.T) {
340321 deps, ready := testBrokerServeDeps(clk)
341322 ctx, cancel := context.WithCancel(context.Background())
342323 defer cancel()
343- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
324+ done := runSandbox(ctx, brokerServeOptions{}, deps)
344325
345326 socketPath := awaitSandboxReady(t, ready, done)
346327 waitForSandboxShutdown(t, clk, done)
@@ -365,7 +346,7 @@ func TestBrokerServeCancelCleanup(t *testing.T) {
365346 deps, ready := testBrokerServeDeps(clk)
366347 ctx, cancel := context.WithCancel(context.Background())
367348 defer cancel()
368- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
349+ done := runSandbox(ctx, brokerServeOptions{}, deps)
369350
370351 socketPath := awaitSandboxReady(t, ready, done)
371352 awaitSandboxCancel(t, cancel, done)
@@ -407,7 +388,7 @@ func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) {
407388 }
408389 ctx, cancel := context.WithCancel(context.Background())
409390 defer cancel()
410- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
391+ done := runSandbox(ctx, brokerServeOptions{}, deps)
411392
412393 awaitSandboxReady(t, ready, done)
413394 select {
@@ -428,12 +409,12 @@ func TestBrokerServeRefusesDuplicateOwnership(t *testing.T) {
428409 deps, ready := testBrokerServeDeps(clk)
429410 ctx, cancel := context.WithCancel(context.Background())
430411 defer cancel()
431- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
412+ done := runSandbox(ctx, brokerServeOptions{}, deps)
432413 socketPath := awaitSandboxReady(t, ready, done)
433414
434415 // A second foreground owner fails fast on the lifetime lock.
435416 secondDeps, _ := testBrokerServeDeps(newSandboxClock())
436- err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, secondDeps)
417+ err := runBrokerServe(context.Background(), brokerServeOptions{}, secondDeps)
437418 require.ErrorIs(t, err, lifecycle.ErrBusy)
438419
439420 // A duplicate listener on the live socket is refused as well.
@@ -86,14 +86,6 @@ func sandboxRegistrationDocument(route any) map[string]any {
86 }86 }
87 }87 }
88 88
89-// writeSandboxConfig marshals one document into root/config.json owner-only.
90-func writeSandboxConfig(t *testing.T, root string, document any) {
91- t.Helper()
92- raw, err := json.Marshal(document)
93- require.NoError(t, err)
94- require.NoError(t, os.WriteFile(filepath.Join(root, brokerconfig.ConfigFileName), raw, 0o600))
95-}
96-
97 // isolateSandboxEnv redirects production XDG roots into private temporary89 // isolateSandboxEnv redirects production XDG roots into private temporary
98 // directories and returns one fresh offline root beside them, so the sandbox90 // directories and returns one fresh offline root beside them, so the sandbox
99 // can never touch the real runtime or state paths.91 // can never touch the real runtime or state paths.
@@ -109,17 +101,6 @@ func isolateSandboxEnv(t *testing.T) (root, prodRuntime, prodState string) {
109 return root, prodRuntime, prodState101 return root, prodRuntime, prodState
110 }102 }
111 103
112-// requireProductionUntouched asserts no production runtime or state entry was
113-// created.
114-func requireProductionUntouched(t *testing.T, prodRuntime, prodState string) {
115- t.Helper()
116- for _, dir := range []string{prodRuntime, prodState} {
117- entries, err := os.ReadDir(dir)
118- require.NoError(t, err)
119- require.Empty(t, entries, "production directory %s was touched", dir)
120- }
121-}
122-
123 // testBrokerServeDeps builds sandbox deps with a deterministic clock and a104 // testBrokerServeDeps builds sandbox deps with a deterministic clock and a
124 // discard logger, and reports readiness on the returned channel.105 // discard logger, and reports readiness on the returned channel.
125 func testBrokerServeDeps(clk ports.Clock) (brokerServeDeps, <-chan string) {106 func testBrokerServeDeps(clk ports.Clock) (brokerServeDeps, <-chan string) {
@@ -219,7 +200,7 @@ func TestProductionBrokerServeRejectsUnsafeRoot(t *testing.T) {
219 layout := emptyProductionBrokerLayout(t, "")200 layout := emptyProductionBrokerLayout(t, "")
220 require.NoError(t, os.MkdirAll(layout.Root, 0o755))201 require.NoError(t, os.MkdirAll(layout.Root, 0o755))
221 deps, _ := testBrokerServeDeps(newSandboxClock())202 deps, _ := testBrokerServeDeps(newSandboxClock())
222- err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, deps)203+ err := runBrokerServe(context.Background(), brokerServeOptions{}, deps)
223 require.ErrorContains(t, err, "0755")204 require.ErrorContains(t, err, "0755")
224 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))205 require.NoFileExists(t, filepath.Join(layout.Runtime, "lifecycle.lock"))
225 }206 }
@@ -234,7 +215,7 @@ func TestBrokerServeRoundTrip(t *testing.T) {
234 deps, ready := testBrokerServeDeps(clk)215 deps, ready := testBrokerServeDeps(clk)
235 ctx, cancel := context.WithCancel(context.Background())216 ctx, cancel := context.WithCancel(context.Background())
236 defer cancel()217 defer cancel()
237- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)218+ done := runSandbox(ctx, brokerServeOptions{}, deps)
238 219
239 socketPath := awaitSandboxReady(t, ready, done)220 socketPath := awaitSandboxReady(t, ready, done)
240 221
@@ -340,7 +321,7 @@ func TestBrokerServeIdleShutdownOnEmptyConfig(t *testing.T) {
340 deps, ready := testBrokerServeDeps(clk)321 deps, ready := testBrokerServeDeps(clk)
341 ctx, cancel := context.WithCancel(context.Background())322 ctx, cancel := context.WithCancel(context.Background())
342 defer cancel()323 defer cancel()
343- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)324+ done := runSandbox(ctx, brokerServeOptions{}, deps)
344 325
345 socketPath := awaitSandboxReady(t, ready, done)326 socketPath := awaitSandboxReady(t, ready, done)
346 waitForSandboxShutdown(t, clk, done)327 waitForSandboxShutdown(t, clk, done)
@@ -365,7 +346,7 @@ func TestBrokerServeCancelCleanup(t *testing.T) {
365 deps, ready := testBrokerServeDeps(clk)346 deps, ready := testBrokerServeDeps(clk)
366 ctx, cancel := context.WithCancel(context.Background())347 ctx, cancel := context.WithCancel(context.Background())
367 defer cancel()348 defer cancel()
368- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)349+ done := runSandbox(ctx, brokerServeOptions{}, deps)
369 350
370 socketPath := awaitSandboxReady(t, ready, done)351 socketPath := awaitSandboxReady(t, ready, done)
371 awaitSandboxCancel(t, cancel, done)352 awaitSandboxCancel(t, cancel, done)
@@ -407,7 +388,7 @@ func TestBrokerServeShutsDownOnUnexpectedAcceptFailure(t *testing.T) {
407 }388 }
408 ctx, cancel := context.WithCancel(context.Background())389 ctx, cancel := context.WithCancel(context.Background())
409 defer cancel()390 defer cancel()
410- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)391+ done := runSandbox(ctx, brokerServeOptions{}, deps)
411 392
412 awaitSandboxReady(t, ready, done)393 awaitSandboxReady(t, ready, done)
413 select {394 select {
@@ -428,12 +409,12 @@ func TestBrokerServeRefusesDuplicateOwnership(t *testing.T) {
428 deps, ready := testBrokerServeDeps(clk)409 deps, ready := testBrokerServeDeps(clk)
429 ctx, cancel := context.WithCancel(context.Background())410 ctx, cancel := context.WithCancel(context.Background())
430 defer cancel()411 defer cancel()
431- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)412+ done := runSandbox(ctx, brokerServeOptions{}, deps)
432 socketPath := awaitSandboxReady(t, ready, done)413 socketPath := awaitSandboxReady(t, ready, done)
433 414
434 // A second foreground owner fails fast on the lifetime lock.415 // A second foreground owner fails fast on the lifetime lock.
435 secondDeps, _ := testBrokerServeDeps(newSandboxClock())416 secondDeps, _ := testBrokerServeDeps(newSandboxClock())
436- err := runBrokerServe(context.Background(), brokerServeOptions{production: true}, secondDeps)417+ err := runBrokerServe(context.Background(), brokerServeOptions{}, secondDeps)
437 require.ErrorIs(t, err, lifecycle.ErrBusy)418 require.ErrorIs(t, err, lifecycle.ErrBusy)
438 419
439 // A duplicate listener on the live socket is refused as well.420 // A duplicate listener on the live socket is refused as well.
M internal/app/broker_ready.go +3 -18
@@ -9,11 +9,9 @@ import (
99 "io"
1010 "io/fs"
1111 "os"
12- "path/filepath"
1312 "strconv"
1413 "time"
1514
16- "github.com/bnema/vev/internal/adapters/brokerconfig"
1715 "github.com/bnema/vev/internal/adapters/brokeripc"
1816 "github.com/bnema/vev/internal/adapters/ipc"
1917 "github.com/bnema/vev/internal/domain"
@@ -24,10 +22,9 @@ import (
2422 const brokerReadyCommand = "_broker-ready"
2523
2624 type brokerReadyOptions struct {
27- offlineRoot string
28- require string
29- timeout time.Duration
30- maxAge time.Duration
25+ require string
26+ timeout time.Duration
27+ maxAge time.Duration
3128 }
3229
3330 type brokerReadyResult struct {
@@ -62,8 +59,6 @@ func parseBrokerReadyArgs(args []string) (brokerReadyOptions, error) {
6259 }
6360 seen[name] = true
6461 switch name {
65- case "--offline-root":
66- o.offlineRoot = value
6762 case "--require":
6863 o.require = value
6964 case "--timeout":
@@ -88,9 +83,6 @@ func parseBrokerReadyArgs(args []string) (brokerReadyOptions, error) {
8883 if o.require != "local-catalogue" && seen["--max-observation-age"] {
8984 return o, usagef("`--max-observation-age` is only valid for local-catalogue")
9085 }
91- if o.offlineRoot != "" && (!filepath.IsAbs(o.offlineRoot) || filepath.Clean(o.offlineRoot) != o.offlineRoot) {
92- return o, usagef("`--offline-root` must be an absolute clean path")
93- }
9486 return o, nil
9587 }
9688
@@ -102,13 +94,6 @@ func runBrokerReady(ctx context.Context, o brokerReadyOptions, out io.Writer) er
10294 deadline, cancel := context.WithTimeout(ctx, o.timeout)
10395 defer cancel()
10496 path := brokeripc.SocketPath(productionBrokerLayout().Runtime)
105- if o.offlineRoot != "" {
106- layout, err := brokerconfig.ResolveLayout(o.offlineRoot, []string{productionBrokerLayout().Runtime, productionBrokerLayout().State})
107- if err != nil {
108- return writeReady(out, o, "terminal", "config", nil, 4)
109- }
110- path = brokeripc.SocketPath(layout.Runtime)
111- }
11297 backoff := 5 * time.Millisecond
11398 for {
11499 if err := deadline.Err(); err != nil {
@@ -9,11 +9,9 @@ import (
9 "io"9 "io"
10 "io/fs"10 "io/fs"
11 "os"11 "os"
12- "path/filepath"
13 "strconv"12 "strconv"
14 "time"13 "time"
15 14
16- "github.com/bnema/vev/internal/adapters/brokerconfig"
17 "github.com/bnema/vev/internal/adapters/brokeripc"15 "github.com/bnema/vev/internal/adapters/brokeripc"
18 "github.com/bnema/vev/internal/adapters/ipc"16 "github.com/bnema/vev/internal/adapters/ipc"
19 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
@@ -24,10 +22,9 @@ import (
24 const brokerReadyCommand = "_broker-ready"22 const brokerReadyCommand = "_broker-ready"
25 23
26 type brokerReadyOptions struct {24 type brokerReadyOptions struct {
27- offlineRoot string25+ require string
28- require string26+ timeout time.Duration
29- timeout time.Duration27+ maxAge time.Duration
30- maxAge time.Duration
31 }28 }
32 29
33 type brokerReadyResult struct {30 type brokerReadyResult struct {
@@ -62,8 +59,6 @@ func parseBrokerReadyArgs(args []string) (brokerReadyOptions, error) {
62 }59 }
63 seen[name] = true60 seen[name] = true
64 switch name {61 switch name {
65- case "--offline-root":
66- o.offlineRoot = value
67 case "--require":62 case "--require":
68 o.require = value63 o.require = value
69 case "--timeout":64 case "--timeout":
@@ -88,9 +83,6 @@ func parseBrokerReadyArgs(args []string) (brokerReadyOptions, error) {
88 if o.require != "local-catalogue" && seen["--max-observation-age"] {83 if o.require != "local-catalogue" && seen["--max-observation-age"] {
89 return o, usagef("`--max-observation-age` is only valid for local-catalogue")84 return o, usagef("`--max-observation-age` is only valid for local-catalogue")
90 }85 }
91- if o.offlineRoot != "" && (!filepath.IsAbs(o.offlineRoot) || filepath.Clean(o.offlineRoot) != o.offlineRoot) {
92- return o, usagef("`--offline-root` must be an absolute clean path")
93- }
94 return o, nil86 return o, nil
95 }87 }
96 88
@@ -102,13 +94,6 @@ func runBrokerReady(ctx context.Context, o brokerReadyOptions, out io.Writer) er
102 deadline, cancel := context.WithTimeout(ctx, o.timeout)94 deadline, cancel := context.WithTimeout(ctx, o.timeout)
103 defer cancel()95 defer cancel()
104 path := brokeripc.SocketPath(productionBrokerLayout().Runtime)96 path := brokeripc.SocketPath(productionBrokerLayout().Runtime)
105- if o.offlineRoot != "" {
106- layout, err := brokerconfig.ResolveLayout(o.offlineRoot, []string{productionBrokerLayout().Runtime, productionBrokerLayout().State})
107- if err != nil {
108- return writeReady(out, o, "terminal", "config", nil, 4)
109- }
110- path = brokeripc.SocketPath(layout.Runtime)
111- }
112 backoff := 5 * time.Millisecond97 backoff := 5 * time.Millisecond
113 for {98 for {
114 if err := deadline.Err(); err != nil {99 if err := deadline.Err(); err != nil {
M internal/app/broker_ready_subprocess_test.go +21 -43
@@ -6,14 +6,13 @@ package app
66 // (TestMain intercepts the hidden broker commands when brokerHelperEnv is set),
77 // so argument parsing, endpoint classification, the real broker IPC setup, the
88 // readiness evaluation, the JSON document, and the process exit code are all
9-// exercised exactly as an operator would observe them. A real `_broker-serve`
10-// composes the offline broker over one isolated root, and the probe reads it
9+// exercised exactly as an operator would observe them. A real production broker serve
10+// composes the broker over isolated XDG directories, and the probe reads it
1111 // over a real AF_UNIX carriage.
1212
1313 import (
1414 "encoding/json"
1515 "errors"
16- "fmt"
1716 "net"
1817 "os"
1918 "os/exec"
@@ -29,36 +28,25 @@ import (
2928 "github.com/bnema/vev/internal/adapters/brokeripc"
3029 )
3130
32-// readySubprocessSandbox isolates one real offline root and records every
31+// readySubprocessSandbox isolates real production XDG directories and records every
3332 // spawned broker helper, exactly like the status subprocess sandbox.
3433 type readySubprocessSandbox struct {
35- t *testing.T
36- root string
37- layout brokerconfig.Layout
38- recordDir string
39- prodRuntime string
40- prodState string
41- broker *exec.Cmd
34+ t *testing.T
35+ layout brokerconfig.Layout
36+ recordDir string
37+ broker *exec.Cmd
4238 }
4339
44-// newReadySubprocessSandbox writes one marker-valid empty sandbox and arranges
40+// newReadySubprocessSandbox provisions one empty production config and arranges
4541 // cleanup of every recorded broker process.
4642 func newReadySubprocessSandbox(t *testing.T) *readySubprocessSandbox {
4743 t.Helper()
48- prodRuntime, prodState := t.TempDir(), t.TempDir()
49- t.Setenv("XDG_RUNTIME_DIR", prodRuntime)
50- t.Setenv("XDG_STATE_HOME", prodState)
51- root := filepath.Join(shortTempDir(t, "vevr"), "s")
52- require.NoError(t, os.MkdirAll(root, 0o700))
53- writeSandboxConfig(t, root, sandboxEmptyDocument("3s"))
44+ layout := emptyProductionBrokerLayout(t, "3s")
5445 recordDir := filepath.Join(shortTempDir(t, "vevw"), "r")
5546 require.NoError(t, os.MkdirAll(recordDir, 0o700))
5647 t.Setenv(brokerHelperEnv, "1")
5748 t.Setenv(brokerHelperRecordDirEnv, recordDir)
58- layout, err := offlineLayout(root)
59- require.NoError(t, err)
60-
61- sandbox := &readySubprocessSandbox{t: t, root: root, layout: layout, recordDir: recordDir, prodRuntime: prodRuntime, prodState: prodState}
49+ sandbox := &readySubprocessSandbox{t: t, layout: layout, recordDir: recordDir}
6250 t.Cleanup(sandbox.cleanup)
6351 return sandbox
6452 }
@@ -72,14 +60,14 @@ func (s *readySubprocessSandbox) socketPath() string {
7260 // returns its stdout, stderr, and exit code.
7361 func (s *readySubprocessSandbox) ready(timeout string, extra ...string) (string, string, int) {
7462 s.t.Helper()
75- args := append([]string{brokerReadyCommand, "--offline-root", s.root, "--timeout", timeout}, extra...)
63+ args := append([]string{brokerReadyCommand, "--timeout", timeout}, extra...)
7664 return runBrokerCommand(s.t, args...)
7765 }
7866
7967 // startBroker runs one foreground `_broker-serve` helper in the background.
8068 func (s *readySubprocessSandbox) startBroker() {
8169 s.t.Helper()
82- command := exec.Command(os.Args[0], brokerServeCommand, "--offline-root", s.root)
70+ command := exec.Command(os.Args[0], productionBrokerServeCommand)
8371 command.Env = os.Environ()
8472 command.Stdout = os.Stdout
8573 command.Stderr = os.Stderr
@@ -114,7 +102,7 @@ func (s *readySubprocessSandbox) cleanup() {
114102 <-done
115103 }
116104 }
117- for _, pid := range recordedPIDsIn(s.t, s.recordDir, brokerServeCommand) {
105+ for _, pid := range recordedPIDsIn(s.t, s.recordDir, productionBrokerServeCommand) {
118106 if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
119107 continue
120108 }
@@ -150,15 +138,9 @@ func decodeReadyReport(t *testing.T, raw string) readyDoc {
150138 // provisioned local route is absent, so it is never falsely reported ready.
151139 func TestBrokerReadySubprocessReadsARealBrokerAuthority(t *testing.T) {
152140 sandbox := newReadySubprocessSandbox(t)
153- // One provisioned local binding whose route nothing listens on: the broker
154- // publishes its local entry as configured authority, so `local-authority` is
141+ // One provisioned local binding whose daemon is not started: the broker
142+ // publishes its local entry as derived authority, so `local-authority` is
155143 // observable while the catalogue is not.
156- require.NoError(t, os.WriteFile(filepath.Join(sandbox.root, brokerconfig.ConfigFileName),
157- []byte(fmt.Sprintf(`{"marker":%q,"registrations":[],"local":{"identity":"subprocess-local","displayOrigin":"local","route":%q,"policy":{"protocolVersion":%d,"catalogSchemaVersion":3,"environmentPolicy":"client-owned","transport":"unix-mux","trust":"same-user","launch":"explicit","isolation":"per-user"}}}`,
158- brokerconfig.Marker, filepath.Join(sandbox.root, "absent-mux.sock"), readyTestPolicy().ProtocolVersion)), 0o600))
159- layout, err := offlineLayout(sandbox.root)
160- require.NoError(t, err)
161- sandbox.layout = layout
162144
163145 sandbox.startBroker()
164146 require.True(t, sandbox.waitForBrokerSocket(15*time.Second), "the broker must bind its endpoint")
@@ -180,7 +162,6 @@ func TestBrokerReadySubprocessReadsARealBrokerAuthority(t *testing.T) {
180162 require.Equal(t, []string{"local-catalogue"}, catalogue.Pending)
181163 require.NotEqual(t, "ready", catalogue.Status, "an unobserved catalogue is never reported ready")
182164
183- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
184165 }
185166
186167 // TestBrokerReadySubprocessStaleSocketIsNeverReady proves a socket file left by
@@ -204,8 +185,7 @@ func TestBrokerReadySubprocessStaleSocketIsNeverReady(t *testing.T) {
204185 info, err := os.Lstat(sandbox.socketPath())
205186 require.NoError(t, err)
206187 require.NotZero(t, info.Mode()&os.ModeSocket)
207- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))
208- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
188+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
209189 }
210190
211191 // TestBrokerReadySubprocessBlockedHandshakeIsNeverReady proves a live endpoint
@@ -226,8 +206,7 @@ func TestBrokerReadySubprocessBlockedHandshakeIsNeverReady(t *testing.T) {
226206 require.Less(t, time.Since(start), 10*time.Second)
227207
228208 // A stalled-but-live endpoint is never respawned over.
229- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))
230- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
209+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
231210 }
232211
233212 // TestBrokerReadySubprocessNonSocketPathIsTerminal proves a path that exists but
@@ -262,7 +241,7 @@ func TestBrokerReadySubprocessNonSocketPathIsTerminal(t *testing.T) {
262241 report := decodeReadyReport(t, stdout)
263242 require.Equal(t, "terminal", report.Status)
264243 require.Equal(t, "security", report.Reason)
265- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))
244+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
266245 })
267246 }
268247 }
@@ -284,7 +263,7 @@ func TestBrokerReadySubprocessPermissionRefusalIsTerminal(t *testing.T) {
284263 report := decodeReadyReport(t, stdout)
285264 require.Equal(t, "terminal", report.Status)
286265 require.Equal(t, "security", report.Reason)
287- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))
266+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
288267 }
289268
290269 // TestBrokerReadySubprocessAbsentSocketTimesOut proves a missing endpoint is
@@ -299,7 +278,7 @@ func TestBrokerReadySubprocessAbsentSocketTimesOut(t *testing.T) {
299278 report := decodeReadyReport(t, stdout)
300279 require.Equal(t, "timeout", report.Status)
301280 require.Equal(t, "deadline", report.Reason)
302- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand), "the readiness probe never ensures or spawns")
281+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand), "the readiness probe never ensures or spawns")
303282 }
304283
305284 // TestBrokerReadySubprocessNonBrokerEndpointIsTerminal proves a live endpoint
@@ -317,8 +296,7 @@ func TestBrokerReadySubprocessNonBrokerEndpointIsTerminal(t *testing.T) {
317296 report := decodeReadyReport(t, stdout)
318297 require.Equal(t, "terminal", report.Status)
319298 require.Equal(t, "protocol", report.Reason)
320- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))
321- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
299+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
322300 }
323301
324302 // answerWithForeignBytes serves one connection per accept with a bounded
@@ -6,14 +6,13 @@ package app
6 // (TestMain intercepts the hidden broker commands when brokerHelperEnv is set),6 // (TestMain intercepts the hidden broker commands when brokerHelperEnv is set),
7 // so argument parsing, endpoint classification, the real broker IPC setup, the7 // so argument parsing, endpoint classification, the real broker IPC setup, the
8 // readiness evaluation, the JSON document, and the process exit code are all8 // readiness evaluation, the JSON document, and the process exit code are all
9-// exercised exactly as an operator would observe them. A real `_broker-serve`9+// exercised exactly as an operator would observe them. A real production broker serve
10-// composes the offline broker over one isolated root, and the probe reads it10+// composes the broker over isolated XDG directories, and the probe reads it
11 // over a real AF_UNIX carriage.11 // over a real AF_UNIX carriage.
12 12
13 import (13 import (
14 "encoding/json"14 "encoding/json"
15 "errors"15 "errors"
16- "fmt"
17 "net"16 "net"
18 "os"17 "os"
19 "os/exec"18 "os/exec"
@@ -29,36 +28,25 @@ import (
29 "github.com/bnema/vev/internal/adapters/brokeripc"28 "github.com/bnema/vev/internal/adapters/brokeripc"
30 )29 )
31 30
32-// readySubprocessSandbox isolates one real offline root and records every31+// readySubprocessSandbox isolates real production XDG directories and records every
33 // spawned broker helper, exactly like the status subprocess sandbox.32 // spawned broker helper, exactly like the status subprocess sandbox.
34 type readySubprocessSandbox struct {33 type readySubprocessSandbox struct {
35- t *testing.T34+ t *testing.T
36- root string35+ layout brokerconfig.Layout
37- layout brokerconfig.Layout36+ recordDir string
38- recordDir string37+ broker *exec.Cmd
39- prodRuntime string
40- prodState string
41- broker *exec.Cmd
42 }38 }
43 39
44-// newReadySubprocessSandbox writes one marker-valid empty sandbox and arranges40+// newReadySubprocessSandbox provisions one empty production config and arranges
45 // cleanup of every recorded broker process.41 // cleanup of every recorded broker process.
46 func newReadySubprocessSandbox(t *testing.T) *readySubprocessSandbox {42 func newReadySubprocessSandbox(t *testing.T) *readySubprocessSandbox {
47 t.Helper()43 t.Helper()
48- prodRuntime, prodState := t.TempDir(), t.TempDir()44+ layout := emptyProductionBrokerLayout(t, "3s")
49- t.Setenv("XDG_RUNTIME_DIR", prodRuntime)
50- t.Setenv("XDG_STATE_HOME", prodState)
51- root := filepath.Join(shortTempDir(t, "vevr"), "s")
52- require.NoError(t, os.MkdirAll(root, 0o700))
53- writeSandboxConfig(t, root, sandboxEmptyDocument("3s"))
54 recordDir := filepath.Join(shortTempDir(t, "vevw"), "r")45 recordDir := filepath.Join(shortTempDir(t, "vevw"), "r")
55 require.NoError(t, os.MkdirAll(recordDir, 0o700))46 require.NoError(t, os.MkdirAll(recordDir, 0o700))
56 t.Setenv(brokerHelperEnv, "1")47 t.Setenv(brokerHelperEnv, "1")
57 t.Setenv(brokerHelperRecordDirEnv, recordDir)48 t.Setenv(brokerHelperRecordDirEnv, recordDir)
58- layout, err := offlineLayout(root)49+ sandbox := &readySubprocessSandbox{t: t, layout: layout, recordDir: recordDir}
59- require.NoError(t, err)
60-
61- sandbox := &readySubprocessSandbox{t: t, root: root, layout: layout, recordDir: recordDir, prodRuntime: prodRuntime, prodState: prodState}
62 t.Cleanup(sandbox.cleanup)50 t.Cleanup(sandbox.cleanup)
63 return sandbox51 return sandbox
64 }52 }
@@ -72,14 +60,14 @@ func (s *readySubprocessSandbox) socketPath() string {
72 // returns its stdout, stderr, and exit code.60 // returns its stdout, stderr, and exit code.
73 func (s *readySubprocessSandbox) ready(timeout string, extra ...string) (string, string, int) {61 func (s *readySubprocessSandbox) ready(timeout string, extra ...string) (string, string, int) {
74 s.t.Helper()62 s.t.Helper()
75- args := append([]string{brokerReadyCommand, "--offline-root", s.root, "--timeout", timeout}, extra...)63+ args := append([]string{brokerReadyCommand, "--timeout", timeout}, extra...)
76 return runBrokerCommand(s.t, args...)64 return runBrokerCommand(s.t, args...)
77 }65 }
78 66
79 // startBroker runs one foreground `_broker-serve` helper in the background.67 // startBroker runs one foreground `_broker-serve` helper in the background.
80 func (s *readySubprocessSandbox) startBroker() {68 func (s *readySubprocessSandbox) startBroker() {
81 s.t.Helper()69 s.t.Helper()
82- command := exec.Command(os.Args[0], brokerServeCommand, "--offline-root", s.root)70+ command := exec.Command(os.Args[0], productionBrokerServeCommand)
83 command.Env = os.Environ()71 command.Env = os.Environ()
84 command.Stdout = os.Stdout72 command.Stdout = os.Stdout
85 command.Stderr = os.Stderr73 command.Stderr = os.Stderr
@@ -114,7 +102,7 @@ func (s *readySubprocessSandbox) cleanup() {
114 <-done102 <-done
115 }103 }
116 }104 }
117- for _, pid := range recordedPIDsIn(s.t, s.recordDir, brokerServeCommand) {105+ for _, pid := range recordedPIDsIn(s.t, s.recordDir, productionBrokerServeCommand) {
118 if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {106 if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
119 continue107 continue
120 }108 }
@@ -150,15 +138,9 @@ func decodeReadyReport(t *testing.T, raw string) readyDoc {
150 // provisioned local route is absent, so it is never falsely reported ready.138 // provisioned local route is absent, so it is never falsely reported ready.
151 func TestBrokerReadySubprocessReadsARealBrokerAuthority(t *testing.T) {139 func TestBrokerReadySubprocessReadsARealBrokerAuthority(t *testing.T) {
152 sandbox := newReadySubprocessSandbox(t)140 sandbox := newReadySubprocessSandbox(t)
153- // One provisioned local binding whose route nothing listens on: the broker141+ // One provisioned local binding whose daemon is not started: the broker
154- // publishes its local entry as configured authority, so `local-authority` is142+ // publishes its local entry as derived authority, so `local-authority` is
155 // observable while the catalogue is not.143 // observable while the catalogue is not.
156- require.NoError(t, os.WriteFile(filepath.Join(sandbox.root, brokerconfig.ConfigFileName),
157- []byte(fmt.Sprintf(`{"marker":%q,"registrations":[],"local":{"identity":"subprocess-local","displayOrigin":"local","route":%q,"policy":{"protocolVersion":%d,"catalogSchemaVersion":3,"environmentPolicy":"client-owned","transport":"unix-mux","trust":"same-user","launch":"explicit","isolation":"per-user"}}}`,
158- brokerconfig.Marker, filepath.Join(sandbox.root, "absent-mux.sock"), readyTestPolicy().ProtocolVersion)), 0o600))
159- layout, err := offlineLayout(sandbox.root)
160- require.NoError(t, err)
161- sandbox.layout = layout
162 144
163 sandbox.startBroker()145 sandbox.startBroker()
164 require.True(t, sandbox.waitForBrokerSocket(15*time.Second), "the broker must bind its endpoint")146 require.True(t, sandbox.waitForBrokerSocket(15*time.Second), "the broker must bind its endpoint")
@@ -180,7 +162,6 @@ func TestBrokerReadySubprocessReadsARealBrokerAuthority(t *testing.T) {
180 require.Equal(t, []string{"local-catalogue"}, catalogue.Pending)162 require.Equal(t, []string{"local-catalogue"}, catalogue.Pending)
181 require.NotEqual(t, "ready", catalogue.Status, "an unobserved catalogue is never reported ready")163 require.NotEqual(t, "ready", catalogue.Status, "an unobserved catalogue is never reported ready")
182 164
183- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
184 }165 }
185 166
186 // TestBrokerReadySubprocessStaleSocketIsNeverReady proves a socket file left by167 // TestBrokerReadySubprocessStaleSocketIsNeverReady proves a socket file left by
@@ -204,8 +185,7 @@ func TestBrokerReadySubprocessStaleSocketIsNeverReady(t *testing.T) {
204 info, err := os.Lstat(sandbox.socketPath())185 info, err := os.Lstat(sandbox.socketPath())
205 require.NoError(t, err)186 require.NoError(t, err)
206 require.NotZero(t, info.Mode()&os.ModeSocket)187 require.NotZero(t, info.Mode()&os.ModeSocket)
207- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))188+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
208- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
209 }189 }
210 190
211 // TestBrokerReadySubprocessBlockedHandshakeIsNeverReady proves a live endpoint191 // TestBrokerReadySubprocessBlockedHandshakeIsNeverReady proves a live endpoint
@@ -226,8 +206,7 @@ func TestBrokerReadySubprocessBlockedHandshakeIsNeverReady(t *testing.T) {
226 require.Less(t, time.Since(start), 10*time.Second)206 require.Less(t, time.Since(start), 10*time.Second)
227 207
228 // A stalled-but-live endpoint is never respawned over.208 // A stalled-but-live endpoint is never respawned over.
229- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))209+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
230- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
231 }210 }
232 211
233 // TestBrokerReadySubprocessNonSocketPathIsTerminal proves a path that exists but212 // TestBrokerReadySubprocessNonSocketPathIsTerminal proves a path that exists but
@@ -262,7 +241,7 @@ func TestBrokerReadySubprocessNonSocketPathIsTerminal(t *testing.T) {
262 report := decodeReadyReport(t, stdout)241 report := decodeReadyReport(t, stdout)
263 require.Equal(t, "terminal", report.Status)242 require.Equal(t, "terminal", report.Status)
264 require.Equal(t, "security", report.Reason)243 require.Equal(t, "security", report.Reason)
265- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))244+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
266 })245 })
267 }246 }
268 }247 }
@@ -284,7 +263,7 @@ func TestBrokerReadySubprocessPermissionRefusalIsTerminal(t *testing.T) {
284 report := decodeReadyReport(t, stdout)263 report := decodeReadyReport(t, stdout)
285 require.Equal(t, "terminal", report.Status)264 require.Equal(t, "terminal", report.Status)
286 require.Equal(t, "security", report.Reason)265 require.Equal(t, "security", report.Reason)
287- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))266+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
288 }267 }
289 268
290 // TestBrokerReadySubprocessAbsentSocketTimesOut proves a missing endpoint is269 // TestBrokerReadySubprocessAbsentSocketTimesOut proves a missing endpoint is
@@ -299,7 +278,7 @@ func TestBrokerReadySubprocessAbsentSocketTimesOut(t *testing.T) {
299 report := decodeReadyReport(t, stdout)278 report := decodeReadyReport(t, stdout)
300 require.Equal(t, "timeout", report.Status)279 require.Equal(t, "timeout", report.Status)
301 require.Equal(t, "deadline", report.Reason)280 require.Equal(t, "deadline", report.Reason)
302- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand), "the readiness probe never ensures or spawns")281+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand), "the readiness probe never ensures or spawns")
303 }282 }
304 283
305 // TestBrokerReadySubprocessNonBrokerEndpointIsTerminal proves a live endpoint284 // TestBrokerReadySubprocessNonBrokerEndpointIsTerminal proves a live endpoint
@@ -317,8 +296,7 @@ func TestBrokerReadySubprocessNonBrokerEndpointIsTerminal(t *testing.T) {
317 report := decodeReadyReport(t, stdout)296 report := decodeReadyReport(t, stdout)
318 require.Equal(t, "terminal", report.Status)297 require.Equal(t, "terminal", report.Status)
319 require.Equal(t, "protocol", report.Reason)298 require.Equal(t, "protocol", report.Reason)
320- require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, brokerServeCommand))299+ require.Empty(t, recordedPIDsIn(t, sandbox.recordDir, productionBrokerServeCommand))
321- requireProductionUntouched(t, sandbox.prodRuntime, sandbox.prodState)
322 }300 }
323 301
324 // answerWithForeignBytes serves one connection per accept with a bounded302 // answerWithForeignBytes serves one connection per accept with a bounded
M internal/app/broker_ready_test.go +4 -8
@@ -277,10 +277,8 @@ func (s *readyDialScript) createdServices() []*readyScriptedService {
277277 // offline root, so no production path is ever inspected.
278278 func readyTestOptions(t *testing.T, requirement string, timeout time.Duration) brokerReadyOptions {
279279 t.Helper()
280- root, _, _ := isolateSandboxEnv(t)
281- require.NoError(t, os.MkdirAll(root, 0o700))
282- writeSandboxConfig(t, root, sandboxEmptyDocument(""))
283- options, err := parseBrokerReadyArgs([]string{"--offline-root", root, "--require", requirement, "--timeout", timeout.String()})
280+ emptyProductionBrokerLayout(t, "")
281+ options, err := parseBrokerReadyArgs([]string{"--require", requirement, "--timeout", timeout.String()})
284282 require.NoError(t, err)
285283 return options
286284 }
@@ -326,8 +324,8 @@ func TestParseBrokerReadyArgs(t *testing.T) {
326324 },
327325 {
328326 name: "catalogue with an explicit observation age",
329- args: []string{"--require", "local-catalogue", "--max-observation-age", "2s", "--timeout", "3s", "--offline-root", "/srv/broker"},
330- want: brokerReadyOptions{offlineRoot: "/srv/broker", require: "local-catalogue", timeout: 3 * time.Second, maxAge: 2 * time.Second},
327+ args: []string{"--require", "local-catalogue", "--max-observation-age", "2s", "--timeout", "3s"},
328+ want: brokerReadyOptions{require: "local-catalogue", timeout: 3 * time.Second, maxAge: 2 * time.Second},
331329 },
332330 {name: "missing require", args: nil, wantErr: "`--require` must be local-authority or local-catalogue"},
333331 {name: "unknown require", args: []string{"--require", "remote-catalogue"}, wantErr: "`--require` must be local-authority or local-catalogue"},
@@ -344,8 +342,6 @@ func TestParseBrokerReadyArgs(t *testing.T) {
344342 args: []string{"--require", "local-authority", "--max-observation-age", "2s"},
345343 wantErr: "`--max-observation-age` is only valid for local-catalogue",
346344 },
347- {name: "relative offline root", args: []string{"--require", "local-authority", "--offline-root", "relative/root"}, wantErr: "`--offline-root` must be an absolute clean path"},
348- {name: "unclean offline root", args: []string{"--require", "local-authority", "--offline-root", "/srv/../srv/broker"}, wantErr: "`--offline-root` must be an absolute clean path"},
349345 }
350346 for _, tt := range tests {
351347 t.Run(tt.name, func(t *testing.T) {
@@ -277,10 +277,8 @@ func (s *readyDialScript) createdServices() []*readyScriptedService {
277 // offline root, so no production path is ever inspected.277 // offline root, so no production path is ever inspected.
278 func readyTestOptions(t *testing.T, requirement string, timeout time.Duration) brokerReadyOptions {278 func readyTestOptions(t *testing.T, requirement string, timeout time.Duration) brokerReadyOptions {
279 t.Helper()279 t.Helper()
280- root, _, _ := isolateSandboxEnv(t)280+ emptyProductionBrokerLayout(t, "")
281- require.NoError(t, os.MkdirAll(root, 0o700))281+ options, err := parseBrokerReadyArgs([]string{"--require", requirement, "--timeout", timeout.String()})
282- writeSandboxConfig(t, root, sandboxEmptyDocument(""))
283- options, err := parseBrokerReadyArgs([]string{"--offline-root", root, "--require", requirement, "--timeout", timeout.String()})
284 require.NoError(t, err)282 require.NoError(t, err)
285 return options283 return options
286 }284 }
@@ -326,8 +324,8 @@ func TestParseBrokerReadyArgs(t *testing.T) {
326 },324 },
327 {325 {
328 name: "catalogue with an explicit observation age",326 name: "catalogue with an explicit observation age",
329- args: []string{"--require", "local-catalogue", "--max-observation-age", "2s", "--timeout", "3s", "--offline-root", "/srv/broker"},327+ args: []string{"--require", "local-catalogue", "--max-observation-age", "2s", "--timeout", "3s"},
330- want: brokerReadyOptions{offlineRoot: "/srv/broker", require: "local-catalogue", timeout: 3 * time.Second, maxAge: 2 * time.Second},328+ want: brokerReadyOptions{require: "local-catalogue", timeout: 3 * time.Second, maxAge: 2 * time.Second},
331 },329 },
332 {name: "missing require", args: nil, wantErr: "`--require` must be local-authority or local-catalogue"},330 {name: "missing require", args: nil, wantErr: "`--require` must be local-authority or local-catalogue"},
333 {name: "unknown require", args: []string{"--require", "remote-catalogue"}, wantErr: "`--require` must be local-authority or local-catalogue"},331 {name: "unknown require", args: []string{"--require", "remote-catalogue"}, wantErr: "`--require` must be local-authority or local-catalogue"},
@@ -344,8 +342,6 @@ func TestParseBrokerReadyArgs(t *testing.T) {
344 args: []string{"--require", "local-authority", "--max-observation-age", "2s"},342 args: []string{"--require", "local-authority", "--max-observation-age", "2s"},
345 wantErr: "`--max-observation-age` is only valid for local-catalogue",343 wantErr: "`--max-observation-age` is only valid for local-catalogue",
346 },344 },
347- {name: "relative offline root", args: []string{"--require", "local-authority", "--offline-root", "relative/root"}, wantErr: "`--offline-root` must be an absolute clean path"},
348- {name: "unclean offline root", args: []string{"--require", "local-authority", "--offline-root", "/srv/../srv/broker"}, wantErr: "`--offline-root` must be an absolute clean path"},
349 }345 }
350 for _, tt := range tests {346 for _, tt := range tests {
351 t.Run(tt.name, func(t *testing.T) {347 t.Run(tt.name, func(t *testing.T) {
M internal/app/broker_routes_test.go +2 -3
@@ -116,7 +116,6 @@ func TestCanonicalBrokerRoutesSelectProductionHelpers(t *testing.T) {
116116 require.NoError(t, err)
117117 cmd, err := parseArgs(spec.Argv[1:])
118118 require.NoError(t, err)
119- require.True(t, cmd.brokerMux.production)
120119 require.Equal(t, ports.BrokerDaemonExistingOnly, cmd.brokerMux.startMode)
121120 for _, args := range [][]string{{"--production", "--production"}, {"--production", "--offline-root", "/tmp/root"}, {"--offline-root", "/tmp/root", "--production"}} {
122121 _, err = parseBrokerMuxArgs(spec.Argv[1], cmd.kind, args)
@@ -140,7 +139,7 @@ func TestProductionBrokerFirstHostAdd(t *testing.T) {
140139 for i := 0; i < 2; i++ {
141140 deps, ready := testBrokerServeDeps(newSandboxClock())
142141 ctx, cancel := context.WithCancel(context.Background())
143- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)
142+ done := runSandbox(ctx, brokerServeOptions{}, deps)
144143 socket := awaitSandboxReady(t, ready, done)
145144 service, err := brokeripc.NewConnector(socket, brokeripc.Config{}).Connect(ctx)
146145 require.NoError(t, err)
@@ -192,7 +191,7 @@ func TestProductionMuxHelperExistingOnlyNeverSpawns(t *testing.T) {
192191 },
193192 spawn: func() error { spawns++; return nil },
194193 }
195- _, err := dialBrokerMuxHelper(context.Background(), brokerMuxOptions{production: true, startMode: ports.BrokerDaemonExistingOnly})
194+ _, err := dialBrokerMuxHelper(context.Background(), brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly})
196195 require.ErrorIs(t, err, os.ErrNotExist)
197196 require.Equal(t, 1, dials)
198197 require.Zero(t, spawns)
@@ -116,7 +116,6 @@ func TestCanonicalBrokerRoutesSelectProductionHelpers(t *testing.T) {
116 require.NoError(t, err)116 require.NoError(t, err)
117 cmd, err := parseArgs(spec.Argv[1:])117 cmd, err := parseArgs(spec.Argv[1:])
118 require.NoError(t, err)118 require.NoError(t, err)
119- require.True(t, cmd.brokerMux.production)
120 require.Equal(t, ports.BrokerDaemonExistingOnly, cmd.brokerMux.startMode)119 require.Equal(t, ports.BrokerDaemonExistingOnly, cmd.brokerMux.startMode)
121 for _, args := range [][]string{{"--production", "--production"}, {"--production", "--offline-root", "/tmp/root"}, {"--offline-root", "/tmp/root", "--production"}} {120 for _, args := range [][]string{{"--production", "--production"}, {"--production", "--offline-root", "/tmp/root"}, {"--offline-root", "/tmp/root", "--production"}} {
122 _, err = parseBrokerMuxArgs(spec.Argv[1], cmd.kind, args)121 _, err = parseBrokerMuxArgs(spec.Argv[1], cmd.kind, args)
@@ -140,7 +139,7 @@ func TestProductionBrokerFirstHostAdd(t *testing.T) {
140 for i := 0; i < 2; i++ {139 for i := 0; i < 2; i++ {
141 deps, ready := testBrokerServeDeps(newSandboxClock())140 deps, ready := testBrokerServeDeps(newSandboxClock())
142 ctx, cancel := context.WithCancel(context.Background())141 ctx, cancel := context.WithCancel(context.Background())
143- done := runSandbox(ctx, brokerServeOptions{production: true}, deps)142+ done := runSandbox(ctx, brokerServeOptions{}, deps)
144 socket := awaitSandboxReady(t, ready, done)143 socket := awaitSandboxReady(t, ready, done)
145 service, err := brokeripc.NewConnector(socket, brokeripc.Config{}).Connect(ctx)144 service, err := brokeripc.NewConnector(socket, brokeripc.Config{}).Connect(ctx)
146 require.NoError(t, err)145 require.NoError(t, err)
@@ -192,7 +191,7 @@ func TestProductionMuxHelperExistingOnlyNeverSpawns(t *testing.T) {
192 },191 },
193 spawn: func() error { spawns++; return nil },192 spawn: func() error { spawns++; return nil },
194 }193 }
195- _, err := dialBrokerMuxHelper(context.Background(), brokerMuxOptions{production: true, startMode: ports.BrokerDaemonExistingOnly})194+ _, err := dialBrokerMuxHelper(context.Background(), brokerMuxOptions{startMode: ports.BrokerDaemonExistingOnly})
196 require.ErrorIs(t, err, os.ErrNotExist)195 require.ErrorIs(t, err, os.ErrNotExist)
197 require.Equal(t, 1, dials)196 require.Equal(t, 1, dials)
198 require.Zero(t, spawns)197 require.Zero(t, spawns)
D internal/app/connectivity_matrix.go +0 -246
@@ -1,246 +0,0 @@
1-package app
2-
3-// Connectivity migration matrix.
4-//
5-// Every CLI entry point has exactly one owner. Ordinary user-facing
6-// operations reach daemons through the per-user
7-// connection broker; narrowly scoped daemon-facing transport helpers stay
8-// infrastructure and never become alternate client façades.
9-//
10-// Ownership rules pinned here:
11-// - Broker-only entries must not keep a direct daemon dialer or a second
12-// host-store writer after cutover. Their CurrentDebt lists today's
13-// direct-connectivity symbols to remove in the same change that
14-// activates the broker owner.
15-// - Broker never owns session persistence. Offline session mutation/list
16-// paths (PersistMutation) become daemon-owned operations reached through
17-// the broker; the persist/snapshot packages stay daemon-side.
18-// - Observation never starts a broker, never undoes an explicit
19-// daemon-stop, and never creates a session attachment.
20-// - Transport helpers terminate transport only. They never own host
21-// configuration, navigation, sessions, or picker state.
22-//
23-// The accompanying test fails when a command kind lacks a row, when a
24-// broker-only row declares no migration debt, when an infra row gains store
25-// or persistence ownership, and when direct-dial, host-store, or offline
26-// persistence symbols appear in files outside the declared debt set.
27-
28-type connectivityOwner uint8
29-
30-const (
31- // connectivityBrokerOnly marks ordinary user-facing operations that go
32- // through the broker façade after cutover.
33- connectivityBrokerOnly connectivityOwner = iota + 1
34- // connectivityTransportInfra marks narrowly scoped daemon-facing
35- // transport helpers: process mechanics and carriage terminators that
36- // must never become client façades.
37- connectivityTransportInfra
38- // connectivityLocalOnly marks commands with no daemon or session
39- // connectivity at all.
40- connectivityLocalOnly
41-)
42-
43-func (o connectivityOwner) String() string {
44- switch o {
45- case connectivityBrokerOnly:
46- return "broker-only"
47- case connectivityTransportInfra:
48- return "transport-infra"
49- case connectivityLocalOnly:
50- return "local-only"
51- default:
52- return "unknown"
53- }
54-}
55-
56-// connectivityEntry is one row of the migration matrix. Kind is the cmdKind
57-// identifier from run.go (for example "kindAttach").
58-type connectivityEntry struct {
59- Kind string
60- // Summary names the user-visible operation.
61- Summary string
62- Owner connectivityOwner
63- // DirectDialDebt lists today's direct-daemon dial symbols that the
64- // cutover removes for broker-only entries. Empty for infra/local rows and
65- // for a broker-only row that has already completed its cutover.
66- DirectDialDebt []string
67- // Migrated marks a broker-only row whose cutover is complete: it keeps no
68- // direct dial, no host-store write, and no persist mutation, so its debt
69- // list is deliberately empty. It exists so a finished row is declared
70- // explicitly instead of being mistaken for an unreviewed one.
71- Migrated bool
72- // HostStoreWrite reports a RemoteHostStore constructor/writer today.
73- // Only the broker owns the host store after cutover.
74- HostStoreWrite bool
75- // PersistMutation reports an offline session persistence read/write
76- // today. These become daemon-owned operations through the broker.
77- PersistMutation bool
78- // Notes records cutover direction and non-obvious boundaries.
79- Notes string
80-}
81-
82-// connectivityMatrix classifies every cmdKind exactly once.
83-var connectivityMatrix = []connectivityEntry{
84- {
85- Kind: "kindAttach",
86- Summary: "interactive terminal attach/new/ephemeral, incl. remote attach, detached creation, and the UI-driver/web path",
87- Owner: connectivityBrokerOnly,
88- DirectDialDebt: []string{
89- "ensureDaemonWithLifecycle detached creation (run.go createDetachedLocalSession)",
90- },
91- Notes: "The ordinary terminal path is already broker-only: runAttach translates the parsed CLI target into client.InitialNavigation/Resolver, connect-or-spawns the per-user broker, and delegates to the shared runBrokerClient (run.go, broker_client.go). The old direct-dialer attach composition and its attach preflight and client host registry are removed; the UI-driver and browser compositions delegate to the shared broker client. The only remaining debt is the nested-session detached creation path, which still dials the local daemon directly and is expressed as broker navigation for ordinary attach.",
92- },
93- {
94- Kind: "kindList",
95- Summary: "session list: local, per-host, and --all",
96- Owner: connectivityBrokerOnly,
97- Migrated: true,
98- Notes: "Cutover complete: runList reaches the per-user broker through the private connectBroker seam (run.go) and reads BrokerOperations.List for the local list or runBrokerSnapshotList for per-host/--all. It keeps no persist read, no daemon dialer, and no direct-dial list path. Remote observation never attaches.",
99- },
100- {
101- Kind: "kindHost",
102- Summary: "host add/rm/list",
103- Owner: connectivityBrokerOnly,
104- DirectDialDebt: []string{"remoteHostDeps.hostStore (remote_hosts.go hostAdd/hostRm)"},
105- HostStoreWrite: true,
106- Notes: "Second host-store writer today; broker is the sole writer after cutover. Host list is store-only (no dial).",
107- },
108- {
109- Kind: "kindKill",
110- Summary: "kill one session, kill-all, explicit daemon-stop",
111- Owner: connectivityBrokerOnly,
112- Migrated: true,
113- Notes: "Cutover complete: runKill and requestDaemonStop reach the per-user broker through the private connectBroker seam (run.go) and drive BrokerOperations.Kill/KillAll/StopDaemon. Daemon-stop uses ExistingOnly, so it can never start what it is stopping; kill-all purges sessions and leaves the daemon running. The offline persisted-mutation path and the direct force-stop fallback are removed, so no request is ever bypassed with a direct process signal.",
114- },
115- {
116- Kind: "kindCmd",
117- Summary: "scriptable command requests, incl. remote-catalog",
118- Owner: connectivityBrokerOnly,
119- Migrated: true,
120- Notes: "Command requests use broker operations in production; the injected direct dial seam remains for focused legacy tracker tests.",
121- },
122- {
123- Kind: "kindUIDriver",
124- Summary: "headless UI-driver attach",
125- Owner: connectivityBrokerOnly,
126- Migrated: true,
127- Notes: "Cutover complete: the driver composes the process' production broker connector and the shared terminal initial-navigation translation through runUIDriverClient. It keeps no launch configuration, no dialer, no endpoint, and no daemon ownership.",
128- },
129- {
130- Kind: "kindUIRemoteCleanup",
131- Summary: "UI fixture cleanup through broker control",
132- Owner: connectivityBrokerOnly,
133- Migrated: true,
134- Notes: "Cleanup uses the broker-owned daemon-stop operation and keeps no direct daemon carriage.",
135- },
136- {
137- Kind: "kindWebDaemon",
138- Summary: "browser gateway launcher",
139- Owner: connectivityBrokerOnly,
140- Migrated: true,
141- Notes: "Launcher mechanics stay local. Cutover complete: the gateway server composes the shared runBrokerClient over the process' production broker connector (web_daemon.go runWebTerminalClient), so gateway session traffic goes through the broker like any client. The launcher keeps no dialer, no session carriage, and no daemon ownership.",
142- },
143- {
144- Kind: "kindWebServe",
145- Summary: "browser gateway server",
146- Owner: connectivityBrokerOnly,
147- Migrated: true,
148- Notes: "Cutover complete: every authenticated WebSocket owns exactly one runBrokerClient over the shared production broker connector, with the virtual webterm.Terminal as that run's terminal, the no-argument local ephemeral creation as its closed initial navigation, and the standard presentation callbacks. It keeps no direct daemon dialer, no sessionwire carriage, no remote factory, and no launch configuration; disconnect cancels only that supervisor's service and logical stream.",
149- },
150- {
151- Kind: "kindDaemon",
152- Summary: "daemon process: owns sessions, serves typed connections",
153- Owner: connectivityTransportInfra,
154- Notes: "Daemon accepts one typed connection per broker logical stream after cutover. Never aggregates other daemons, never owns the host registry (P7 removal set).",
155- },
156- {
157- Kind: "kindDaemonLauncher",
158- Summary: "short-lived daemon spawn launcher",
159- Owner: connectivityTransportInfra,
160- Notes: "Process mechanics only; never a connectivity façade.",
161- },
162- {
163- Kind: "kindWebRenew",
164- Summary: "browser token renewal",
165- Owner: connectivityLocalOnly,
166- Notes: "Local control only; no daemon or session connectivity.",
167- },
168- {
169- Kind: "kindBrokerReady",
170- Summary: "hidden dial-only broker readiness probe",
171- Owner: connectivityLocalOnly,
172- Notes: "Dials only the selected broker IPC socket, registers, subscribes, and reads snapshots; it never ensures, spawns, opens a logical stream, reconciles, or mutates.",
173- },
174- {
175- Kind: "kindHelp",
176- Summary: "usage text",
177- Owner: connectivityLocalOnly,
178- Notes: "No connectivity.",
179- },
180- {
181- Kind: "kindVersion",
182- Summary: "version line",
183- Owner: connectivityLocalOnly,
184- Notes: "No connectivity.",
185- },
186- {
187- Kind: "kindBrokerServe",
188- Summary: "hidden _broker-serve isolated offline broker sandbox",
189- Owner: connectivityLocalOnly,
190- Notes: "Foreground sandbox process over its own private root: no production runtime/state, no daemon dial, no RemoteHostStore, and no session persistence. It composes the broker under a temporary offline config and does not become a client façade or an ordinary command.",
191- },
192- {
193- Kind: "kindProductionBrokerServe",
194- Summary: "hidden production broker server",
195- Owner: connectivityLocalOnly,
196- Notes: "Foreground production broker using only productionBrokerLayout and productionBrokerConfigPath; it accepts no offline root.",
197- },
198- {
199- Kind: "kindBrokerClient",
200- Summary: "hidden _broker-client autonomous offline client harness",
201- Owner: connectivityLocalOnly,
202- Notes: "Explicit offline-root composition only: real broker IPC and logical streams exercise terminal and UI harnesses without changing any ordinary command or production path.",
203- },
204- {
205- Kind: "kindBrokerLauncher",
206- Summary: "hidden _broker-launcher detached offline broker launcher",
207- Owner: connectivityLocalOnly,
208- Notes: "Process mechanics only, scoped to one operator-supplied offline root: it starts _broker-serve in a new session and exits, like the daemon launcher, and never dials a daemon, owns the host store, or mutates persistence.",
209- },
210- {
211- Kind: "kindProductionBrokerLauncher",
212- Summary: "hidden detached production broker launcher",
213- Owner: connectivityLocalOnly,
214- Notes: "Process mechanics for the production broker only; it accepts no offline root and starts the production serve role in a new session.",
215- },
216- {
217- Kind: "kindBrokerStatus",
218- Summary: "hidden _broker-status offline broker probe and connect-or-spawn",
219- Owner: connectivityLocalOnly,
220- Notes: "Reports the offline broker's local IPC endpoint and, with --ensure, elects one spawner under a descriptor-backed lock. It dials only the operator-supplied broker sandbox socket (never a daemon or RemoteHostStore) and never becomes a client façade or ordinary command.",
221- },
222- {
223- Kind: "kindBrokerMuxStdio",
224- Summary: "hidden _broker-mux-stdio remote SSH stdio daemonmux bridge",
225- Owner: connectivityTransportInfra,
226- DirectDialDebt: []string{
227- "ipc.DialMuxContext local daemonmux carriage (broker_offline_mux.go)",
228- },
229- Notes: "Remote-side helper over one operator-supplied offline root: it bridges its own stdio to the single provisioned private Unix daemonmux carriage. It never starts a broker, observer, or recursive helper, never dials the production daemon socket, and never fabricates a daemon incarnation. It starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and only when its own provisioned policy permits launching; otherwise it dials only an existing carriage. Direct-dial debt is recorded here rather than as broker debt because the helper itself stays transport infra.",
230- },
231- {
232- Kind: "kindBrokerMuxQUICBootstrap",
233- Summary: "hidden _broker-mux-quic-bootstrap remote SSH-side QUIC bootstrap",
234- Owner: connectivityTransportInfra,
235- Notes: "Starts one detached _broker-mux-quic-proxy in a new session, forwards its single readiness line, and exits. Its only child is that proxy: it never starts a broker, observer, or ordinary daemon, and one-time QUIC credentials never reach an address, log, or error.",
236- },
237- {
238- Kind: "kindBrokerMuxQUICProxy",
239- Summary: "hidden _broker-mux-quic-proxy detached remote QUIC daemonmux bridge",
240- Owner: connectivityTransportInfra,
241- DirectDialDebt: []string{
242- "ipc.DialMuxContext local daemonmux carriage (broker_offline_mux.go)",
243- },
244- Notes: "Mints one ephemeral authenticated QUIC server, admits exactly one carriage, and bridges it to the single provisioned private Unix daemonmux carriage. It never dials the production daemon socket, never fabricates a daemon incarnation, and starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and its own provisioned launch policy.",
245- },
246-}
@@ -1,246 +0,0 @@
1-package app
2-
3-// Connectivity migration matrix.
4-//
5-// Every CLI entry point has exactly one owner. Ordinary user-facing
6-// operations reach daemons through the per-user
7-// connection broker; narrowly scoped daemon-facing transport helpers stay
8-// infrastructure and never become alternate client façades.
9-//
10-// Ownership rules pinned here:
11-// - Broker-only entries must not keep a direct daemon dialer or a second
12-// host-store writer after cutover. Their CurrentDebt lists today's
13-// direct-connectivity symbols to remove in the same change that
14-// activates the broker owner.
15-// - Broker never owns session persistence. Offline session mutation/list
16-// paths (PersistMutation) become daemon-owned operations reached through
17-// the broker; the persist/snapshot packages stay daemon-side.
18-// - Observation never starts a broker, never undoes an explicit
19-// daemon-stop, and never creates a session attachment.
20-// - Transport helpers terminate transport only. They never own host
21-// configuration, navigation, sessions, or picker state.
22-//
23-// The accompanying test fails when a command kind lacks a row, when a
24-// broker-only row declares no migration debt, when an infra row gains store
25-// or persistence ownership, and when direct-dial, host-store, or offline
26-// persistence symbols appear in files outside the declared debt set.
27-
28-type connectivityOwner uint8
29-
30-const (
31- // connectivityBrokerOnly marks ordinary user-facing operations that go
32- // through the broker façade after cutover.
33- connectivityBrokerOnly connectivityOwner = iota + 1
34- // connectivityTransportInfra marks narrowly scoped daemon-facing
35- // transport helpers: process mechanics and carriage terminators that
36- // must never become client façades.
37- connectivityTransportInfra
38- // connectivityLocalOnly marks commands with no daemon or session
39- // connectivity at all.
40- connectivityLocalOnly
41-)
42-
43-func (o connectivityOwner) String() string {
44- switch o {
45- case connectivityBrokerOnly:
46- return "broker-only"
47- case connectivityTransportInfra:
48- return "transport-infra"
49- case connectivityLocalOnly:
50- return "local-only"
51- default:
52- return "unknown"
53- }
54-}
55-
56-// connectivityEntry is one row of the migration matrix. Kind is the cmdKind
57-// identifier from run.go (for example "kindAttach").
58-type connectivityEntry struct {
59- Kind string
60- // Summary names the user-visible operation.
61- Summary string
62- Owner connectivityOwner
63- // DirectDialDebt lists today's direct-daemon dial symbols that the
64- // cutover removes for broker-only entries. Empty for infra/local rows and
65- // for a broker-only row that has already completed its cutover.
66- DirectDialDebt []string
67- // Migrated marks a broker-only row whose cutover is complete: it keeps no
68- // direct dial, no host-store write, and no persist mutation, so its debt
69- // list is deliberately empty. It exists so a finished row is declared
70- // explicitly instead of being mistaken for an unreviewed one.
71- Migrated bool
72- // HostStoreWrite reports a RemoteHostStore constructor/writer today.
73- // Only the broker owns the host store after cutover.
74- HostStoreWrite bool
75- // PersistMutation reports an offline session persistence read/write
76- // today. These become daemon-owned operations through the broker.
77- PersistMutation bool
78- // Notes records cutover direction and non-obvious boundaries.
79- Notes string
80-}
81-
82-// connectivityMatrix classifies every cmdKind exactly once.
83-var connectivityMatrix = []connectivityEntry{
84- {
85- Kind: "kindAttach",
86- Summary: "interactive terminal attach/new/ephemeral, incl. remote attach, detached creation, and the UI-driver/web path",
87- Owner: connectivityBrokerOnly,
88- DirectDialDebt: []string{
89- "ensureDaemonWithLifecycle detached creation (run.go createDetachedLocalSession)",
90- },
91- Notes: "The ordinary terminal path is already broker-only: runAttach translates the parsed CLI target into client.InitialNavigation/Resolver, connect-or-spawns the per-user broker, and delegates to the shared runBrokerClient (run.go, broker_client.go). The old direct-dialer attach composition and its attach preflight and client host registry are removed; the UI-driver and browser compositions delegate to the shared broker client. The only remaining debt is the nested-session detached creation path, which still dials the local daemon directly and is expressed as broker navigation for ordinary attach.",
92- },
93- {
94- Kind: "kindList",
95- Summary: "session list: local, per-host, and --all",
96- Owner: connectivityBrokerOnly,
97- Migrated: true,
98- Notes: "Cutover complete: runList reaches the per-user broker through the private connectBroker seam (run.go) and reads BrokerOperations.List for the local list or runBrokerSnapshotList for per-host/--all. It keeps no persist read, no daemon dialer, and no direct-dial list path. Remote observation never attaches.",
99- },
100- {
101- Kind: "kindHost",
102- Summary: "host add/rm/list",
103- Owner: connectivityBrokerOnly,
104- DirectDialDebt: []string{"remoteHostDeps.hostStore (remote_hosts.go hostAdd/hostRm)"},
105- HostStoreWrite: true,
106- Notes: "Second host-store writer today; broker is the sole writer after cutover. Host list is store-only (no dial).",
107- },
108- {
109- Kind: "kindKill",
110- Summary: "kill one session, kill-all, explicit daemon-stop",
111- Owner: connectivityBrokerOnly,
112- Migrated: true,
113- Notes: "Cutover complete: runKill and requestDaemonStop reach the per-user broker through the private connectBroker seam (run.go) and drive BrokerOperations.Kill/KillAll/StopDaemon. Daemon-stop uses ExistingOnly, so it can never start what it is stopping; kill-all purges sessions and leaves the daemon running. The offline persisted-mutation path and the direct force-stop fallback are removed, so no request is ever bypassed with a direct process signal.",
114- },
115- {
116- Kind: "kindCmd",
117- Summary: "scriptable command requests, incl. remote-catalog",
118- Owner: connectivityBrokerOnly,
119- Migrated: true,
120- Notes: "Command requests use broker operations in production; the injected direct dial seam remains for focused legacy tracker tests.",
121- },
122- {
123- Kind: "kindUIDriver",
124- Summary: "headless UI-driver attach",
125- Owner: connectivityBrokerOnly,
126- Migrated: true,
127- Notes: "Cutover complete: the driver composes the process' production broker connector and the shared terminal initial-navigation translation through runUIDriverClient. It keeps no launch configuration, no dialer, no endpoint, and no daemon ownership.",
128- },
129- {
130- Kind: "kindUIRemoteCleanup",
131- Summary: "UI fixture cleanup through broker control",
132- Owner: connectivityBrokerOnly,
133- Migrated: true,
134- Notes: "Cleanup uses the broker-owned daemon-stop operation and keeps no direct daemon carriage.",
135- },
136- {
137- Kind: "kindWebDaemon",
138- Summary: "browser gateway launcher",
139- Owner: connectivityBrokerOnly,
140- Migrated: true,
141- Notes: "Launcher mechanics stay local. Cutover complete: the gateway server composes the shared runBrokerClient over the process' production broker connector (web_daemon.go runWebTerminalClient), so gateway session traffic goes through the broker like any client. The launcher keeps no dialer, no session carriage, and no daemon ownership.",
142- },
143- {
144- Kind: "kindWebServe",
145- Summary: "browser gateway server",
146- Owner: connectivityBrokerOnly,
147- Migrated: true,
148- Notes: "Cutover complete: every authenticated WebSocket owns exactly one runBrokerClient over the shared production broker connector, with the virtual webterm.Terminal as that run's terminal, the no-argument local ephemeral creation as its closed initial navigation, and the standard presentation callbacks. It keeps no direct daemon dialer, no sessionwire carriage, no remote factory, and no launch configuration; disconnect cancels only that supervisor's service and logical stream.",
149- },
150- {
151- Kind: "kindDaemon",
152- Summary: "daemon process: owns sessions, serves typed connections",
153- Owner: connectivityTransportInfra,
154- Notes: "Daemon accepts one typed connection per broker logical stream after cutover. Never aggregates other daemons, never owns the host registry (P7 removal set).",
155- },
156- {
157- Kind: "kindDaemonLauncher",
158- Summary: "short-lived daemon spawn launcher",
159- Owner: connectivityTransportInfra,
160- Notes: "Process mechanics only; never a connectivity façade.",
161- },
162- {
163- Kind: "kindWebRenew",
164- Summary: "browser token renewal",
165- Owner: connectivityLocalOnly,
166- Notes: "Local control only; no daemon or session connectivity.",
167- },
168- {
169- Kind: "kindBrokerReady",
170- Summary: "hidden dial-only broker readiness probe",
171- Owner: connectivityLocalOnly,
172- Notes: "Dials only the selected broker IPC socket, registers, subscribes, and reads snapshots; it never ensures, spawns, opens a logical stream, reconciles, or mutates.",
173- },
174- {
175- Kind: "kindHelp",
176- Summary: "usage text",
177- Owner: connectivityLocalOnly,
178- Notes: "No connectivity.",
179- },
180- {
181- Kind: "kindVersion",
182- Summary: "version line",
183- Owner: connectivityLocalOnly,
184- Notes: "No connectivity.",
185- },
186- {
187- Kind: "kindBrokerServe",
188- Summary: "hidden _broker-serve isolated offline broker sandbox",
189- Owner: connectivityLocalOnly,
190- Notes: "Foreground sandbox process over its own private root: no production runtime/state, no daemon dial, no RemoteHostStore, and no session persistence. It composes the broker under a temporary offline config and does not become a client façade or an ordinary command.",
191- },
192- {
193- Kind: "kindProductionBrokerServe",
194- Summary: "hidden production broker server",
195- Owner: connectivityLocalOnly,
196- Notes: "Foreground production broker using only productionBrokerLayout and productionBrokerConfigPath; it accepts no offline root.",
197- },
198- {
199- Kind: "kindBrokerClient",
200- Summary: "hidden _broker-client autonomous offline client harness",
201- Owner: connectivityLocalOnly,
202- Notes: "Explicit offline-root composition only: real broker IPC and logical streams exercise terminal and UI harnesses without changing any ordinary command or production path.",
203- },
204- {
205- Kind: "kindBrokerLauncher",
206- Summary: "hidden _broker-launcher detached offline broker launcher",
207- Owner: connectivityLocalOnly,
208- Notes: "Process mechanics only, scoped to one operator-supplied offline root: it starts _broker-serve in a new session and exits, like the daemon launcher, and never dials a daemon, owns the host store, or mutates persistence.",
209- },
210- {
211- Kind: "kindProductionBrokerLauncher",
212- Summary: "hidden detached production broker launcher",
213- Owner: connectivityLocalOnly,
214- Notes: "Process mechanics for the production broker only; it accepts no offline root and starts the production serve role in a new session.",
215- },
216- {
217- Kind: "kindBrokerStatus",
218- Summary: "hidden _broker-status offline broker probe and connect-or-spawn",
219- Owner: connectivityLocalOnly,
220- Notes: "Reports the offline broker's local IPC endpoint and, with --ensure, elects one spawner under a descriptor-backed lock. It dials only the operator-supplied broker sandbox socket (never a daemon or RemoteHostStore) and never becomes a client façade or ordinary command.",
221- },
222- {
223- Kind: "kindBrokerMuxStdio",
224- Summary: "hidden _broker-mux-stdio remote SSH stdio daemonmux bridge",
225- Owner: connectivityTransportInfra,
226- DirectDialDebt: []string{
227- "ipc.DialMuxContext local daemonmux carriage (broker_offline_mux.go)",
228- },
229- Notes: "Remote-side helper over one operator-supplied offline root: it bridges its own stdio to the single provisioned private Unix daemonmux carriage. It never starts a broker, observer, or recursive helper, never dials the production daemon socket, and never fabricates a daemon incarnation. It starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and only when its own provisioned policy permits launching; otherwise it dials only an existing carriage. Direct-dial debt is recorded here rather than as broker debt because the helper itself stays transport infra.",
230- },
231- {
232- Kind: "kindBrokerMuxQUICBootstrap",
233- Summary: "hidden _broker-mux-quic-bootstrap remote SSH-side QUIC bootstrap",
234- Owner: connectivityTransportInfra,
235- Notes: "Starts one detached _broker-mux-quic-proxy in a new session, forwards its single readiness line, and exits. Its only child is that proxy: it never starts a broker, observer, or ordinary daemon, and one-time QUIC credentials never reach an address, log, or error.",
236- },
237- {
238- Kind: "kindBrokerMuxQUICProxy",
239- Summary: "hidden _broker-mux-quic-proxy detached remote QUIC daemonmux bridge",
240- Owner: connectivityTransportInfra,
241- DirectDialDebt: []string{
242- "ipc.DialMuxContext local daemonmux carriage (broker_offline_mux.go)",
243- },
244- Notes: "Mints one ephemeral authenticated QUIC server, admits exactly one carriage, and bridges it to the single provisioned private Unix daemonmux carriage. It never dials the production daemon socket, never fabricates a daemon incarnation, and starts the daemon behind that carriage only under the propagated --daemon-start if-needed authorization and its own provisioned launch policy.",
245- },
246-}
D internal/app/connectivity_matrix_test.go +0 -331
@@ -1,331 +0,0 @@
1-package app
2-
3-import (
4- "go/ast"
5- "go/parser"
6- "go/token"
7- "os"
8- "path/filepath"
9- "regexp"
10- "sort"
11- "strings"
12- "testing"
13-
14- "github.com/stretchr/testify/require"
15-)
16-
17-// testConnectivityOwnerFor looks up the matrix owner for a cmdKind identifier
18-// directly against connectivityMatrix, the same declarative fixture every
19-// other pinning test in this file ranges over.
20-func testConnectivityOwnerFor(kind string) (connectivityOwner, bool) {
21- for _, entry := range connectivityMatrix {
22- if entry.Kind == kind {
23- return entry.Owner, true
24- }
25- }
26- return 0, false
27-}
28-
29-// TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind
30-// constant declared in run.go has exactly one matrix row. A new CLI entry
31-// point without a row fails here before any transport work can ignore it.
32-func TestConnectivityMatrixCoversEveryCommandKind(t *testing.T) {
33- t.Parallel()
34-
35- kinds := commandKindsFromRunGo(t)
36- require.NotEmpty(t, kinds, "run.go must declare cmdKind constants")
37-
38- seen := make(map[string]int, len(connectivityMatrix))
39- for _, entry := range connectivityMatrix {
40- require.NotEmpty(t, entry.Kind, "matrix row must name a cmdKind")
41- require.NotEmpty(t, entry.Summary, "matrix row %q must summarize the operation", entry.Kind)
42- require.NotEmpty(t, entry.Notes, "matrix row %q must record cutover direction", entry.Kind)
43- seen[entry.Kind]++
44- }
45- for kind, count := range seen {
46- require.Equal(t, 1, count, "matrix row %q must appear exactly once", kind)
47- }
48- var missing, extra []string
49- for _, kind := range kinds {
50- if _, ok := seen[kind]; !ok {
51- missing = append(missing, kind)
52- }
53- }
54- kindSet := make(map[string]struct{}, len(kinds))
55- for _, kind := range kinds {
56- kindSet[kind] = struct{}{}
57- }
58- for kind := range seen {
59- if _, ok := kindSet[kind]; !ok {
60- extra = append(extra, kind)
61- }
62- }
63- sort.Strings(missing)
64- sort.Strings(extra)
65- require.Empty(t, missing, "matrix is missing cmdKind rows")
66- require.Empty(t, extra, "matrix references unknown cmdKind rows")
67-
68- owner, ok := testConnectivityOwnerFor("kindAttach")
69- require.True(t, ok, "matrix lookup must resolve known kinds")
70- require.Equal(t, connectivityBrokerOnly, owner)
71- _, ok = testConnectivityOwnerFor("kindDoesNotExist")
72- require.False(t, ok, "matrix lookup must reject unknown kinds")
73-}
74-
75-// TestConnectivityMatrixOwnershipRules pins the P1.2 ownership invariants:
76-// broker-only rows must declare migration debt, infra rows must never gain
77-// store or persistence ownership, and local-only rows carry no debt.
78-func TestConnectivityMatrixOwnershipRules(t *testing.T) {
79- t.Parallel()
80-
81- tests := make([]struct {
82- name string
83- entry connectivityEntry
84- }, 0, len(connectivityMatrix))
85- for _, entry := range connectivityMatrix {
86- tests = append(tests, struct {
87- name string
88- entry connectivityEntry
89- }{name: entry.Kind, entry: entry})
90- }
91- for _, tt := range tests {
92- tt := tt
93- t.Run(tt.name, func(t *testing.T) {
94- t.Parallel()
95- entry := tt.entry
96- switch entry.Owner {
97- case connectivityBrokerOnly:
98- hasDebt := len(entry.DirectDialDebt) > 0 || entry.HostStoreWrite || entry.PersistMutation
99- if entry.Migrated {
100- require.False(t, hasDebt, "migrated broker-only %q must declare no remaining migration debt", entry.Kind)
101- break
102- }
103- require.True(t, hasDebt, "broker-only %q must declare migration debt (direct dial, host-store write, or persist mutation)", entry.Kind)
104- case connectivityTransportInfra:
105- require.False(t, entry.HostStoreWrite, "infra %q must never own the host store", entry.Kind)
106- require.False(t, entry.PersistMutation, "infra %q must never own session persistence", entry.Kind)
107- if len(entry.DirectDialDebt) > 0 {
108- require.NotEmpty(t, entry.Notes, "infra %q with local dial debt must justify why it stays infra", entry.Kind)
109- }
110- case connectivityLocalOnly:
111- require.Empty(t, entry.DirectDialDebt, "local-only %q must carry no dial debt", entry.Kind)
112- require.False(t, entry.HostStoreWrite, "local-only %q must never own the host store", entry.Kind)
113- require.False(t, entry.PersistMutation, "local-only %q must never own session persistence", entry.Kind)
114- default:
115- t.Fatalf("row %q has unknown owner %v", entry.Kind, entry.Owner)
116- }
117- for _, debt := range entry.DirectDialDebt {
118- require.NotEmpty(t, debt, "row %q has an empty debt entry", entry.Kind)
119- }
120- })
121- }
122-}
123-
124-// TestConnectivityMatrixDebtFilesExist keeps the matrix honest: every file
125-// named in a debt entry must exist under internal/app.
126-func TestConnectivityMatrixDebtFilesExist(t *testing.T) {
127- t.Parallel()
128-
129- fileRef := regexp.MustCompile(`([a-z_]+\.go)`)
130- for _, entry := range connectivityMatrix {
131- for _, debt := range entry.DirectDialDebt {
132- for _, match := range fileRef.FindAllStringSubmatch(debt, -1) {
133- path := filepath.Join("internal", "app", match[1])
134- // Tests run with the package dir as CWD, so resolve from there.
135- if _, err := os.Stat(match[1]); err == nil {
136- continue
137- }
138- if _, err := os.Stat(path); err == nil {
139- continue
140- }
141- t.Errorf("row %q references missing file %q (debt %q)", entry.Kind, match[1], debt)
142- }
143- }
144- }
145-}
146-
147-// TestConnectivityMatrixCurrentDebtPresent pins today's direct-connectivity
148-// debt so the P7 cutover can find every site it must remove in the same
149-// change that activates the broker owner. If any of these symbols disappear,
150-// update the matrix in the same change.
151-func TestConnectivityMatrixCurrentDebtPresent(t *testing.T) {
152- t.Parallel()
153-
154- sources := loadAppSources(t, false)
155-
156- assertContains := func(symbol, file string) {
157- t.Helper()
158- body, ok := sources[file]
159- require.True(t, ok, "expected app source file %q", file)
160- require.Contains(t, body, symbol, "expected debt symbol %q in %q", symbol, file)
161- }
162-
163- assertContains("func ensureDaemonWithLifecycle", "spawn.go")
164- assertContains("func realDial", "spawn.go")
165- assertContains("func realSpawn", "spawn.go")
166-}
167-
168-// TestConnectivityMatrixListKillStopAreBrokerOnly pins the P7.4f cutover for
169-// the list/kill/stop rows: each reaches the per-user broker through the private
170-// connectBroker seam and keeps no removed bypass, so runList and runKill/stop
171-// can never read durable state or signal a process directly.
172-func TestConnectivityMatrixListKillStopAreBrokerOnly(t *testing.T) {
173- t.Parallel()
174-
175- sources := loadAppSources(t, false)
176- runBody, ok := sources["run.go"]
177- require.True(t, ok, "expected app source file run.go")
178- require.Contains(t, runBody, "var connectBroker = connectProductionBroker",
179- "run.go must keep the private connectBroker composition seam")
180- require.Contains(t, runBody, "func runList", "run.go must keep runList")
181- require.Contains(t, runBody, "func runKill", "run.go must keep runKill")
182- require.Contains(t, runBody, "func requestDaemonStop", "run.go must keep requestDaemonStop")
183-
184- forbiddenFiles := map[string][]string{
185- "run.go": {
186- "runOfflineNamedKill", "persist.LoadReadOnly", "forceStopDaemonFallback",
187- "forceStopDaemonFn", "waitForDaemonOrLifecycle", "waitForLifecycleAvailability",
188- },
189- }
190- for file, forbidden := range forbiddenFiles {
191- body, ok := sources[file]
192- require.True(t, ok, "expected app source file %q", file)
193- for _, symbol := range forbidden {
194- require.NotContains(t, body, symbol,
195- "%s must not reintroduce the removed bypass %q", file, symbol)
196- }
197- }
198-
199- // The whole force-stop subsystem is removed, so no production file may
200- // resurrect an interactive process-signal fallback.
201- for file, body := range sources {
202- require.NotContains(t, body, "forceStop",
203- "the force-stop subsystem must stay removed (%s)", file)
204- }
205-}
206-
207-// TestConnectivityMatrixSingleHostStoreWriterSet fails when a new
208-// RemoteHostStore constructor call site appears outside the declared set.
209-// After cutover the broker is the sole writer; until then the matrix must
210-// name every writer so none is missed.
211-func TestConnectivityMatrixSingleHostStoreWriterSet(t *testing.T) {
212- t.Parallel()
213-
214- sources := loadAppSources(t, false)
215- var writers []string
216- for file, body := range sources {
217- if strings.Contains(body, "NewFileHostStore") {
218- writers = append(writers, file)
219- }
220- }
221- sort.Strings(writers)
222- require.Empty(t, writers,
223- "the broker is the sole host-store owner; app must not construct another writer")
224-}
225-
226-// TestConnectivityMatrixReadyProbeIsLocalDialOnly pins the kindBrokerReady row
227-// and its production source: the hidden readiness probe is local-only, reaches
228-// no daemon or store, and its production body dials, subscribes, and reads only.
229-// It must never ensure or spawn a broker, open a logical stream, request a
230-// reconcile, or mutate membership; each of those would turn a machine probe
231-// into a client façade that changes the state it reports.
232-func TestConnectivityMatrixReadyProbeIsLocalDialOnly(t *testing.T) {
233- t.Parallel()
234-
235- owner, ok := testConnectivityOwnerFor("kindBrokerReady")
236- require.True(t, ok, "the readiness probe must have a matrix row")
237- require.Equal(t, connectivityLocalOnly, owner, "the readiness probe is dial-only local infrastructure")
238- for _, entry := range connectivityMatrix {
239- if entry.Kind != "kindBrokerReady" {
240- continue
241- }
242- require.Empty(t, entry.DirectDialDebt, "a dial-only probe owes no connectivity migration")
243- require.False(t, entry.HostStoreWrite, "the readiness probe never owns the host store")
244- require.False(t, entry.PersistMutation, "the readiness probe never mutates session persistence")
245- }
246-
247- sources := loadAppSources(t, false)
248- body, ok := sources["broker_ready.go"]
249- require.True(t, ok, "expected app source file broker_ready.go")
250-
251- forbidden := []struct {
252- symbol string
253- reason string
254- }{
255- {"ensureBrokerReady", "a readiness probe must never ensure a broker"},
256- {"ensureDaemonWithLifecycle", "a readiness probe must never start a daemon"},
257- {"spawnBrokerLauncher", "a readiness probe must never spawn a broker"},
258- {"spawnProductionBrokerLauncher", "a readiness probe must never spawn a broker"},
259- {"realDial", "a readiness probe dials the broker endpoint only"},
260- {"OpenStream", "a readiness probe opens no logical stream"},
261- {"RequestReconcile", "a readiness probe requests no reconcile"},
262- {"AddHost", "a readiness probe mutates no membership"},
263- {"RemoveHost", "a readiness probe mutates no membership"},
264- {"UpdateHostPolicy", "a readiness probe mutates no membership"},
265- {"NewFileHostStore", "a readiness probe owns no host store"},
266- }
267- for _, forbidden := range forbidden {
268- require.NotContains(t, body, forbidden.symbol, "%s (%s)", forbidden.symbol, forbidden.reason)
269- }
270-
271- // Positive pins: the probe is one subscription over one snapshot loop, and
272- // it classifies terminal failures without retrying them.
273- for _, required := range []string{"func runBrokerReady", "func observeBrokerReady", "service.Subscribe()", "service.Snapshot()", "terminalReadyReason", "brokerReadyEndpointSecurity"} {
274- require.Contains(t, body, required, "broker_ready.go must keep %s", required)
275- }
276-}
277-
278-// commandKindsFromRunGo extracts the kind* const names from run.go so the
279-// matrix test tracks the real dispatch table instead of a hardcoded copy.
280-func commandKindsFromRunGo(t *testing.T) []string {
281- t.Helper()
282-
283- fset := token.NewFileSet()
284- file, err := parser.ParseFile(fset, "run.go", nil, 0)
285- require.NoError(t, err, "parse run.go for cmdKind constants")
286-
287- var kinds []string
288- for _, decl := range file.Decls {
289- gen, ok := decl.(*ast.GenDecl)
290- if !ok || gen.Tok != token.CONST {
291- continue
292- }
293- for _, spec := range gen.Specs {
294- value, ok := spec.(*ast.ValueSpec)
295- if !ok {
296- continue
297- }
298- for _, name := range value.Names {
299- if strings.HasPrefix(name.Name, "kind") {
300- kinds = append(kinds, name.Name)
301- }
302- }
303- }
304- }
305- sort.Strings(kinds)
306- return kinds
307-}
308-
309-// loadAppSources reads production (non-test) .go files in this package.
310-// When includeTests is false, _test.go files are skipped.
311-func loadAppSources(t *testing.T, includeTests bool) map[string]string {
312- t.Helper()
313-
314- entries, err := os.ReadDir(".")
315- require.NoError(t, err, "read app package dir")
316- sources := make(map[string]string)
317- for _, entry := range entries {
318- name := entry.Name()
319- if entry.IsDir() || !strings.HasSuffix(name, ".go") {
320- continue
321- }
322- if !includeTests && strings.HasSuffix(name, "_test.go") {
323- continue
324- }
325- raw, err := os.ReadFile(name)
326- require.NoError(t, err, "read app source %q", name)
327- sources[name] = string(raw)
328- }
329- require.NotEmpty(t, sources, "app sources must not be empty")
330- return sources
331-}
@@ -1,331 +0,0 @@
1-package app
2-
3-import (
4- "go/ast"
5- "go/parser"
6- "go/token"
7- "os"
8- "path/filepath"
9- "regexp"
10- "sort"
11- "strings"
12- "testing"
13-
14- "github.com/stretchr/testify/require"
15-)
16-
17-// testConnectivityOwnerFor looks up the matrix owner for a cmdKind identifier
18-// directly against connectivityMatrix, the same declarative fixture every
19-// other pinning test in this file ranges over.
20-func testConnectivityOwnerFor(kind string) (connectivityOwner, bool) {
21- for _, entry := range connectivityMatrix {
22- if entry.Kind == kind {
23- return entry.Owner, true
24- }
25- }
26- return 0, false
27-}
28-
29-// TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind
30-// constant declared in run.go has exactly one matrix row. A new CLI entry
31-// point without a row fails here before any transport work can ignore it.
32-func TestConnectivityMatrixCoversEveryCommandKind(t *testing.T) {
33- t.Parallel()
34-
35- kinds := commandKindsFromRunGo(t)
36- require.NotEmpty(t, kinds, "run.go must declare cmdKind constants")
37-
38- seen := make(map[string]int, len(connectivityMatrix))
39- for _, entry := range connectivityMatrix {
40- require.NotEmpty(t, entry.Kind, "matrix row must name a cmdKind")
41- require.NotEmpty(t, entry.Summary, "matrix row %q must summarize the operation", entry.Kind)
42- require.NotEmpty(t, entry.Notes, "matrix row %q must record cutover direction", entry.Kind)
43- seen[entry.Kind]++
44- }
45- for kind, count := range seen {
46- require.Equal(t, 1, count, "matrix row %q must appear exactly once", kind)
47- }
48- var missing, extra []string
49- for _, kind := range kinds {
50- if _, ok := seen[kind]; !ok {
51- missing = append(missing, kind)
52- }
53- }
54- kindSet := make(map[string]struct{}, len(kinds))
55- for _, kind := range kinds {
56- kindSet[kind] = struct{}{}
57- }
58- for kind := range seen {
59- if _, ok := kindSet[kind]; !ok {
60- extra = append(extra, kind)
61- }
62- }
63- sort.Strings(missing)
64- sort.Strings(extra)
65- require.Empty(t, missing, "matrix is missing cmdKind rows")
66- require.Empty(t, extra, "matrix references unknown cmdKind rows")
67-
68- owner, ok := testConnectivityOwnerFor("kindAttach")
69- require.True(t, ok, "matrix lookup must resolve known kinds")
70- require.Equal(t, connectivityBrokerOnly, owner)
71- _, ok = testConnectivityOwnerFor("kindDoesNotExist")
72- require.False(t, ok, "matrix lookup must reject unknown kinds")
73-}
74-
75-// TestConnectivityMatrixOwnershipRules pins the P1.2 ownership invariants:
76-// broker-only rows must declare migration debt, infra rows must never gain
77-// store or persistence ownership, and local-only rows carry no debt.
78-func TestConnectivityMatrixOwnershipRules(t *testing.T) {
79- t.Parallel()
80-
81- tests := make([]struct {
82- name string
83- entry connectivityEntry
84- }, 0, len(connectivityMatrix))
85- for _, entry := range connectivityMatrix {
86- tests = append(tests, struct {
87- name string
88- entry connectivityEntry
89- }{name: entry.Kind, entry: entry})
90- }
91- for _, tt := range tests {
92- tt := tt
93- t.Run(tt.name, func(t *testing.T) {
94- t.Parallel()
95- entry := tt.entry
96- switch entry.Owner {
97- case connectivityBrokerOnly:
98- hasDebt := len(entry.DirectDialDebt) > 0 || entry.HostStoreWrite || entry.PersistMutation
99- if entry.Migrated {
100- require.False(t, hasDebt, "migrated broker-only %q must declare no remaining migration debt", entry.Kind)
101- break
102- }
103- require.True(t, hasDebt, "broker-only %q must declare migration debt (direct dial, host-store write, or persist mutation)", entry.Kind)
104- case connectivityTransportInfra:
105- require.False(t, entry.HostStoreWrite, "infra %q must never own the host store", entry.Kind)
106- require.False(t, entry.PersistMutation, "infra %q must never own session persistence", entry.Kind)
107- if len(entry.DirectDialDebt) > 0 {
108- require.NotEmpty(t, entry.Notes, "infra %q with local dial debt must justify why it stays infra", entry.Kind)
109- }
110- case connectivityLocalOnly:
111- require.Empty(t, entry.DirectDialDebt, "local-only %q must carry no dial debt", entry.Kind)
112- require.False(t, entry.HostStoreWrite, "local-only %q must never own the host store", entry.Kind)
113- require.False(t, entry.PersistMutation, "local-only %q must never own session persistence", entry.Kind)
114- default:
115- t.Fatalf("row %q has unknown owner %v", entry.Kind, entry.Owner)
116- }
117- for _, debt := range entry.DirectDialDebt {
118- require.NotEmpty(t, debt, "row %q has an empty debt entry", entry.Kind)
119- }
120- })
121- }
122-}
123-
124-// TestConnectivityMatrixDebtFilesExist keeps the matrix honest: every file
125-// named in a debt entry must exist under internal/app.
126-func TestConnectivityMatrixDebtFilesExist(t *testing.T) {
127- t.Parallel()
128-
129- fileRef := regexp.MustCompile(`([a-z_]+\.go)`)
130- for _, entry := range connectivityMatrix {
131- for _, debt := range entry.DirectDialDebt {
132- for _, match := range fileRef.FindAllStringSubmatch(debt, -1) {
133- path := filepath.Join("internal", "app", match[1])
134- // Tests run with the package dir as CWD, so resolve from there.
135- if _, err := os.Stat(match[1]); err == nil {
136- continue
137- }
138- if _, err := os.Stat(path); err == nil {
139- continue
140- }
141- t.Errorf("row %q references missing file %q (debt %q)", entry.Kind, match[1], debt)
142- }
143- }
144- }
145-}
146-
147-// TestConnectivityMatrixCurrentDebtPresent pins today's direct-connectivity
148-// debt so the P7 cutover can find every site it must remove in the same
149-// change that activates the broker owner. If any of these symbols disappear,
150-// update the matrix in the same change.
151-func TestConnectivityMatrixCurrentDebtPresent(t *testing.T) {
152- t.Parallel()
153-
154- sources := loadAppSources(t, false)
155-
156- assertContains := func(symbol, file string) {
157- t.Helper()
158- body, ok := sources[file]
159- require.True(t, ok, "expected app source file %q", file)
160- require.Contains(t, body, symbol, "expected debt symbol %q in %q", symbol, file)
161- }
162-
163- assertContains("func ensureDaemonWithLifecycle", "spawn.go")
164- assertContains("func realDial", "spawn.go")
165- assertContains("func realSpawn", "spawn.go")
166-}
167-
168-// TestConnectivityMatrixListKillStopAreBrokerOnly pins the P7.4f cutover for
169-// the list/kill/stop rows: each reaches the per-user broker through the private
170-// connectBroker seam and keeps no removed bypass, so runList and runKill/stop
171-// can never read durable state or signal a process directly.
172-func TestConnectivityMatrixListKillStopAreBrokerOnly(t *testing.T) {
173- t.Parallel()
174-
175- sources := loadAppSources(t, false)
176- runBody, ok := sources["run.go"]
177- require.True(t, ok, "expected app source file run.go")
178- require.Contains(t, runBody, "var connectBroker = connectProductionBroker",
179- "run.go must keep the private connectBroker composition seam")
180- require.Contains(t, runBody, "func runList", "run.go must keep runList")
181- require.Contains(t, runBody, "func runKill", "run.go must keep runKill")
182- require.Contains(t, runBody, "func requestDaemonStop", "run.go must keep requestDaemonStop")
183-
184- forbiddenFiles := map[string][]string{
185- "run.go": {
186- "runOfflineNamedKill", "persist.LoadReadOnly", "forceStopDaemonFallback",
187- "forceStopDaemonFn", "waitForDaemonOrLifecycle", "waitForLifecycleAvailability",
188- },
189- }
190- for file, forbidden := range forbiddenFiles {
191- body, ok := sources[file]
192- require.True(t, ok, "expected app source file %q", file)
193- for _, symbol := range forbidden {
194- require.NotContains(t, body, symbol,
195- "%s must not reintroduce the removed bypass %q", file, symbol)
196- }
197- }
198-
199- // The whole force-stop subsystem is removed, so no production file may
200- // resurrect an interactive process-signal fallback.
201- for file, body := range sources {
202- require.NotContains(t, body, "forceStop",
203- "the force-stop subsystem must stay removed (%s)", file)
204- }
205-}
206-
207-// TestConnectivityMatrixSingleHostStoreWriterSet fails when a new
208-// RemoteHostStore constructor call site appears outside the declared set.
209-// After cutover the broker is the sole writer; until then the matrix must
210-// name every writer so none is missed.
211-func TestConnectivityMatrixSingleHostStoreWriterSet(t *testing.T) {
212- t.Parallel()
213-
214- sources := loadAppSources(t, false)
215- var writers []string
216- for file, body := range sources {
217- if strings.Contains(body, "NewFileHostStore") {
218- writers = append(writers, file)
219- }
220- }
221- sort.Strings(writers)
222- require.Empty(t, writers,
223- "the broker is the sole host-store owner; app must not construct another writer")
224-}
225-
226-// TestConnectivityMatrixReadyProbeIsLocalDialOnly pins the kindBrokerReady row
227-// and its production source: the hidden readiness probe is local-only, reaches
228-// no daemon or store, and its production body dials, subscribes, and reads only.
229-// It must never ensure or spawn a broker, open a logical stream, request a
230-// reconcile, or mutate membership; each of those would turn a machine probe
231-// into a client façade that changes the state it reports.
232-func TestConnectivityMatrixReadyProbeIsLocalDialOnly(t *testing.T) {
233- t.Parallel()
234-
235- owner, ok := testConnectivityOwnerFor("kindBrokerReady")
236- require.True(t, ok, "the readiness probe must have a matrix row")
237- require.Equal(t, connectivityLocalOnly, owner, "the readiness probe is dial-only local infrastructure")
238- for _, entry := range connectivityMatrix {
239- if entry.Kind != "kindBrokerReady" {
240- continue
241- }
242- require.Empty(t, entry.DirectDialDebt, "a dial-only probe owes no connectivity migration")
243- require.False(t, entry.HostStoreWrite, "the readiness probe never owns the host store")
244- require.False(t, entry.PersistMutation, "the readiness probe never mutates session persistence")
245- }
246-
247- sources := loadAppSources(t, false)
248- body, ok := sources["broker_ready.go"]
249- require.True(t, ok, "expected app source file broker_ready.go")
250-
251- forbidden := []struct {
252- symbol string
253- reason string
254- }{
255- {"ensureBrokerReady", "a readiness probe must never ensure a broker"},
256- {"ensureDaemonWithLifecycle", "a readiness probe must never start a daemon"},
257- {"spawnBrokerLauncher", "a readiness probe must never spawn a broker"},
258- {"spawnProductionBrokerLauncher", "a readiness probe must never spawn a broker"},
259- {"realDial", "a readiness probe dials the broker endpoint only"},
260- {"OpenStream", "a readiness probe opens no logical stream"},
261- {"RequestReconcile", "a readiness probe requests no reconcile"},
262- {"AddHost", "a readiness probe mutates no membership"},
263- {"RemoveHost", "a readiness probe mutates no membership"},
264- {"UpdateHostPolicy", "a readiness probe mutates no membership"},
265- {"NewFileHostStore", "a readiness probe owns no host store"},
266- }
267- for _, forbidden := range forbidden {
268- require.NotContains(t, body, forbidden.symbol, "%s (%s)", forbidden.symbol, forbidden.reason)
269- }
270-
271- // Positive pins: the probe is one subscription over one snapshot loop, and
272- // it classifies terminal failures without retrying them.
273- for _, required := range []string{"func runBrokerReady", "func observeBrokerReady", "service.Subscribe()", "service.Snapshot()", "terminalReadyReason", "brokerReadyEndpointSecurity"} {
274- require.Contains(t, body, required, "broker_ready.go must keep %s", required)
275- }
276-}
277-
278-// commandKindsFromRunGo extracts the kind* const names from run.go so the
279-// matrix test tracks the real dispatch table instead of a hardcoded copy.
280-func commandKindsFromRunGo(t *testing.T) []string {
281- t.Helper()
282-
283- fset := token.NewFileSet()
284- file, err := parser.ParseFile(fset, "run.go", nil, 0)
285- require.NoError(t, err, "parse run.go for cmdKind constants")
286-
287- var kinds []string
288- for _, decl := range file.Decls {
289- gen, ok := decl.(*ast.GenDecl)
290- if !ok || gen.Tok != token.CONST {
291- continue
292- }
293- for _, spec := range gen.Specs {
294- value, ok := spec.(*ast.ValueSpec)
295- if !ok {
296- continue
297- }
298- for _, name := range value.Names {
299- if strings.HasPrefix(name.Name, "kind") {
300- kinds = append(kinds, name.Name)
301- }
302- }
303- }
304- }
305- sort.Strings(kinds)
306- return kinds
307-}
308-
309-// loadAppSources reads production (non-test) .go files in this package.
310-// When includeTests is false, _test.go files are skipped.
311-func loadAppSources(t *testing.T, includeTests bool) map[string]string {
312- t.Helper()
313-
314- entries, err := os.ReadDir(".")
315- require.NoError(t, err, "read app package dir")
316- sources := make(map[string]string)
317- for _, entry := range entries {
318- name := entry.Name()
319- if entry.IsDir() || !strings.HasSuffix(name, ".go") {
320- continue
321- }
322- if !includeTests && strings.HasSuffix(name, "_test.go") {
323- continue
324- }
325- raw, err := os.ReadFile(name)
326- require.NoError(t, err, "read app source %q", name)
327- sources[name] = string(raw)
328- }
329- require.NotEmpty(t, sources, "app sources must not be empty")
330- return sources
331-}
M internal/app/run.go +20 -43
@@ -65,10 +65,6 @@ const (
6565 kindWebDaemon
6666 kindWebRenew
6767 kindWebServe
68- kindBrokerServe
69- kindBrokerClient
70- kindBrokerLauncher
71- kindBrokerStatus
7268 kindBrokerMuxStdio
7369 kindBrokerMuxQUICBootstrap
7470 kindBrokerMuxQUICProxy
@@ -82,29 +78,26 @@ const (
8278 // command is the parsed CLI invocation: what to do, plus the attach intent
8379 // and session name where relevant.
8480 type command struct {
85- kind cmdKind
86- intent uint8
87- name string
88- remoteTarget string
89- listHost string
90- listAll bool
91- hostAction string
92- hostTarget string
93- killAll bool
94- killDaemon bool
95- killBroker bool
96- cmd cmdInvocation
97- brokerServe brokerServeOptions
98- brokerClient brokerClientOptions
99- brokerLauncher brokerLauncherOptions
100- brokerStatus brokerStatusOptions
101- brokerMux brokerMuxOptions
102- brokerReady brokerReadyOptions
103- uiDriver uiDriverOptions
104- uiObserve bool
105- uiControl bool
106- uiSocket string
107- web webOptions
81+ kind cmdKind
82+ intent uint8
83+ name string
84+ remoteTarget string
85+ listHost string
86+ listAll bool
87+ hostAction string
88+ hostTarget string
89+ killAll bool
90+ killDaemon bool
91+ killBroker bool
92+ cmd cmdInvocation
93+ brokerServe brokerServeOptions
94+ brokerMux brokerMuxOptions
95+ brokerReady brokerReadyOptions
96+ uiDriver uiDriverOptions
97+ uiObserve bool
98+ uiControl bool
99+ uiSocket string
100+ web webOptions
108101 }
109102
110103 // usageError is a user-facing argument error; the app prints it (with usage)
@@ -238,18 +231,10 @@ parsedUIFlags:
238231 return command{kind: kindDaemon}, nil
239232 case "--daemon-launcher":
240233 return command{kind: kindDaemonLauncher}, nil
241- case brokerServeCommand:
242- return parseBrokerServeArgs(args[1:])
243234 case productionBrokerServeCommand:
244235 return parseProductionBrokerServeArgs(args[1:])
245- case brokerClientCommand:
246- return parseBrokerClientArgs(args[1:])
247- case brokerLauncherCommand:
248- return parseBrokerLauncherArgs(args[1:])
249236 case productionBrokerLauncherCommand:
250237 return parseProductionBrokerLauncherArgs(args[1:])
251- case brokerStatusCommand:
252- return parseBrokerStatusArgs(args[1:])
253238 case brokerReadyCommand:
254239 options, err := parseBrokerReadyArgs(args[1:])
255240 return command{kind: kindBrokerReady, brokerReady: options}, err
@@ -375,14 +360,6 @@ func dispatch(ctx context.Context, cmd command) error {
375360 return runDaemon()
376361 case kindDaemonLauncher:
377362 return runDaemonLauncher()
378- case kindBrokerServe:
379- return runBrokerServeCommand(ctx, cmd.brokerServe)
380- case kindBrokerClient:
381- return runBrokerClientCommand(ctx, cmd.brokerClient)
382- case kindBrokerLauncher:
383- return runBrokerLauncherCommand(ctx, cmd.brokerLauncher)
384- case kindBrokerStatus:
385- return runBrokerStatusCommand(ctx, cmd.brokerStatus)
386363 case kindBrokerMuxStdio:
387364 return runBrokerMuxStdioCommand(ctx, cmd.brokerMux)
388365 case kindBrokerMuxQUICBootstrap:
@@ -65,10 +65,6 @@ const (
65 kindWebDaemon65 kindWebDaemon
66 kindWebRenew66 kindWebRenew
67 kindWebServe67 kindWebServe
68- kindBrokerServe
69- kindBrokerClient
70- kindBrokerLauncher
71- kindBrokerStatus
72 kindBrokerMuxStdio68 kindBrokerMuxStdio
73 kindBrokerMuxQUICBootstrap69 kindBrokerMuxQUICBootstrap
74 kindBrokerMuxQUICProxy70 kindBrokerMuxQUICProxy
@@ -82,29 +78,26 @@ const (
82 // command is the parsed CLI invocation: what to do, plus the attach intent78 // command is the parsed CLI invocation: what to do, plus the attach intent
83 // and session name where relevant.79 // and session name where relevant.
84 type command struct {80 type command struct {
85- kind cmdKind81+ kind cmdKind
86- intent uint882+ intent uint8
87- name string83+ name string
88- remoteTarget string84+ remoteTarget string
89- listHost string85+ listHost string
90- listAll bool86+ listAll bool
91- hostAction string87+ hostAction string
92- hostTarget string88+ hostTarget string
93- killAll bool89+ killAll bool
94- killDaemon bool90+ killDaemon bool
95- killBroker bool91+ killBroker bool
96- cmd cmdInvocation92+ cmd cmdInvocation
97- brokerServe brokerServeOptions93+ brokerServe brokerServeOptions
98- brokerClient brokerClientOptions94+ brokerMux brokerMuxOptions
99- brokerLauncher brokerLauncherOptions95+ brokerReady brokerReadyOptions
100- brokerStatus brokerStatusOptions96+ uiDriver uiDriverOptions
101- brokerMux brokerMuxOptions97+ uiObserve bool
102- brokerReady brokerReadyOptions98+ uiControl bool
103- uiDriver uiDriverOptions99+ uiSocket string
104- uiObserve bool100+ web webOptions
105- uiControl bool
106- uiSocket string
107- web webOptions
108 }101 }
109 102
110 // usageError is a user-facing argument error; the app prints it (with usage)103 // usageError is a user-facing argument error; the app prints it (with usage)
@@ -238,18 +231,10 @@ parsedUIFlags:
238 return command{kind: kindDaemon}, nil231 return command{kind: kindDaemon}, nil
239 case "--daemon-launcher":232 case "--daemon-launcher":
240 return command{kind: kindDaemonLauncher}, nil233 return command{kind: kindDaemonLauncher}, nil
241- case brokerServeCommand:
242- return parseBrokerServeArgs(args[1:])
243 case productionBrokerServeCommand:234 case productionBrokerServeCommand:
244 return parseProductionBrokerServeArgs(args[1:])235 return parseProductionBrokerServeArgs(args[1:])
245- case brokerClientCommand:
246- return parseBrokerClientArgs(args[1:])
247- case brokerLauncherCommand:
248- return parseBrokerLauncherArgs(args[1:])
249 case productionBrokerLauncherCommand:236 case productionBrokerLauncherCommand:
250 return parseProductionBrokerLauncherArgs(args[1:])237 return parseProductionBrokerLauncherArgs(args[1:])
251- case brokerStatusCommand:
252- return parseBrokerStatusArgs(args[1:])
253 case brokerReadyCommand:238 case brokerReadyCommand:
254 options, err := parseBrokerReadyArgs(args[1:])239 options, err := parseBrokerReadyArgs(args[1:])
255 return command{kind: kindBrokerReady, brokerReady: options}, err240 return command{kind: kindBrokerReady, brokerReady: options}, err
@@ -375,14 +360,6 @@ func dispatch(ctx context.Context, cmd command) error {
375 return runDaemon()360 return runDaemon()
376 case kindDaemonLauncher:361 case kindDaemonLauncher:
377 return runDaemonLauncher()362 return runDaemonLauncher()
378- case kindBrokerServe:
379- return runBrokerServeCommand(ctx, cmd.brokerServe)
380- case kindBrokerClient:
381- return runBrokerClientCommand(ctx, cmd.brokerClient)
382- case kindBrokerLauncher:
383- return runBrokerLauncherCommand(ctx, cmd.brokerLauncher)
384- case kindBrokerStatus:
385- return runBrokerStatusCommand(ctx, cmd.brokerStatus)
386 case kindBrokerMuxStdio:363 case kindBrokerMuxStdio:
387 return runBrokerMuxStdioCommand(ctx, cmd.brokerMux)364 return runBrokerMuxStdioCommand(ctx, cmd.brokerMux)
388 case kindBrokerMuxQUICBootstrap:365 case kindBrokerMuxQUICBootstrap:
M internal/app/spawn_test.go +2 -3
@@ -30,12 +30,11 @@ func TestMain(m *testing.M) {
3030 // runs.
3131 if len(os.Args) >= 2 && os.Getenv(brokerHelperEnv) == "1" {
3232 switch os.Args[1] {
33- case brokerServeCommand, brokerLauncherCommand, brokerStatusCommand,
34- brokerMuxStdioCommand, brokerMuxQUICBootstrapCommand, brokerMuxQUICProxyCommand,
33+ case brokerMuxStdioCommand, brokerMuxQUICBootstrapCommand, brokerMuxQUICProxyCommand,
3534 brokerReadyCommand,
3635 productionBrokerServeCommand, productionBrokerLauncherCommand, "--daemon":
3736 recordBrokerHelperProcess(os.Args[1])
38- if os.Args[1] == brokerLauncherCommand || os.Args[1] == productionBrokerLauncherCommand {
37+ if os.Args[1] == productionBrokerLauncherCommand {
3938 if record := os.Getenv(brokerLauncherBlockRecordEnv); record != "" {
4039 blockBrokerLauncherHelper(record, os.Getenv(brokerLauncherBlockReleaseEnv))
4140 }
@@ -30,12 +30,11 @@ func TestMain(m *testing.M) {
30 // runs.30 // runs.
31 if len(os.Args) >= 2 && os.Getenv(brokerHelperEnv) == "1" {31 if len(os.Args) >= 2 && os.Getenv(brokerHelperEnv) == "1" {
32 switch os.Args[1] {32 switch os.Args[1] {
33- case brokerServeCommand, brokerLauncherCommand, brokerStatusCommand,33+ case brokerMuxStdioCommand, brokerMuxQUICBootstrapCommand, brokerMuxQUICProxyCommand,
34- brokerMuxStdioCommand, brokerMuxQUICBootstrapCommand, brokerMuxQUICProxyCommand,
35 brokerReadyCommand,34 brokerReadyCommand,
36 productionBrokerServeCommand, productionBrokerLauncherCommand, "--daemon":35 productionBrokerServeCommand, productionBrokerLauncherCommand, "--daemon":
37 recordBrokerHelperProcess(os.Args[1])36 recordBrokerHelperProcess(os.Args[1])
38- if os.Args[1] == brokerLauncherCommand || os.Args[1] == productionBrokerLauncherCommand {37+ if os.Args[1] == productionBrokerLauncherCommand {
39 if record := os.Getenv(brokerLauncherBlockRecordEnv); record != "" {38 if record := os.Getenv(brokerLauncherBlockRecordEnv); record != "" {
40 blockBrokerLauncherHelper(record, os.Getenv(brokerLauncherBlockReleaseEnv))39 blockBrokerLauncherHelper(record, os.Getenv(brokerLauncherBlockReleaseEnv))
41 }40 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close