Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package k0sctl for openSUSE:Factory checked 
in at 2026-07-07 21:01:30
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/k0sctl (Old)
 and      /work/SRC/openSUSE:Factory/.k0sctl.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "k0sctl"

Tue Jul  7 21:01:30 2026 rev:25 rq:1364054 version:0.32.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/k0sctl/k0sctl.changes    2026-07-03 
16:11:26.862872152 +0200
+++ /work/SRC/openSUSE:Factory/.k0sctl.new.1982/k0sctl.changes  2026-07-07 
21:03:29.370860160 +0200
@@ -1,0 +2,10 @@
+Mon Jul 06 12:35:25 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.32.1:
+  * chore: finetune rig v2 migration note
+  * build(deps): bump github.com/k0sproject/rig/v2 from 2.0.1 to
+    2.1.0 (#1110)
+  * build(deps): bump github.com/k0sproject/rig/v2 from 2.0.0 to
+    2.0.1
+
+-------------------------------------------------------------------

Old:
----
  k0sctl-0.32.0.obscpio

New:
----
  k0sctl-0.32.1.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ k0sctl.spec ++++++
--- /var/tmp/diff_new_pack.CchIsA/_old  2026-07-07 21:03:31.282925731 +0200
+++ /var/tmp/diff_new_pack.CchIsA/_new  2026-07-07 21:03:31.282925731 +0200
@@ -18,7 +18,7 @@
 
 
 Name:           k0sctl
-Version:        0.32.0
+Version:        0.32.1
 Release:        0
 Summary:        A bootstrapping and management tool for k0s clusters
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.CchIsA/_old  2026-07-07 21:03:31.322927103 +0200
+++ /var/tmp/diff_new_pack.CchIsA/_new  2026-07-07 21:03:31.326927241 +0200
@@ -2,7 +2,7 @@
   <service name="obs_scm" mode="manual">
     <param name="url">https://github.com/k0sproject/k0sctl.git</param>
     <param name="scm">git</param>
-    <param name="revision">refs/tags/v0.32.0</param>
+    <param name="revision">refs/tags/v0.32.1</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.CchIsA/_old  2026-07-07 21:03:31.354928200 +0200
+++ /var/tmp/diff_new_pack.CchIsA/_new  2026-07-07 21:03:31.358928338 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/k0sproject/k0sctl.git</param>
-              <param 
name="changesrevision">64fda2ad15fe647fb4c47e1f6ecb1c39b5ea050c</param></service></servicedata>
+              <param 
name="changesrevision">db3dfb711f8b95e6f3bf4297db60dd5fbd337f85</param></service></servicedata>
 (No newline at EOF)
 

++++++ k0sctl-0.32.0.obscpio -> k0sctl-0.32.1.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.32.0/cmd/flags.go 
new/k0sctl-0.32.1/cmd/flags.go
--- old/k0sctl-0.32.0/cmd/flags.go      2026-07-03 08:56:55.000000000 +0200
+++ new/k0sctl-0.32.1/cmd/flags.go      2026-07-06 13:40:55.000000000 +0200
@@ -281,11 +281,11 @@
 func warnRigMigration(ctx *cli.Context) error {
        var warningRows []string
        warningRows = append(warningRows, "")
-       warningRows = append(warningRows, " ▌ This release replaces k0sctl's 
host connection and remote execution")
-       warningRows = append(warningRows, " ▌ layer (rig v2). Behavior should 
be unchanged, but if you hit")
-       warningRows = append(warningRows, " ▌ unexpected connection, sudo, OS 
detection or file transfer issues, please")
-       warningRows = append(warningRows, " ▌ report them at 
https://github.com/k0sproject/k0sctl/issues and roll back")
-       warningRows = append(warningRows, " ▌ to v0.31.1 until the issue is 
resolved.")
+       warningRows = append(warningRows, " ▌ k0sctl's host connection and 
remote execution functionality has been")
+       warningRows = append(warningRows, " ▌ updated. Behavior should be 
unchanged, but if you encounter any")
+       warningRows = append(warningRows, " ▌ issues, please report them at:")
+       warningRows = append(warningRows, " ▌ hem at 
https://github.com/k0sproject/k0sctl/issues";)
+       warningRows = append(warningRows, " ▌ and roll back to v0.31.1 until 
the issue is resolved.")
        warningRows = append(warningRows, "")
        for _, row := range warningRows {
                fmt.Fprintln(ctx.App.ErrWriter, row)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.32.0/go.mod new/k0sctl-0.32.1/go.mod
--- old/k0sctl-0.32.0/go.mod    2026-07-03 08:56:55.000000000 +0200
+++ new/k0sctl-0.32.1/go.mod    2026-07-06 13:40:55.000000000 +0200
@@ -33,7 +33,7 @@
 require (
        github.com/carlmjohnson/versioninfo v0.22.5
        github.com/jellydator/validation v1.2.0
-       github.com/k0sproject/rig/v2 v2.0.0
+       github.com/k0sproject/rig/v2 v2.1.0
        github.com/k0sproject/version v0.8.0
        github.com/samber/slog-logrus/v2 v2.5.4
        github.com/sergi/go-diff v1.4.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.32.0/go.sum new/k0sctl-0.32.1/go.sum
--- old/k0sctl-0.32.0/go.sum    2026-07-03 08:56:55.000000000 +0200
+++ new/k0sctl-0.32.1/go.sum    2026-07-06 13:40:55.000000000 +0200
@@ -108,8 +108,8 @@
 github.com/json-iterator/go v1.1.12/go.mod 
h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/k0sproject/dig v0.4.0 
h1:yBxFUUxNXAMGBg6b7c6ypxdx/o3RmhoI5v5ABOw5tn0=
 github.com/k0sproject/dig v0.4.0/go.mod 
h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4=
-github.com/k0sproject/rig/v2 v2.0.0 
h1:PcRzPiugIReXk6kpFsQ9yCsC/OvQWwIh2U/UB0TT8Us=
-github.com/k0sproject/rig/v2 v2.0.0/go.mod 
h1:4bp1yGRyoANCEe9aFAAJzttFvTRWMPnbleYBQUaY38c=
+github.com/k0sproject/rig/v2 v2.1.0 
h1:Xt7FtMlyyknnpAVN8HKP4JZmZsDWZz39pw+FrwEcpl4=
+github.com/k0sproject/rig/v2 v2.1.0/go.mod 
h1:4bp1yGRyoANCEe9aFAAJzttFvTRWMPnbleYBQUaY38c=
 github.com/k0sproject/version v0.8.0 
h1:Yh1SFDeBqQ7etrGwffY8bWKdbAUjeBOhiZ6oQuuz4sM=
 github.com/k0sproject/version v0.8.0/go.mod 
h1:iNV3O8blndsQhxZ8zACfpQhrLDlrTvDlCzx+vgCFtSI=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 
h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=

++++++ k0sctl.obsinfo ++++++
--- /var/tmp/diff_new_pack.CchIsA/_old  2026-07-07 21:03:31.770942467 +0200
+++ /var/tmp/diff_new_pack.CchIsA/_new  2026-07-07 21:03:31.778942742 +0200
@@ -1,5 +1,5 @@
 name: k0sctl
-version: 0.32.0
-mtime: 1783061815
-commit: 64fda2ad15fe647fb4c47e1f6ecb1c39b5ea050c
+version: 0.32.1
+mtime: 1783338055
+commit: db3dfb711f8b95e6f3bf4297db60dd5fbd337f85
 

++++++ vendor.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/client.go 
new/vendor/github.com/k0sproject/rig/v2/client.go
--- old/vendor/github.com/k0sproject/rig/v2/client.go   2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/client.go   2026-07-06 
13:40:55.000000000 +0200
@@ -228,6 +228,7 @@
 
                c.Runner = c.options.GetRunner(c.connection)
                log.InjectLogger(logger, c.Runner)
+               c.injectCommandGate(c.Runner)
 
                c.SudoProvider = c.options.GetSudoProvider(c.Runner)
                c.InitSystemProvider = c.options.GetInitSystemProvider(c.Runner)
@@ -238,6 +239,24 @@
        return c.initErr
 }
 
+// injectCommandGate installs the configured [cmd.CommandGate] onto the given
+// runner when the runner supports it. This is how a gate set once on the 
client
+// reaches every derived runner: the base runner here, and sudo runners via the
+// re-run of setup during [Client.Clone]. A nil configured gate is applied too,
+// clearing any gate a reused runner may already carry so that
+// WithCommandGate(nil) reliably disables gating.
+func (c *Client) injectCommandGate(runner cmd.Runner) {
+       setter, ok := runner.(cmd.CommandGateSetter)
+       if !ok {
+               if c.options.commandGate != nil {
+                       c.Log().Warn("command gate configured but the runner 
does not support it; commands will run ungated",
+                               "runner", fmt.Sprintf("%T", runner))
+               }
+               return
+       }
+       setter.SetCommandGate(c.options.commandGate)
+}
+
 // Service returns a manager for a named service on the remote host using
 // the host's init system if one can be detected. This can be used to
 // start, stop, restart, and check the status of services.
@@ -371,14 +390,38 @@
 
 // ExecInteractive executes a command on the host and passes 
stdin/stdout/stderr as-is to the session.
 // The session is terminated when ctx is cancelled or its deadline is exceeded.
-func (c *Client) ExecInteractive(ctx context.Context, cmd string, stdin 
io.Reader, stdout, stderr io.Writer) error {
-       if conn, ok := c.connection.(protocol.InteractiveExecer); ok {
-               if err := conn.ExecInteractive(ctx, cmd, stdin, stdout, 
stderr); err != nil {
-                       return fmt.Errorf("exec interactive: %w", err)
+//
+// A configured [cmd.CommandGate] is consulted for the command before the
+// session starts. Because interactive exec runs directly on the connection
+// rather than through the runner, the gate sees the raw command as given here,
+// without sudo/shell decoration or secret redaction, and commands typed inside
+// the interactive session are not gated.
+func (c *Client) ExecInteractive(ctx context.Context, command string, stdin 
io.Reader, stdout, stderr io.Writer) error {
+       conn, ok := c.connection.(protocol.InteractiveExecer)
+       if !ok {
+               return errInteractiveNotSupported
+       }
+       // Short-circuit a cancelled/expired context before consulting the 
gate, so
+       // a gate implementation is never asked to prompt for a doomed session. 
This
+       // mirrors Executor.Start.
+       if err := ctx.Err(); err != nil {
+               return fmt.Errorf("exec interactive: %w", err)
+       }
+       if gate := c.options.commandGate; gate != nil {
+               if err := gate.AllowCommand(ctx, c.String(), command); err != 
nil {
+                       // A context cancellation/deadline is not a rejection: 
wrap it
+                       // without cmd.ErrCommandRejected so errors.Is reports 
the context
+                       // error but not a rejection, letting callers 
distinguish the two.
+                       if errors.Is(err, context.Canceled) || errors.Is(err, 
context.DeadlineExceeded) {
+                               return fmt.Errorf("exec interactive: %w", err)
+                       }
+                       return fmt.Errorf("exec interactive: %w: %w", 
cmd.ErrCommandRejected, err)
                }
-               return nil
        }
-       return errInteractiveNotSupported
+       if err := conn.ExecInteractive(ctx, command, stdin, stdout, stderr); 
err != nil {
+               return fmt.Errorf("exec interactive: %w", err)
+       }
+       return nil
 }
 
 // The provider Getters would be available and working via the embedding 
already, but the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/clientoptions.go 
new/vendor/github.com/k0sproject/rig/v2/clientoptions.go
--- old/vendor/github.com/k0sproject/rig/v2/clientoptions.go    2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/clientoptions.go    2026-07-06 
13:40:55.000000000 +0200
@@ -1,6 +1,7 @@
 package rig
 
 import (
+       "context"
        "errors"
        "fmt"
 
@@ -14,7 +15,10 @@
        "github.com/k0sproject/rig/v2/sudo"
 )
 
-var errNilOSRelease = errors.New("os release provider returned nil release 
without error")
+var (
+       errNilOSRelease    = errors.New("os release provider returned nil 
release without error")
+       errConfirmDeclined = errors.New("declined by confirmation callback")
+)
 
 // ConnectionFactory can create connections. When a connection is not given, 
the factory is used
 // to build a connection.
@@ -33,6 +37,7 @@
        connection        protocol.Connection
        connectionFactory ConnectionFactory
        runner            cmd.Runner
+       commandGate       cmd.CommandGate
        retryConnection   bool
        providersContainer
 }
@@ -135,6 +140,7 @@
                connection:         o.connection,
                connectionFactory:  o.connectionFactory,
                runner:             o.runner,
+               commandGate:        o.commandGate,
                retryConnection:    o.retryConnection,
                providersContainer: o.providersContainer,
        }
@@ -201,6 +207,53 @@
        }
 }
 
+// WithCommandGate installs a [cmd.CommandGate] that is consulted before
+// commands run on the client and all of its derived runners (sudo, filesystem,
+// services). Returning a non-nil error from the gate aborts the command,
+// wrapped in [cmd.ErrCommandRejected]; a context cancellation/deadline error 
is
+// the exception and propagates as a context error without 
[cmd.ErrCommandRejected].
+// Use [WithConfirmFunc] for the common case of a yes/no confirmation prompt.
+// See [cmd.CommandGate] for exactly which commands are gated and which 
internal
+// paths are not.
+//
+// The gate reaches a runner only if that runner implements
+// [cmd.CommandGateSetter]. The default runner does; a custom runner supplied
+// via [WithRunner] that does not implement it runs ungated, and the client
+// logs a warning during setup.
+func WithCommandGate(gate cmd.CommandGate) ClientOption {
+       return func(o *ClientOptions) {
+               o.commandGate = gate
+       }
+}
+
+// WithConfirmFunc installs a confirmation callback consulted before commands
+// run on the client and all of its derived runners (sudo, filesystem,
+// services). The callback receives the host string and the human-readable,
+// redacted command; returning false aborts the command with
+// [cmd.ErrCommandRejected]. This is a convenience wrapper around
+// [WithCommandGate]; see [cmd.CommandGate] for which commands are gated. A nil
+// fn disables gating, matching WithCommandGate(nil).
+//
+// One exception: [Client.ExecInteractive] runs directly on the connection, so
+// its command reaches the callback raw — undecorated and unredacted. Avoid
+// logging the callback argument as-is if interactive commands may carry
+// secrets.
+func WithConfirmFunc(fn func(host, command string) bool) ClientOption {
+       if fn == nil {
+               return WithCommandGate(nil)
+       }
+       gate := cmd.CommandGateFunc(func(ctx context.Context, host, command 
string) error {
+               if err := ctx.Err(); err != nil {
+                       return err //nolint:wrapcheck // pass the context error 
through unchanged so it isn't misread as a rejection
+               }
+               if fn(host, command) {
+                       return nil
+               }
+               return errConfirmDeclined
+       })
+       return WithCommandGate(gate)
+}
+
 // WithConnectionFactory is a functional option that sets the connection 
factory to use for connecting.
 func WithConnectionFactory(factory ConnectionFactory) ClientOption {
        return func(o *ClientOptions) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go 
new/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go
--- old/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go      2026-07-06 
13:40:55.000000000 +0200
@@ -0,0 +1,55 @@
+package cmd
+
+import "context"
+
+// CommandGate is consulted before a command runs, on the [Executor] execution
+// path that every Exec/Start call (and thus every remotefs and service call)
+// funnels through. Returning a non-nil error aborts the command before it
+// starts; the error is propagated to the caller wrapped in 
[ErrCommandRejected].
+//
+// A context cancellation or deadline error is the one exception: it is
+// propagated without [ErrCommandRejected], so errors.Is reports the context
+// error but not a rejection. Callers that treat rejections specially can thus
+// distinguish an explicit refusal from a cancelled/expired context.
+//
+// A gate fires once per command at the outermost runner, so a command that is
+// wrapped by sudo (or any other decorator chain) is presented in its final,
+// fully wrapped form exactly once. On that path the command argument is the
+// human-readable form — decorated, sudo-wrapped, PowerShell-decoded, and with
+// any registered secrets redacted — suitable for a confirmation prompt or
+// audit log.
+//
+// The following do not go through this path and are therefore not gated:
+//   - [Executor.StartProcess], the low-level primitive used for runner 
chaining.
+//   - The built-in Windows-detection probe (ver.exe), which runs via
+//     StartProcess at runner construction to decide command formatting.
+//   - Privilege-escalation probes (sudo/doas availability, UID-0 and
+//     Windows-admin checks), which run [Ungated] so a rejecting gate surfaces
+//     an error rather than silently disabling sudo.
+//
+// [github.com/k0sproject/rig/v2.Client.ExecInteractive] consults the gate
+// separately, with the raw (undecorated, unredacted) command.
+//
+// AllowCommand may block (for example to prompt on stdin); it should honor the
+// supplied context and return its error when the caller cancels.
+type CommandGate interface {
+       // AllowCommand reports whether the command may run on the named host.
+       // A nil return allows the command; a non-nil error aborts it.
+       AllowCommand(ctx context.Context, host, command string) error
+}
+
+// CommandGateFunc adapts an ordinary function to the [CommandGate] interface.
+type CommandGateFunc func(ctx context.Context, host, command string) error
+
+// AllowCommand implements [CommandGate].
+func (f CommandGateFunc) AllowCommand(ctx context.Context, host, command 
string) error {
+       return f(ctx, host, command)
+}
+
+// CommandGateSetter is implemented by runners that support installing a
+// [CommandGate]. [Executor] implements it, allowing a gate configured on a
+// [github.com/k0sproject/rig/v2.Client] to be propagated to every runner the
+// client derives (sudo, filesystem, services).
+type CommandGateSetter interface {
+       SetCommandGate(gate CommandGate)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go 
new/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go
--- old/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go  2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go  2026-07-06 
13:40:55.000000000 +0200
@@ -51,6 +51,8 @@
        traceOut      io.Writer
        traceErr      io.Writer
        outputClosers []io.Closer
+
+       skipGate bool
 }
 
 // Format returns the command string with all per-call decorators applied.
@@ -71,6 +73,11 @@
        return o.logCommand
 }
 
+// SkipGate returns true if the [CommandGate] should not be consulted for this 
command.
+func (o *ExecOptions) SkipGate() bool {
+       return o.skipGate
+}
+
 var (
        errCharDevice    = errors.New("reader is a character device")
        errUnknownReader = errors.New("unknown type of reader")
@@ -337,6 +344,15 @@
        }
 }
 
+// Ungated exec option for exempting a single command from the runner's
+// [CommandGate]. Use it for internal probes and detection commands that must
+// run unconditionally, so a rejecting gate cannot silently change behavior.
+func Ungated() ExecOption {
+       return func(o *ExecOptions) {
+               o.skipGate = true
+       }
+}
+
 // Trace exec option for attaching a [Tracer] to a single command execution.
 // If the Tracer also implements [OutputTracer], per-line stdout and stderr
 // hooks are enabled automatically.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/executor.go 
new/vendor/github.com/k0sproject/rig/v2/cmd/executor.go
--- old/vendor/github.com/k0sproject/rig/v2/cmd/executor.go     2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/cmd/executor.go     2026-07-06 
13:40:55.000000000 +0200
@@ -50,6 +50,7 @@
        isWin      func() bool
        osKnown    atomic.Bool
        tracer     Tracer
+       gate       CommandGate
 }
 
 func isWinFunc(conn protocol.ProcessStarter) func() bool {
@@ -82,6 +83,16 @@
        r.tracer = t
 }
 
+// SetCommandGate installs a [CommandGate] that is consulted by the Start and
+// Exec family of methods before a command runs. A nil gate disables gating.
+// The gate is not consulted by [Executor.StartProcess], which exists only to
+// satisfy the connection interface for runner chaining. SetCommandGate must
+// not be called concurrently with Start, Exec, or any other command-execution
+// method.
+func (r *Executor) SetCommandGate(gate CommandGate) {
+       r.gate = gate
+}
+
 func (r *Executor) formatCommandForOS(command string, execOpts *ExecOptions, 
isWindows bool) string {
        cmd := r.Format(execOpts.Format(command))
        if isWindows && !isExe(cmd) {
@@ -285,9 +296,38 @@
        return strings.Join(parts, " ")
 }
 
+// closeAll closes every closer, ignoring individual close errors.
+func closeAll(closers []io.Closer) {
+       for _, c := range closers {
+               _ = c.Close()
+       }
+}
+
+// gateCommand consults the runner's [CommandGate] for the given command, which
+// must already be in redacted, human-readable form. It returns nil when there
+// is no gate, when the command opts out via [Ungated], or when the gate allows
+// the command. A non-nil return means the command must not be started: a
+// rejection wraps [ErrCommandRejected], while a context cancellation/deadline
+// is wrapped without [ErrCommandRejected] so that errors.Is reports the 
context
+// error but not a rejection, letting callers tell the two apart.
+func (r *Executor) gateCommand(ctx context.Context, execOpts *ExecOptions, 
redacted string) error {
+       if r.gate == nil || execOpts.SkipGate() {
+               return nil
+       }
+       err := r.gate.AllowCommand(ctx, r.String(), redacted)
+       if err == nil {
+               return nil
+       }
+       if errors.Is(err, context.Canceled) || errors.Is(err, 
context.DeadlineExceeded) {
+               log.Trace(ctx, "command gate cancelled", log.HostAttr(r), 
log.KeyCommand, redacted, log.KeyError, err)
+               return fmt.Errorf("command gate: %w", err)
+       }
+       log.Trace(ctx, "command rejected by gate", log.HostAttr(r), 
log.KeyCommand, redacted, log.KeyError, err)
+       return fmt.Errorf("%w: %w", ErrCommandRejected, err)
+}
+
 // Start starts the command and returns a Waiter.
 func (r *Executor) Start(ctx context.Context, command string, opts 
...ExecOption) (protocol.Waiter, error) {
-       log.Trace(ctx, "starting command", log.HostAttr(r), log.KeyCommand, 
command)
        if ctx.Err() != nil {
                return nil, fmt.Errorf("runner context error: %w", ctx.Err())
        }
@@ -320,8 +360,24 @@
        // non-prefixed commands through cmd.exe.
        cmd := r.formatCommand(command, execOpts)
 
+       // redactedCmd is the human-readable command (secrets masked, PowerShell
+       // -EncodedCommand decoded), computed once and reused for gating and 
every
+       // log line below. Tracer callbacks deliberately receive the raw 
formatted
+       // cmd, as their contract documents.
+       redactedCmd := execOpts.Redact(decodeEncoded(cmd))
+
+       // Consult the gate before emitting any command-logging or tracer
+       // lifecycle events so a rejected command produces no dangling
+       // "executing"/CommandFormatted/ProcessStarted events.
+       if err := r.gateCommand(ctx, execOpts, redactedCmd); err != nil {
+               closeAll(traceClosers)
+               return nil, err
+       }
+
+       log.Trace(ctx, "starting command", log.HostAttr(r), log.KeyCommand, 
redactedCmd)
+
        if execOpts.LogCommand() {
-               r.Log().Debug("executing command", log.KeyCommand, 
execOpts.Redact(decodeEncoded(cmd)))
+               r.Log().Debug("executing command", log.KeyCommand, redactedCmd)
        }
 
        if tracer != nil {
@@ -334,18 +390,14 @@
 
        waiter, err := r.connection.StartProcess(ctx, cmd, execOpts.Stdin(), 
stdout, stderr) //nolint:contextcheck // Stdin() uses trace logger which takes 
context
        if err != nil {
-               for _, c := range traceClosers {
-                       _ = c.Close()
-               }
-               log.Trace(ctx, "start process failed", log.HostAttr(r), 
log.KeyCommand, cmd, log.KeyError, err)
+               closeAll(traceClosers)
+               log.Trace(ctx, "start process failed", log.HostAttr(r), 
log.KeyCommand, redactedCmd, log.KeyError, err)
                return nil, fmt.Errorf("runner start command: %w", err)
        }
 
        if waiter == nil {
-               for _, c := range traceClosers {
-                       _ = c.Close()
-               }
-               log.Trace(ctx, "start process returned nil waiter", 
log.HostAttr(r), log.KeyCommand, cmd, log.KeyError, err)
+               closeAll(traceClosers)
+               log.Trace(ctx, "start process returned nil waiter", 
log.HostAttr(r), log.KeyCommand, redactedCmd, log.KeyError, errInternal)
                return nil, fmt.Errorf("%w: connection returned no error but a 
nil waiter", errInternal)
        }
 
@@ -405,20 +457,22 @@
 
        opts = append(opts, Stdout(out))
 
-       log.Trace(ctx, "starting command for execoutput", log.HostAttr(r), 
log.KeyCommand, command)
+       // Start gates the command and logs it once in its canonical redacted,
+       // decoded, fully decorated form. These traces only mark the execoutput
+       // lifecycle, so they deliberately omit the command to avoid re-deriving
+       // (and drifting from) that canonical form.
        proc, err := r.Start(ctx, command, opts...)
        if err != nil {
                return "", fmt.Errorf("start command: %w", err)
        }
-       log.Trace(ctx, "waiting on command", log.HostAttr(r), log.KeyCommand, 
command)
+       log.Trace(ctx, "waiting on command", log.HostAttr(r))
        if err := proc.Wait(); err != nil {
-               log.Trace(ctx, "waiting returned an error", log.HostAttr(r), 
log.KeyCommand, command, log.KeyError, err)
+               log.Trace(ctx, "waiting returned an error", log.HostAttr(r), 
log.KeyError, err)
                return "", fmt.Errorf("command result: %w", err)
        }
 
-       log.Trace(ctx, "command finished", log.HostAttr(r), log.KeyCommand, 
command)
-       execOpts := Build(opts...)
-       return execOpts.FormatOutput(out.String()), nil
+       log.Trace(ctx, "command finished", log.HostAttr(r))
+       return Build(opts...).FormatOutput(out.String()), nil
 }
 
 // ExecOutput executes the command and returns the stdout output or an error.
@@ -435,14 +489,17 @@
                return pipeR
        }
        opts = append(opts, Stdout(pipeW))
+       // ExecContext -> Start gates and logs the command once in its canonical
+       // redacted, decoded form. These traces only mark the execreader 
lifecycle,
+       // so they deliberately omit the command to avoid re-deriving it here.
        go func() {
                if err := r.ExecContext(ctx, command, opts...); err != nil {
-                       log.Trace(ctx, "execreader: execcontext returned an 
error", log.HostAttr(r), log.KeyCommand, command, log.KeyError, err)
+                       log.Trace(ctx, "execreader: execcontext returned an 
error", log.HostAttr(r), log.KeyError, err)
                        pipeW.CloseWithError(fmt.Errorf("exec reader: %w", err))
                } else {
                        pipeW.Close()
                }
-               log.Trace(ctx, "execreader: execcontext finished", 
log.HostAttr(r), log.KeyCommand, command)
+               log.Trace(ctx, "execreader: execcontext finished", 
log.HostAttr(r))
        }()
        return pipeR
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/runner.go 
new/vendor/github.com/k0sproject/rig/v2/cmd/runner.go
--- old/vendor/github.com/k0sproject/rig/v2/cmd/runner.go       2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/cmd/runner.go       2026-07-06 
13:40:55.000000000 +0200
@@ -17,6 +17,9 @@
 
        // ErrWroteStderr is returned when a windows command writes to stderr, 
unless AllowWinStderr is set.
        ErrWroteStderr = errors.New("command wrote output to stderr")
+
+       // ErrCommandRejected is returned when a [CommandGate] refuses to allow 
a command to run.
+       ErrCommandRejected = errors.New("command rejected")
 )
 
 // DecorateFunc is a function that takes a string and returns a decorated 
string.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go 
new/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go
--- old/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go    
2026-07-03 08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go    
2026-07-06 13:40:55.000000000 +0200
@@ -107,15 +107,16 @@
 }
 
 // Cmd builds a command-line for executing a PowerShell command or script.
-// Scripts that contain newlines, double-quotes, or cmd.exe metacharacters
-// are passed via -EncodedCommand to avoid shell expansion; simple one-liners
-// are passed via -Command so they remain readable in logs.
-// cmd.exe metacharacters guarded: " % ! ^ & | < >.
+// The command is always passed via -EncodedCommand (base64 UTF-16LE): the
+// payload is opaque to both cmd.exe and PowerShell, so it survives intact
+// regardless of the outer login shell. This matters when the remote host runs
+// OpenSSH with DefaultShell=PowerShell, where a plain -Command "..." argument
+// would be parsed and its $-tokens expanded by the outer shell before the
+// inner powershell.exe ever sees them. The trade-off is unreadable command
+// lines, but the runner's debug log decodes -EncodedCommand back to the
+// original script.
 func Cmd(psCmd string) string {
-       if strings.ContainsAny(psCmd, "\n\r\"%!^&|<>") {
-               return "powershell.exe -NonInteractive -ExecutionPolicy 
Unrestricted -NoP -E " + EncodeCmd(psCmd)
-       }
-       return "powershell.exe -NonInteractive -ExecutionPolicy Unrestricted 
-NoP -Command \"" + withProgressPreference(psCmd) + "\""
+       return "powershell.exe -NonInteractive -ExecutionPolicy Unrestricted 
-NoP -E " + EncodeCmd(psCmd)
 }
 
 // SingleQuote quotes and escapes a string in a format that is accepted by 
powershell scriptlets
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/doas.go 
new/vendor/github.com/k0sproject/rig/v2/sudo/doas.go
--- old/vendor/github.com/k0sproject/rig/v2/sudo/doas.go        2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/sudo/doas.go        2026-07-06 
13:40:55.000000000 +0200
@@ -16,7 +16,9 @@
                if c.IsWindows() {
                        return nil, false
                }
-               if c.Exec(Doas("true")) != nil {
+               // Ungated: a CommandGate that rejects this probe would silently
+               // disable doas rather than surface an error, so it must always 
run.
+               if c.Exec(Doas("true"), cmd.Ungated()) != nil {
                        return nil, false
                }
                return cmd.NewExecutor(c, Doas), true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/noop.go 
new/vendor/github.com/k0sproject/rig/v2/sudo/noop.go
--- old/vendor/github.com/k0sproject/rig/v2/sudo/noop.go        2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/sudo/noop.go        2026-07-06 
13:40:55.000000000 +0200
@@ -15,7 +15,9 @@
                if c.IsWindows() {
                        return nil, false
                }
-               if c.Exec(`[ "$(id -u)" = 0 ]`) != nil {
+               // Ungated: a CommandGate that rejects this probe would 
silently change
+               // sudo detection, so it must always run.
+               if c.Exec(`[ "$(id -u)" = 0 ]`, cmd.Ungated()) != nil {
                        return nil, false
                }
                return cmd.NewExecutor(c, Noop), true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go 
new/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go
--- old/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go        2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go        2026-07-06 
13:40:55.000000000 +0200
@@ -16,7 +16,9 @@
                if c.IsWindows() {
                        return nil, false
                }
-               if c.Exec(Sudo("true")) != nil {
+               // Ungated: a CommandGate that rejects this probe would silently
+               // disable sudo rather than surface an error, so it must always 
run.
+               if c.Exec(Sudo("true"), cmd.Ungated()) != nil {
                        return nil, false
                }
                return cmd.NewExecutor(c, Sudo), true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/windows.go 
new/vendor/github.com/k0sproject/rig/v2/sudo/windows.go
--- old/vendor/github.com/k0sproject/rig/v2/sudo/windows.go     2026-07-03 
08:56:55.000000000 +0200
+++ new/vendor/github.com/k0sproject/rig/v2/sudo/windows.go     2026-07-06 
13:40:55.000000000 +0200
@@ -16,7 +16,9 @@
                        return nil, false
                }
                const isAdminCmd = `if (-not (New-Object 
Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
 { exit 1 }`
-               if runner.Exec(isAdminCmd, cmd.PS()) != nil {
+               // Ungated: a CommandGate that rejects this probe would 
silently change
+               // privilege detection, so it must always run.
+               if runner.Exec(isAdminCmd, cmd.PS(), cmd.Ungated()) != nil {
                        return nil, false
                }
                return cmd.NewExecutor(runner, Noop), true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt
--- old/vendor/modules.txt      2026-07-03 08:56:55.000000000 +0200
+++ new/vendor/modules.txt      2026-07-06 13:40:55.000000000 +0200
@@ -143,7 +143,7 @@
 # github.com/k0sproject/dig v0.4.0
 ## explicit; go 1.21
 github.com/k0sproject/dig
-# github.com/k0sproject/rig/v2 v2.0.0
+# github.com/k0sproject/rig/v2 v2.1.0
 ## explicit; go 1.26.0
 github.com/k0sproject/rig/v2
 github.com/k0sproject/rig/v2/byteslice

Reply via email to