From 7101fd53bcc1ea4e7a800bee0f3a496940aa5dc5 Mon Sep 17 00:00:00 2001 From: Alessandro Rinaldi Date: Sat, 5 Sep 2026 21:05:37 +0200 Subject: [PATCH] Rename our "host" vocabulary to "agent", the term DWService uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dwshell called the machine you connect to a "host". DWService does not: its own English strings shipped with the agent say "Agent name", "Creating a new agent", "The agent {0} already exists" — 39 occurrences of "agent" against one of "host", and that one is `proxyHost`, a network proxy. The protocol modules are `agent`, `share` and `group`. Speaking the service's language matters more now that agent management is coming: a command group named after the thing it manages reads wrong if the rest of the CLI calls that thing something else. Not a breaking change: "host" was never a flag or a config key, only variable names, help text and prose. Three things keep the word, because there it means a network host and renaming would be wrong or would falsify a record: - internal/client — the local machine's hostname, used to label a registered trusted device ("dwshell on "); - internal/session/socket.go — the relay host (u.Host); - docs/PROTOCOL.md — "Hosts and terminology" is about the service's own domains, and two further hits are captured terminal output (user@host:~#) and a file literally named "hosts"; - docs/DESIGN.md keeps "conhost", the Windows console host, and the README keeps "repositories are hosted at". parseUserHost becomes parseUserAndAgent rather than parseUserAgent, which would read as the HTTP header. Pure rename: 130 insertions against 130 deletions, no logic touched. Verified beyond the suite by exercising the real paths against live agents — list, -c with its exit code, and ls on a remote directory. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MvidAFW9a2r4hTgHPW9ywG --- README.md | 50 +++++++++++------------ cmd/dwshell/files.go | 74 ++++++++++++++++----------------- cmd/dwshell/files_test.go | 32 +++++++-------- cmd/dwshell/main.go | 86 +++++++++++++++++++-------------------- docs/DESIGN.md | 18 ++++---- 5 files changed, 130 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 4a94336..ddf25f8 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ $ dwshell myserver -c "uname -sr" Linux 6.8.0 ``` -After `login`, control commands (`list`, ``, `-c`) reuse the saved session +After `login`, control commands (`list`, ``, `-c`) reuse the saved session and **never prompt** for the account password. When the session expires they refresh silently via the trusted device, or, if there is none, tell you to run `dwshell login` again. Leave an interactive shell with `exit` (or Ctrl-D). @@ -162,21 +162,21 @@ the session without any of this. To supply a code non-interactively, see | `dwshell login [--user U] [--no-trusted]` | Authenticate and persist the session (and, by default, a trusted device). | | `dwshell logout` | Deregister the trusted device and forget local credentials. | | `dwshell list [--json]` | List machines with OS, online state, and owned/shared. | -| `dwshell ` | Open an interactive shell. | -| `dwshell -c "cmd"` | Run a command non-interactively; exit code is propagated. | -| `dwshell shell ` | Explicit form of the above. | -| `dwshell ls [:]` | List a remote directory (root if the path is omitted). | -| `dwshell get [-r] : [local]` | Download a file (or directory with `-r`). | -| `dwshell put [-r] :` | Upload a file (or directory with `-r`). | -| `dwshell rm [-r] : [...]` | Remove remote file(s) (directories with `-r`). | -| `dwshell sync [flags] ` | One-way sync (size+mtime or `--checksum`); one side is `host:path`. | +| `dwshell ` | Open an interactive shell. | +| `dwshell -c "cmd"` | Run a command non-interactively; exit code is propagated. | +| `dwshell shell ` | Explicit form of the above. | +| `dwshell ls [:]` | List a remote directory (root if the path is omitted). | +| `dwshell get [-r] : [local]` | Download a file (or directory with `-r`). | +| `dwshell put [-r] :` | Upload a file (or directory with `-r`). | +| `dwshell rm [-r] : [...]` | Remove remote file(s) (directories with `-r`). | +| `dwshell sync [flags] ` | One-way sync (size+mtime or `--checksum`); one side is `agent:path`. | | `dwshell version` | Print the version and exit. | | `dwshell help` | Show usage. | #### File transfer `ls`, `get`, `put`, and `rm` operate on the DWService filesystem app. Remote endpoints -are written `host:path` (the split is on the first colon, so a remote Windows +are written `agent:path` (the split is on the first colon, so a remote Windows path like `GHE:C:\Users` works): ```sh @@ -196,34 +196,34 @@ dwshell sync -n GHE:/data ./data # dry-run download-sync Single-file transfers, recursive `get -r` / `put -r` / `rm -r`, and one-way `sync` (transfers only files that differ by size or mtime). `sync` takes exactly -one `host:path` side; direction is inferred. It preserves mtimes (locally on +one `agent:path` side; direction is inferred. It preserves mtimes (locally on download; on upload it sets the remote mtime via the shell — falling back to size-only when that is unavailable, e.g. on Windows remotes). `--size-only` compares by size only; `--checksum` compares by SHA-256 (hashing the remote via the shell); `--delete` removes destination entries missing from the source; `-n` is a dry run. -`--own` / `--shared` disambiguate the host as elsewhere. +`--own` / `--shared` disambiguate the agent as elsewhere. Remote paths are always rooted at `/`, so `GHE:/etc` and a relative `GHE:etc` address the same directory, and an omitted path means the root. On Windows `/` is -the root and lists the drives; address a drive as `host:/C:/dir` or `host:C:/dir` -(`/` and `\` are interchangeable, and a bare `host:C:` means the drive root). +the root and lists the drives; address a drive as `agent:/C:/dir` or `agent:C:/dir` +(`/` and `\` are interchangeable, and a bare `agent:C:` means the drive root). -#### Host name vs subcommand +#### Agent name vs subcommand -`dwshell ` is a convenience shortcut: the first argument is treated as a -host **unless** it exactly matches one of the subcommands in the +`dwshell ` is a convenience shortcut: the first argument is treated as an +agent **unless** it exactly matches one of the subcommands in the [Commands](#commands) table above. So `dwshell version` prints the version, it does not connect to a machine. If you actually have a machine named like one of those, use the explicit `shell` -subcommand, which always treats its argument as a host: +subcommand, which always treats its argument as an agent: ```sh -dwshell shell version # connect to the host named "version" -dwshell shell list -c "id" # run a command on the host named "list" +dwshell shell version # connect to the agent named "version" +dwshell shell list -c "id" # run a command on the agent named "list" ``` -`` is a machine **name** or **id**, optionally prefixed `user@` (SSH-style; +`` is a machine **name** or **id**, optionally prefixed `user@` (SSH-style; defaults to your local username). If a name is ambiguous (a name shared between an owned agent and a share, or duplicate share names), pass the id or add `--own` / `--shared`. The `user@` part matters only when the agent requires authentication @@ -240,8 +240,8 @@ never taken from the command line). Access-restricted users (`shell.users_allowed`) are enforced by the agent. ```sh -dwshell alice@myhost # log in as alice; prompts for her password if required -DWSHELL_REMOTE_PASSWORD=… dwshell alice@myhost -c "id" +dwshell alice@myserver # log in as alice; prompts for her password if required +DWSHELL_REMOTE_PASSWORD=… dwshell alice@myserver -c "id" ``` ### Flags @@ -255,7 +255,7 @@ DWSHELL_REMOTE_PASSWORD=… dwshell alice@myhost -c "id" remote truncates the line silently and runs what is left of it, and since the exit-code marker goes with it, `-c` waits for output that never arrives — so pass a bigger script with `dwshell put` and run it by path instead. -- `--own` / `--shared` — resolve `` among owned agents / incoming shares only. +- `--own` / `--shared` — resolve `` among owned agents / incoming shares only. - `--term ` — TERM to send to a *nix remote (default: your local `$TERM`). - `--no-term` — do not send a TERM to the remote. - `--timeout ` — command timeout for `-c` (default: none; e.g. `30s`, `5m`). @@ -320,7 +320,7 @@ native UI all reuse the same login/session/connect libraries under `internal/`. ## Status Reverse-engineered and verified end-to-end against the live service on Linux and -Windows remotes. Unit tests cover the crypto, request framing, and host +Windows remotes. Unit tests cover the crypto, request framing, and agent resolution; `go test ./...`. ## Legal & disclaimer diff --git a/cmd/dwshell/files.go b/cmd/dwshell/files.go index c6eba0c..e6b359a 100644 --- a/cmd/dwshell/files.go +++ b/cmd/dwshell/files.go @@ -16,20 +16,20 @@ import ( "github.com/porech/dwshell/internal/session" ) -// parseRemote splits a "host:path" endpoint. It splits on the first colon so -// remote Windows paths (host:C:\dir) work; the host must be non-empty. An empty +// parseRemote splits an "agent:path" endpoint. It splits on the first colon so +// remote Windows paths (agent:C:\dir) work; the agent must be non-empty. An empty // path is allowed and means the remote root (see canonicalRemotePath). -func parseRemote(s string) (host, rpath string, err error) { +func parseRemote(s string) (agent, rpath string, err error) { i := strings.IndexByte(s, ':') if i <= 0 { - return "", "", fmt.Errorf("expected host:path, got %q", s) + return "", "", fmt.Errorf("expected agent:path, got %q", s) } return s[:i], s[i+1:], nil } -// isRemoteEndpoint reports whether s looks like a "host:path" remote endpoint -// rather than a local path. A single-letter host followed by a separator is -// treated as a local Windows drive (C:\dir), not a remote host. +// isRemoteEndpoint reports whether s looks like an "agent:path" remote endpoint +// rather than a local path. A single-letter agent followed by a separator is +// treated as a local Windows drive (C:\dir), not a remote agent. func isRemoteEndpoint(s string) bool { i := strings.IndexByte(s, ':') if i <= 0 { @@ -103,18 +103,18 @@ func cmdLs(ctx context.Context, args []string) int { fs.BoolVar(&own, "own", false, "owned agents only") fs.BoolVar(&shared, "shared", false, "incoming shares only") - endpoint, rest := extractHost(args) + endpoint, rest := extractAgent(args) if endpoint == "" { - return fail("usage: dwshell ls [:]") + return fail("usage: dwshell ls [:]") } if err := fs.Parse(rest); err != nil { return 2 } - // The path is optional: "dwshell ls " (or ":") lists the root. - host, rpath := endpoint, "" + // The path is optional: "dwshell ls " (or ":") lists the root. + agent, rpath := endpoint, "" if strings.ContainsRune(endpoint, ':') { var err error - if host, rpath, err = parseRemote(endpoint); err != nil { + if agent, rpath, err = parseRemote(endpoint); err != nil { return fail("%v", err) } } @@ -127,7 +127,7 @@ func cmdLs(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - m, sess, err := c.ConnectApp(ctx, host, filter, "filesystem") + m, sess, err := c.ConnectApp(ctx, agent, filter, "filesystem") if err != nil { return fail("%v", err) } @@ -172,9 +172,9 @@ func cmdGet(ctx context.Context, args []string) int { return 2 } if len(pos) < 1 || len(pos) > 2 { - return fail("usage: dwshell get : [local]") + return fail("usage: dwshell get : [local]") } - host, rpath, err := parseRemote(pos[0]) + agent, rpath, err := parseRemote(pos[0]) if err != nil { return fail("%v", err) } @@ -191,7 +191,7 @@ func cmdGet(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - m, sess, err := c.ConnectApp(ctx, host, filter, "filesystem") + m, sess, err := c.ConnectApp(ctx, agent, filter, "filesystem") if err != nil { return fail("%v", err) } @@ -251,12 +251,12 @@ func cmdSync(ctx context.Context, args []string) int { return 2 } if len(pos) != 2 { - return fail("usage: dwshell sync (exactly one of src/dst is host:path)") + return fail("usage: dwshell sync (exactly one of src/dst is agent:path)") } src, dst := pos[0], pos[1] srcR, dstR := isRemoteEndpoint(src), isRemoteEndpoint(dst) if srcR == dstR { - return fail("exactly one of source and destination must be a host:path endpoint") + return fail("exactly one of source and destination must be an agent:path endpoint") } var dir files.Direction @@ -266,7 +266,7 @@ func cmdSync(ctx context.Context, args []string) int { } else { dir, endpoint, localRoot = files.Upload, dst, src } - host, rpath, err := parseRemote(endpoint) + agent, rpath, err := parseRemote(endpoint) if err != nil { return fail("%v", err) } @@ -279,7 +279,7 @@ func cmdSync(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - m, sess, err := c.ConnectApp(ctx, host, filter, "filesystem") + m, sess, err := c.ConnectApp(ctx, agent, filter, "filesystem") if err != nil { return fail("%v", err) } @@ -305,7 +305,7 @@ func cmdSync(ctx context.Context, args []string) int { } if dir == files.Upload && !sizeOnly && !checksum { if m.OS.IsUnix() { - cfg.SetRemoteMTimes = remoteMTimeSetter(sess, m, host) + cfg.SetRemoteMTimes = remoteMTimeSetter(sess, m, agent) } else { fmt.Fprintln(os.Stderr, "note: cannot set mtimes on a Windows remote; comparing by size only") } @@ -335,7 +335,7 @@ func cmdSync(ctx context.Context, args []string) int { // remoteMTimeSetter returns a function that sets mtimes on *nix remote files via // the shell (the filesystem app has no set-mtime command). Failures are warned // and swallowed (best-effort), so the sync still succeeds. -func remoteMTimeSetter(sess *session.Session, m *remote.Machine, host string) func(context.Context, map[string]time.Time) error { +func remoteMTimeSetter(sess *session.Session, m *remote.Machine, agent string) func(context.Context, map[string]time.Time) error { return func(ctx context.Context, times map[string]time.Time) error { var b strings.Builder flush := func() error { @@ -431,21 +431,21 @@ func cmdRm(ctx context.Context, args []string) int { return 2 } if len(pos) < 1 { - return fail("usage: dwshell rm : [:...]") + return fail("usage: dwshell rm : [:...]") } - // All targets must be on the same host. - var host string + // All targets must be on the same agent. + var agent string var rpaths []string for _, p := range pos { h, rp, err := parseRemote(p) if err != nil { return fail("%v", err) } - if host == "" { - host = h - } else if h != host { - return fail("all paths must be on the same host (%q vs %q)", host, h) + if agent == "" { + agent = h + } else if h != agent { + return fail("all paths must be on the same agent (%q vs %q)", agent, h) } rpaths = append(rpaths, rp) } @@ -458,7 +458,7 @@ func cmdRm(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - m, sess, err := c.ConnectApp(ctx, host, filter, "filesystem") + m, sess, err := c.ConnectApp(ctx, agent, filter, "filesystem") if err != nil { return fail("%v", err) } @@ -474,7 +474,7 @@ func cmdRm(ctx context.Context, args []string) int { rc = fail("%v", err) continue } - fmt.Fprintf(os.Stderr, "removed %s:%s (recursively)\n", host, rp) + fmt.Fprintf(os.Stderr, "removed %s:%s (recursively)\n", agent, rp) } return rc } @@ -502,7 +502,7 @@ func cmdRm(ctx context.Context, args []string) int { } for _, dir := range order { for _, n := range byDir[dir] { - fmt.Fprintf(os.Stderr, "removed %s:%s%s\n", host, dir, n) + fmt.Fprintf(os.Stderr, "removed %s:%s%s\n", agent, dir, n) } } return 0 @@ -524,10 +524,10 @@ func cmdPut(ctx context.Context, args []string) int { return 2 } if len(pos) != 2 { - return fail("usage: dwshell put :") + return fail("usage: dwshell put :") } local := pos[0] - host, rpath, err := parseRemote(pos[1]) + agent, rpath, err := parseRemote(pos[1]) if err != nil { return fail("%v", err) } @@ -540,7 +540,7 @@ func cmdPut(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - m, sess, err := c.ConnectApp(ctx, host, filter, "filesystem") + m, sess, err := c.ConnectApp(ctx, agent, filter, "filesystem") if err != nil { return fail("%v", err) } @@ -557,7 +557,7 @@ func cmdPut(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - fmt.Fprintf(os.Stderr, "uploaded %d files (%d bytes) → %s:%s\n", count, total, host, rpath) + fmt.Fprintf(os.Stderr, "uploaded %d files (%d bytes) → %s:%s\n", count, total, agent, rpath) return 0 } @@ -576,7 +576,7 @@ func cmdPut(ctx context.Context, args []string) int { if n >= 0 { sz = fmt.Sprintf("%d bytes", n) } - fmt.Fprintf(os.Stderr, "uploaded %s (%s) → %s:%s\n", local, sz, host, rpath) + fmt.Fprintf(os.Stderr, "uploaded %s (%s) → %s:%s\n", local, sz, agent, rpath) return 0 } diff --git a/cmd/dwshell/files_test.go b/cmd/dwshell/files_test.go index 580b5cc..8ea68fd 100644 --- a/cmd/dwshell/files_test.go +++ b/cmd/dwshell/files_test.go @@ -4,25 +4,25 @@ import "testing" func TestParseRemote(t *testing.T) { tests := []struct { - in string - host, path string - ok bool + in string + agent, path string + ok bool }{ {"GHE:/etc/hostname", "GHE", "/etc/hostname", true}, {"GHE:C:\\Users\\me", "GHE", "C:\\Users\\me", true}, // first colon splits; Windows remote path survives {"myhost:/", "myhost", "/", true}, {"/local/path", "", "", false}, // no colon - {":/nohost", "", "", false}, // empty host - {"host:", "host", "", true}, // empty path means the remote root + {":/nohost", "", "", false}, // empty agent + {"agent:", "agent", "", true}, // empty path means the remote root } for _, tc := range tests { - host, p, err := parseRemote(tc.in) + agent, p, err := parseRemote(tc.in) if tc.ok { - if err != nil || host != tc.host || p != tc.path { - t.Errorf("parseRemote(%q) = (%q,%q,%v), want (%q,%q,nil)", tc.in, host, p, err, tc.host, tc.path) + if err != nil || agent != tc.agent || p != tc.path { + t.Errorf("parseRemote(%q) = (%q,%q,%v), want (%q,%q,nil)", tc.in, agent, p, err, tc.agent, tc.path) } } else if err == nil { - t.Errorf("parseRemote(%q) expected error, got (%q,%q)", tc.in, host, p) + t.Errorf("parseRemote(%q) expected error, got (%q,%q)", tc.in, agent, p) } } } @@ -74,13 +74,13 @@ func TestCanonicalRemotePath(t *testing.T) { func TestIsRemoteEndpoint(t *testing.T) { cases := map[string]bool{ - "GHE:/etc": true, - "GHE:C:\\Users": true, - `C:\Users`: false, // local Windows drive - "C:/Users": false, - "/local/path": false, - "./rel": false, - "host:relative": true, + "GHE:/etc": true, + "GHE:C:\\Users": true, + `C:\Users`: false, // local Windows drive + "C:/Users": false, + "/local/path": false, + "./rel": false, + "agent:relative": true, } for in, want := range cases { if got := isRemoteEndpoint(in); got != want { diff --git a/cmd/dwshell/main.go b/cmd/dwshell/main.go index 5530aab..ed98e04 100644 --- a/cmd/dwshell/main.go +++ b/cmd/dwshell/main.go @@ -29,14 +29,14 @@ func newFlags(name string) *flag.FlagSet { return fs } -// valueFlags are host-command flags that consume a following value token. +// valueFlags are agent-command flags that consume a following value token. var valueFlags = map[string]bool{"c": true, "term": true, "config": true, "timeout": true} -// extractHost pulls the first positional (the host) out of args, returning it +// extractAgent pulls the first positional (the agent) out of args, returning it // plus the remaining flag arguments in order. It skips the value token that // follows a value flag so `dwshell GHE -c "ls"` and `dwshell -c "ls" GHE` both // work. -func extractHost(args []string) (host string, flagArgs []string) { +func extractAgent(args []string) (agent string, flagArgs []string) { i := 0 for i < len(args) { a := args[i] @@ -51,9 +51,9 @@ func extractHost(args []string) (host string, flagArgs []string) { i++ continue } - host = a + agent = a flagArgs = append(flagArgs, args[i+1:]...) - return host, flagArgs + return agent, flagArgs } return "", flagArgs } @@ -85,20 +85,20 @@ Usage: dwshell login [--user U] [--no-trusted] Authenticate and persist the session dwshell logout Forget stored credentials dwshell list [--json] List machines (agents + shares) - dwshell [flags] Open an interactive shell - dwshell -c "command" [flags] Run a command and exit - dwshell shell [flags] Explicit form (use if is + dwshell [flags] Open an interactive shell + dwshell -c "command" [flags] Run a command and exit + dwshell shell [flags] Explicit form (use if is named like a subcommand) - dwshell ls [:] List a remote directory (root if omitted) - dwshell get [-r] : [local] Download a file or directory - dwshell put [-r] : Upload a file or directory - dwshell rm [-r] : [...] Remove remote file(s)/dir(s) + dwshell ls [:] List a remote directory (root if omitted) + dwshell get [-r] : [local] Download a file or directory + dwshell put [-r] : Upload a file or directory + dwshell rm [-r] : [...] Remove remote file(s)/dir(s) dwshell sync [-n] [--delete] [--checksum] One-way sync -Host flags: +Agent flags: -c string Run command non-interactively, capture output, exit - --own Resolve among owned agents only - --shared Resolve among incoming shares only + --own Resolve among owned agents only + --shared Resolve among incoming shares only --term string TERM to send to a *nix remote (default: local $TERM) --no-term Do not send a TERM to the remote --timeout dur Command timeout for -c (default: none) @@ -113,11 +113,11 @@ Remote paths: "/C:/dir" or "C:/dir" ('/' and '\' are interchangeable). If the path is omitted, the root is used. -Host name vs subcommand: - "dwshell " is a shortcut: the first argument is treated as a host unless +Agent name vs subcommand: + "dwshell " is a shortcut: the first argument is treated as an agent unless it is one of the subcommands listed above. If a machine is actually named like - one of those, use the explicit form "dwshell shell ", which always treats - the argument as a host (e.g. "dwshell shell version" → host named "version"). + one of those, use the explicit form "dwshell shell ", which always treats + the argument as an agent (e.g. "dwshell shell version" → agent named "version"). ` func main() { @@ -156,13 +156,13 @@ func run() int { case "sync": return cmdSync(ctx, os.Args[2:]) case "shell": - // Explicit form: the next argument is always a host, even if it happens + // Explicit form: the next argument is always an agent, even if it happens // to be named like a subcommand (e.g. `dwshell shell version`). - return cmdHost(ctx, os.Args[2:]) + return cmdAgent(ctx, os.Args[2:]) default: - // Shortcut form: `dwshell [flags]`. If your host is named like a - // subcommand, use the explicit `dwshell shell ` form above. - return cmdHost(ctx, os.Args[1:]) + // Shortcut form: `dwshell [flags]`. If your agent is named like a + // subcommand, use the explicit `dwshell shell ` form above. + return cmdAgent(ctx, os.Args[1:]) } } @@ -289,14 +289,14 @@ func cmdList(ctx context.Context, args []string) int { return 0 } -// --- host (interactive / -c) --- +// --- agent (interactive / -c) --- -func cmdHost(ctx context.Context, args []string) int { - // args[0] is the host; flags may precede or follow it. +func cmdAgent(ctx context.Context, args []string) int { + // args[0] is the agent; flags may precede or follow it. var command, termValue, configPath string var own, shared, noTerm bool var timeout time.Duration - fs := newFlags("host") + fs := newFlags("agent") fs.StringVar(&command, "c", "", "run command and exit") fs.StringVar(&termValue, "term", "", "TERM for *nix remote") fs.StringVar(&configPath, "config", "", "config path") @@ -305,14 +305,14 @@ func cmdHost(ctx context.Context, args []string) int { fs.BoolVar(&noTerm, "no-term", false, "do not send TERM") fs.DurationVar(&timeout, "timeout", 0, "command timeout for -c (0 = no timeout)") - hostArg, rest := extractHost(args) - if hostArg == "" { - return fail("a host is required (see `dwshell --help`)") + agentArg, rest := extractAgent(args) + if agentArg == "" { + return fail("an agent is required (see `dwshell --help`)") } if err := fs.Parse(rest); err != nil { return 2 } - username, host, userExplicit := parseUserHost(hostArg) + username, agent, userExplicit := parseUserAndAgent(agentArg) filter := remote.Any switch { @@ -328,13 +328,13 @@ func cmdHost(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - m, sess, err := c.Connect(ctx, host, filter) + m, sess, err := c.Connect(ctx, agent, filter) if err != nil { return fail("%v", err) } if command != "" { - res, err := shell.Run(ctx, sess, m.OS, command, timeout, username, nonInteractivePassword(username, host)) + res, err := shell.Run(ctx, sess, m.OS, command, timeout, username, nonInteractivePassword(username, agent)) if err != nil { return fail("%v", err) } @@ -351,17 +351,17 @@ func cmdHost(ctx context.Context, args []string) int { return interactive(ctx, sess, m, username, userExplicit, termValue, noTerm) } -// parseUserHost splits "user@host" into (user, host, explicit); when no user is +// parseUserAndAgent splits "user@agent" into (user, agent, explicit); when no user is // given the local username is used, SSH-style, and explicit is false. -func parseUserHost(arg string) (user, host string, explicit bool) { +func parseUserAndAgent(arg string) (user, agent string, explicit bool) { if i := strings.LastIndexByte(arg, '@'); i >= 0 { return arg[:i], arg[i+1:], true } return localUser(), arg, false } -func warnUserIgnored(username, host string) { - fmt.Fprintf(os.Stderr, "dwshell: note: %s does not require authentication; the user %q is ignored (shell runs as the agent's user)\n", host, username) +func warnUserIgnored(username, agent string) { + fmt.Fprintf(os.Stderr, "dwshell: note: %s does not require authentication; the user %q is ignored (shell runs as the agent's user)\n", agent, username) } func localUser() string { @@ -375,15 +375,15 @@ func localUser() string { } // interactivePassword prompts on the TTY (agent asked for a password). -func interactivePassword(user, host string) shell.PasswordFunc { +func interactivePassword(user, agent string) shell.PasswordFunc { return func(u string, retry bool) (string, error) { if p := os.Getenv("DWSHELL_REMOTE_PASSWORD"); p != "" && !retry { return p, nil } if !xterm.IsTerminal(int(os.Stdin.Fd())) { - return "", fmt.Errorf("agent requires a password for %s@%s; set DWSHELL_REMOTE_PASSWORD", u, host) + return "", fmt.Errorf("agent requires a password for %s@%s; set DWSHELL_REMOTE_PASSWORD", u, agent) } - label := fmt.Sprintf("%s@%s's password: ", u, host) + label := fmt.Sprintf("%s@%s's password: ", u, agent) if retry { label = "Sorry, try again.\n" + label } @@ -396,12 +396,12 @@ func interactivePassword(user, host string) shell.PasswordFunc { // nonInteractivePassword supplies the remote password for -c from the // environment (or a flag); it never prompts. -func nonInteractivePassword(user, host string) shell.PasswordFunc { +func nonInteractivePassword(user, agent string) shell.PasswordFunc { return func(u string, retry bool) (string, error) { if p := os.Getenv("DWSHELL_REMOTE_PASSWORD"); p != "" && !retry { return p, nil } - return "", fmt.Errorf("agent requires a password for %s@%s; set DWSHELL_REMOTE_PASSWORD", u, host) + return "", fmt.Errorf("agent requires a password for %s@%s; set DWSHELL_REMOTE_PASSWORD", u, agent) } } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1137a99..bc024a1 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -42,9 +42,9 @@ The DWService `filesystem` app runs over the same agent session as the shell New package `internal/app/files` builds on these. -CLI, phase 1 (single files): `dwshell ls :`, -`dwshell get : [local]`, `dwshell put :`. -`host:path` endpoints; `--own`/`--shared` and agent-auth as for the shell. +CLI, phase 1 (single files): `dwshell ls :`, +`dwshell get : [local]`, `dwshell put :`. +`agent:path` endpoints; `--own`/`--shared` and agent-auth as for the shell. Phase 2, recursive + sync (rsync-style). Design decisions (recorded now): @@ -107,7 +107,7 @@ shell on an owned agent and runs `-c`. - If a trusted-device token exists: passwordless `type=device` login. - Else: `user` then `password` step; on success register a trusted device (unless `--no-trusted`) and store its token. -2. **Resolve host** → list agents + shares, match by name or id (see below), +2. **Resolve agent** → list agents + shares, match by name or id (see below), confirm it is online (`state==N`) and supports `shell`. 3. **Connect** → `agent|share connection` with a fresh signing key → agent session command URL. @@ -133,7 +133,7 @@ Two credential tiers are persisted in the config (mode 0600): `dwshell login` registers **one** and reuses it; it is never re-registered while a stored one exists. -Control-command resolution (`list`, ``, `-c`): +Control-command resolution (`list`, ``, `-c`): 1. Stored session still valid → use it. 2. Expired but a trusted device exists → silently re-login via device, refresh @@ -150,11 +150,11 @@ still cached and reused, so re-auth is infrequent, not per-command. dwshell login [--user U] [--no-trusted] # authenticate, persist session (+device) dwshell logout # forget local creds; deregister the device dwshell list [--json] # list agents + shares -dwshell [flags] # interactive shell -dwshell [flags] -c "command" # run command, capture, exit +dwshell [flags] # interactive shell +dwshell [flags] -c "command" # run command, capture, exit ``` -- ``: agent/share **name** or **id**. If a name is ambiguous (collision +- ``: agent/share **name** or **id**. If a name is ambiguous (collision between owned/shared, or duplicate share ids), require the id or `--own`/`--shared`. - Global flags: `--term `, `--no-term`, `--config `, `--json`, @@ -186,7 +186,7 @@ Agents may require an OS login for the shell (`shell.enable_authentication`), which the agent renders as an in-terminal `User:`/`Password:` prompt (see `PROTOCOL.md` §5.5). `dwshell` drives it automatically: -- The host may be `user@host`; with no `user@`, the **local username** is used +- The agent may be `user@agent`; with no `user@`, the **local username** is used (like SSH). - The username is sent automatically; the password is requested **only when the agent asks** — a TTY prompt interactively, or `DWSHELL_REMOTE_PASSWORD` for