Skip to content
Draft
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
1 change: 1 addition & 0 deletions agent/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.53.0
golang.org/x/sys v0.46.0
golang.org/x/term v0.44.0
)

require (
Expand Down
60 changes: 60 additions & 0 deletions agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,66 @@ func main() {
},
})

shareCmd := &cobra.Command{ // nolint: exhaustruct
Use: "share [-- command...]",
Short: "Share a local terminal over a public link",
Long: `Spawn a command (or your login shell) inside a PTY and expose it as a public shared
terminal — like tmate or upterm. Open the link in a browser to watch the session live, without
signing in. The share is read-only by default; use --write to let guests type. The share always
ends when the command exits; --duration sets an additional time limit (--duration 0 disables it).
You keep using the terminal normally.`,
Args: cobra.ArbitraryArgs,
Run: func(cmd *cobra.Command, args []string) {
loglevel.SetLogLevel()

cfg, _, err := LoadConfigFromEnv()
if err != nil {
log.WithError(err).Fatal("Failed to load the configuration from the environmental variables")
}

name, _ := cmd.Flags().GetString("name")
writable, _ := cmd.Flags().GetBool("write")

// User precedence: explicit --user flag, else the SHELLHUB_SHARE_USER env (set by the
// host wrapper to the invoking user), else the flag default.
user, _ := cmd.Flags().GetString("user")
if !cmd.Flags().Changed("user") {
if envUser := os.Getenv("SHELLHUB_SHARE_USER"); envUser != "" {
user = envUser
}
}

// Resolve the lifetime: flag not set -> server default (0); set to 0 -> no expiry (-1);
// set to a positive duration -> that many seconds.
ttlSeconds := 0
if cmd.Flags().Changed("duration") {
duration, _ := cmd.Flags().GetDuration("duration")
if duration <= 0 {
ttlSeconds = -1
} else {
ttlSeconds = int(duration.Seconds())
}
}

opts := ShareOptions{
Command: args,
Name: name,
Writable: writable,
TTLSeconds: ttlSeconds,
User: user,
}

if err := NewShareSession(cfg, opts).Run(cmd.Context()); err != nil {
log.WithError(err).Fatal("Failed to share the terminal")
}
},
}
shareCmd.Flags().String("name", "", "Optional label for the share, shown in the namespace's list")
shareCmd.Flags().Bool("write", false, "Allow guests to type into the session (collaborative mode)")
shareCmd.Flags().Duration("duration", 0, "Time limit for the share (e.g. 30m, 2h); 0 means no time limit")
shareCmd.Flags().String("user", "root", "Host user to run the command as")
rootCmd.AddCommand(shareCmd)

registerInstallerCommands(rootCmd)

rootCmd.AddCommand(&cobra.Command{ // nolint: exhaustruct
Expand Down
59 changes: 59 additions & 0 deletions agent/scripts/shellhub-agent
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/sh
#
# ShellHub agent host wrapper.
#
# Install this at /usr/local/bin/shellhub-agent on the host where the agent
# container runs, so host users can invoke the agent CLI directly:
#
# shellhub-agent share -- bash
#
# The agent itself runs inside a container, so a plain `share` would spawn the
# command inside that container. This wrapper forwards the invocation into the
# agent container with `docker exec`, and passes the invoking host user via
# SHELLHUB_SHARE_USER so the command runs on the host as that user (the agent
# resolves it through the host's /etc/passwd, the same way SSH sessions do).
#
# Everything is auto-detected — no configuration needed in either development or
# production. The env vars below are optional overrides:
#
# SHELLHUB_AGENT_CONTAINER force a specific container/name
# SHELLHUB_AGENT_BIN force the agent binary path inside the container
#
set -eu

container="${SHELLHUB_AGENT_CONTAINER:-}"
if [ -z "$container" ]; then
# Prefer a Compose-managed `agent` service (development stack and most deployments),
# then a plainly-named container, then anything from the official agent image.
for filter in \
'label=com.docker.compose.service=agent' \
'name=shellhub-agent' \
'ancestor=shellhubio/agent'; do
container=$(docker ps --filter "$filter" --format '{{.Names}}' 2>/dev/null | head -n1)
[ -n "$container" ] && break
done
fi

if [ -z "$container" ]; then
echo "shellhub-agent: could not find a running agent container." >&2
echo "Set SHELLHUB_AGENT_CONTAINER to its name." >&2
exit 1
fi

bin="${SHELLHUB_AGENT_BIN:-}"
if [ -z "$bin" ]; then
# Development runs under air with the binary at /tmp/air/main; production has it on PATH.
if docker exec "$container" test -x /tmp/air/main 2>/dev/null; then
bin=/tmp/air/main
else
bin=shellhub-agent
fi
fi

# Allocate a TTY only when one is attached, so the wrapper also works in pipelines/scripts.
tty="-i"
[ -t 0 ] && tty="-it"

exec docker exec "$tty" \
-e "SHELLHUB_SHARE_USER=$(id -un)" \
"$container" "$bin" "$@"
Loading
Loading