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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/cli/broker_dial_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ import (
func newBrokerHTTPClient(int) *http.Client { return nil }

var errBrokerUnsupported = errors.New("flashduty: broker mode is not supported on this platform")

// brokerEgressCapable reports whether this build can act as a broker-mode client.
// False on non-unix builds, which lack the FLASHDUTY_CRED_FD dial path. The
// runner reads this (via `fduty version --json`) to decide whether to advertise
// broker mode to safari. See broker_dial_unix.go for the rationale.
const brokerEgressCapable = false
7 changes: 7 additions & 0 deletions internal/cli/broker_dial_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import (
// never returns nil), but defaultNewClient references it on every platform.
var errBrokerUnsupported = errors.New("flashduty: broker mode is not supported on this platform")

// brokerEgressCapable reports whether this build can act as a broker-mode client
// (read FLASHDUTY_CRED_FD and dial over the inherited control fd). The runner
// probes it via `fduty version --json` and only advertises broker mode to safari
// when true — otherwise safari would deliver the per-person key out-of-band to an
// fduty that can't read it. True on unix, where this file is built.
const brokerEgressCapable = true

// brokerDialer owns the inherited control fd and serializes per-dial handshakes.
// Each Dial sends a 1-byte request datagram on the control channel and receives
// one dedicated SOCK_STREAM fd back via SCM_RIGHTS.
Expand Down
23 changes: 22 additions & 1 deletion internal/cli/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,28 @@ func newVersionCmd() *cobra.Command {
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("flashduty version %s (%s) built %s\n", versionStr, commitStr, dateStr)
out := cmd.OutOrStdout()
// A structured (--json / --output-format) request emits a
// machine-readable object that includes broker_egress, the capability
// the runner probes before advertising broker mode to safari. An older
// fduty ignores these flags and prints the plain line below, so the
// absence of the broker_egress field reads as "not capable". The plain
// human output is unchanged.
if currentOutputFormat().Structured() {
b, err := marshalStructured(map[string]any{
"version": versionStr,
"commit": commitStr,
"date": dateStr,
"broker_egress": brokerEgressCapable,
})
if err != nil {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), err)
return
}
_, _ = fmt.Fprintln(out, string(b))
return
}
_, _ = fmt.Fprintf(out, "flashduty version %s (%s) built %s\n", versionStr, commitStr, dateStr)
},
}
}
48 changes: 48 additions & 0 deletions internal/cli/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cli

import (
"bytes"
"encoding/json"
"strings"
"testing"
)

// TestVersionPlainOutput keeps the human-readable line stable (the runner's
// install scripts and humans rely on it).
func TestVersionPlainOutput(t *testing.T) {
cmd := newVersionCmd()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.Run(cmd, nil)
if got := buf.String(); !strings.HasPrefix(got, "flashduty version ") {
t.Fatalf("plain version output changed: %q", got)
}
}

// TestVersionJSONAdvertisesBrokerEgress locks the capability contract the runner
// depends on: `fduty version --json` must emit a broker_egress field whose value
// matches this build's compile-time capability (true on unix, false elsewhere).
// If the field went missing, broker-capable runners would stop advertising
// broker mode and silently fall back to the legacy env-key path.
func TestVersionJSONAdvertisesBrokerEgress(t *testing.T) {
origJSON := flagJSON
flagJSON = true
defer func() { flagJSON = origJSON }()

cmd := newVersionCmd()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.Run(cmd, nil)

var got map[string]any
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
t.Fatalf("version --json must be valid JSON, got %q: %v", buf.String(), err)
}
v, ok := got["broker_egress"]
if !ok {
t.Fatalf("version --json must include the broker_egress field, got: %q", buf.String())
}
if v != brokerEgressCapable {
t.Fatalf("broker_egress = %v, want %v (compile-time capability)", v, brokerEgressCapable)
}
}
Loading