updated spec

This commit is contained in:
2026-08-13 22:43:50 +02:00
parent 610571fc70
commit 0e15ac061b

View File

@@ -1,4 +1,4 @@
# `porthole` Spec v0.2 # `porthole` - Spec v0.2
Named, managed SSH port forwards. Wraps `ssh -L/-R/-D` so forwards are Named, managed SSH port forwards. Wraps `ssh -L/-R/-D` so forwards are
addressable by name instead of by PID, terminal tab, or shell history. addressable by name instead of by PID, terminal tab, or shell history.
@@ -8,7 +8,7 @@ addressable by name instead of by PID, terminal tab, or shell history.
## 1. Overview ## 1. Overview
**Problem:** SSH forwards are anonymous and ephemeral. They die when a **Problem:** SSH forwards are anonymous and ephemeral. They die when a
terminal closes, when a laptop sleeps, or when a network blips silently, terminal closes, when a laptop sleeps, or when a network blips - silently,
with no notification. There's no built-in way to list what's currently with no notification. There's no built-in way to list what's currently
forwarded, and multi-hop / reverse forwards have enough flag surface that forwarded, and multi-hop / reverse forwards have enough flag surface that
people end up hand-rolling shell aliases per-tunnel. people end up hand-rolling shell aliases per-tunnel.
@@ -19,9 +19,9 @@ auto-reconnect, health status, and a single command to see everything that's
open. open.
**Non-goals:** Not a replacement for a VPN or a full SOCKS/proxy manager. **Non-goals:** Not a replacement for a VPN or a full SOCKS/proxy manager.
Not a secrets manager SSH auth still comes from your existing SSH config, Not a secrets manager - SSH auth still comes from your existing SSH config,
agent, or identity files. No GUI. Surviving a full reboot/logout is also agent, or identity files. No GUI. Surviving a full reboot/logout is also
out of scope for v0.1 see §8. out of scope for v0.1 - see §8.
--- ---
@@ -51,7 +51,7 @@ A saved definition. Does not imply anything is running.
Exists only while a profile is open. Tracked separately from the profile so Exists only while a profile is open. Tracked separately from the profile so
`list`/`status` can report live data without touching the saved definition. `list`/`status` can report live data without touching the saved definition.
**The absence of an instance file is what "closed" means** there is no **The absence of an instance file is what "closed" means** - there is no
separate `down` state; see §3 for exactly when the file is created/removed. separate `down` state; see §3 for exactly when the file is created/removed.
| Field | Type | Notes | | Field | Type | Notes |
@@ -59,7 +59,7 @@ separate `down` state; see §3 for exactly when the file is created/removed.
| `name` | string | FK to profile | | `name` | string | FK to profile |
| `pid` | int | Supervisor process PID, not raw `ssh` PID | | `pid` | int | Supervisor process PID, not raw `ssh` PID |
| `state` | enum | `up` \| `reconnecting` \| `error` | | `state` | enum | `up` \| `reconnecting` \| `error` |
| `opened_at` | timestamp | Anchor for "session uptime" (§5.5) set once, at `open` | | `opened_at` | timestamp | Anchor for "session uptime" (§5.5) - set once, at `open` |
| `connected_at` | timestamp? | Start of the *current* unbroken connection; resets each reconnect (§5.5) | | `connected_at` | timestamp? | Start of the *current* unbroken connection; resets each reconnect (§5.5) |
| `last_error` | string? | Most recent failure message, if any | | `last_error` | string? | Most recent failure message, if any |
| `reconnect_count` | int | Since last manual `open` | | `reconnect_count` | int | Since last manual `open` |
@@ -71,10 +71,10 @@ separate `down` state; see §3 for exactly when the file is created/removed.
- Runtime state: `~/.local/state/porthole/<name>.json` (written by the - Runtime state: `~/.local/state/porthole/<name>.json` (written by the
supervisor, not hand-edited; absence means the profile is closed) supervisor, not hand-edited; absence means the profile is closed)
- Lock: `~/.local/state/porthole/<name>.lock` (advisory `flock`, held for - Lock: `~/.local/state/porthole/<name>.lock` (advisory `flock`, held for
the supervisor's entire lifetime see §3) the supervisor's entire lifetime - see §3)
- Logs: `~/.local/state/porthole/<name>.log`, rotated to a single - Logs: `~/.local/state/porthole/<name>.log`, rotated to a single
`<name>.log.1` backup once it exceeds 10 MiB (checked on each reconnect `<name>.log.1` backup once it exceeds 10 MiB (checked on each reconnect
attempt, not per line these are meant to run for months, unlike vmic's attempt, not per line - these are meant to run for months, unlike vmic's
short-lived CLI invocations) short-lived CLI invocations)
--- ---
@@ -82,7 +82,7 @@ separate `down` state; see §3 for exactly when the file is created/removed.
## 3. Supervisor architecture ## 3. Supervisor architecture
`open` has to hand off to a process that keeps running after the invoking `open` has to hand off to a process that keeps running after the invoking
shell/terminal exits the same one-shot-CLI-can't-host-a-daemon problem shell/terminal exits - the same one-shot-CLI-can't-host-a-daemon problem
`vmic` solves by spawning detached `pw-loopback` subprocesses tracked by `vmic` solves by spawning detached `pw-loopback` subprocesses tracked by
pid. porthole has no external long-running helper to shell out to (there's pid. porthole has no external long-running helper to shell out to (there's
no `ssh-loopback` equivalent), so it supervises `ssh` itself via a hidden no `ssh-loopback` equivalent), so it supervises `ssh` itself via a hidden
@@ -90,12 +90,12 @@ re-exec of its own binary:
1. `porthole open <name>` validates the profile, tries to acquire 1. `porthole open <name>` validates the profile, tries to acquire
`<name>.lock` (if already held by a live pid: no-op, print status, exit `<name>.lock` (if already held by a live pid: no-op, print status, exit
0 see §5.2), then spawns *itself* via `std::env::current_exe()` with a 0 - see §5.2), then spawns *itself* via `std::env::current_exe()` with a
hidden subcommand: `porthole __supervise <name>`. hidden subcommand: `porthole __supervise <name>`.
2. The spawned process detaches before doing anything else: `stdin` from 2. The spawned process detaches before doing anything else: `stdin` from
`/dev/null`, `stdout`/`stderr` appended to `<name>.log`, and `/dev/null`, `stdout`/`stderr` appended to `<name>.log`, and
`libc::setsid()` called via `CommandExt::pre_exec` so it leaves the `libc::setsid()` called via `CommandExt::pre_exec` so it leaves the
parent's process group/session it survives the terminal closing and parent's process group/session - it survives the terminal closing and
doesn't receive the shell's Ctrl-C/SIGHUP. doesn't receive the shell's Ctrl-C/SIGHUP.
3. The foregrounding `open` call blocks briefly (bounded, a few seconds) 3. The foregrounding `open` call blocks briefly (bounded, a few seconds)
waiting for the supervisor to write its pid + initial `state` into the waiting for the supervisor to write its pid + initial `state` into the
@@ -110,11 +110,11 @@ re-exec of its own binary:
which is unreliable) is what distinguishes an intentional `close` from which is unreliable) is what distinguishes an intentional `close` from
a dropped connection: `close` sends SIGTERM to the *supervisor* pid, a dropped connection: `close` sends SIGTERM to the *supervisor* pid,
whose handler kills its `ssh` child, waits briefly, deletes its lock and whose handler kills its `ssh` child, waits briefly, deletes its lock and
instance file, and exits any *other* way the `ssh` child ends (any instance file, and exits - any *other* way the `ssh` child ends (any
exit status) is treated as a failure to reconnect from, per §4. Because exit status) is treated as a failure to reconnect from, per §4. Because
`step 2`'s `setsid()` makes the supervisor its own process group leader `step 2`'s `setsid()` makes the supervisor its own process group leader
and `ssh` inherits that group, `--force` sends SIGKILL to the whole and `ssh` inherits that group, `--force` sends SIGKILL to the whole
group (`kill(-pid, SIGKILL)`) rather than just the supervisor pid a group (`kill(-pid, SIGKILL)`) rather than just the supervisor pid - a
plain single-pid SIGKILL would leave `ssh` running, orphaned and plain single-pid SIGKILL would leave `ssh` running, orphaned and
untracked, since a killed process can't forward anything to its child. untracked, since a killed process can't forward anything to its child.
A supervisor that exits on its own due to a fatal failure (§4.2) leaves A supervisor that exits on its own due to a fatal failure (§4.2) leaves
@@ -128,17 +128,24 @@ regardless of the user's own `~/.ssh/config`:
| Flag | Why | | Flag | Why |
|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-o BatchMode=yes` | A headless supervised process must never block on a password/passphrase/host-key TTY prompt without this, a first connection to an unknown host or a locked key just hangs forever, indistinguishable from "reconnecting." | | `-o BatchMode=yes` | A headless supervised process must never block on a password/passphrase/host-key TTY prompt - without this, a first connection to an unknown host or a locked key just hangs forever, indistinguishable from "reconnecting." |
| `-o StrictHostKeyChecking=accept-new` | Trust-on-first-use for a host with no `known_hosts` entry yet; still hard-fails if a *known* host's key later changes. Without this, `BatchMode=yes` turns a brand-new host into an immediate fatal failure, since `ssh` has no way to prompt for acceptance. |
| `-o LogLevel=ERROR` | Suppresses the routine "Permanently added ... to the list of known hosts" line `accept-new` produces on first connect, keeping profile logs free of non-error noise. |
| `-o ExitOnForwardFailure=yes` | Makes `ssh` exit non-zero immediately if the requested forward can't be bound, instead of staying up as a plain (forward-less) session that *looks* healthy. | | `-o ExitOnForwardFailure=yes` | Makes `ssh` exit non-zero immediately if the requested forward can't be bound, instead of staying up as a plain (forward-less) session that *looks* healthy. |
| `-o ConnectTimeout=10` | Bounds how long one connection attempt can hang before porthole's own backoff logic (§4.1) gets a turn. | | `-o ConnectTimeout=10` | Bounds how long one connection attempt can hang before porthole's own backoff logic (§4.1) gets a turn. |
| `-N` | No remote command — porthole only ever wants the forward, never a shell. | | `-o ServerAliveCountMax=3` | Paired with `ServerAliveInterval` (below), makes dead-connection detection time deterministic (`keepalive × 3`) instead of depending on `ssh`'s compiled-in default. |
| `-J <hops>` + positional target | See below — `--via`'s *last* hop is the actual connection target, not another jump. | | `-o ControlMaster=no`, `-o ControlPath=none` | Blocks `ssh` connection multiplexing, so a `ControlMaster`/`ControlPersist` setting in the user's own `~/.ssh/config` can't make porthole's process silently share a control socket with another session. Supervision (§3) assumes one spawned process owns one tunnel exclusively. |
| `-o ClearAllForwardings=yes` | Ignores any `LocalForward`/`RemoteForward`/`DynamicForward` the user's `~/.ssh/config` declares for the matched host, so the profile's own forward is the only one that ever applies. |
| `-o IdentitiesOnly=yes` | Added only when the profile sets `identity` (alongside `-i`, below) - stops `ssh` from also offering agent/default keys, which avoids authentication-failure lockouts on servers with a low `MaxAuthTries`. |
| `-N` | No remote command - porthole only ever wants the forward, never a shell. |
| `-T` | No pseudo-tty. Redundant with `-N` (no command runs), kept as insurance against a server-side `sshd_config` forcing one anyway. |
| `-J <hops>` + positional target | See below - `--via`'s *last* hop is the actual connection target, not another jump. |
`ServerAliveInterval` comes from the profile's `keepalive` field (not `ServerAliveInterval` comes from the profile's `keepalive` field (not
hardcoded), so it stays user-tunable. hardcoded), so it stays user-tunable.
**`--via``ssh` argument translation:** `ssh -J a,b,c` is not itself a **`--via``ssh` argument translation:** `ssh -J a,b,c` is not itself a
valid invocation `-J` only ever carries jump hosts *before* the final valid invocation - `-J` only ever carries jump hosts *before* the final
hop; `ssh` still needs a positional `destination` to actually connect (and hop; `ssh` still needs a positional `destination` to actually connect (and
run the forward from). So porthole splits `--via`'s comma list at the last run the forward from). So porthole splits `--via`'s comma list at the last
entry: everything before it becomes `-J`'s value (omitted entirely if entry: everything before it becomes `-J`'s value (omitted entirely if
@@ -146,7 +153,7 @@ entry: everything before it becomes `-J`'s value (omitted entirely if
target argument. `--via jumpbox``ssh ... jumpbox` (no `-J`). `--via target argument. `--via jumpbox``ssh ... jumpbox` (no `-J`). `--via
bastion1,bastion2``ssh -J bastion1 ... bastion2` (connect through bastion1,bastion2``ssh -J bastion1 ... bastion2` (connect through
bastion1, forward runs from bastion2). This is also why `--via` is bastion1, forward runs from bastion2). This is also why `--via` is
**required**, not optional (see §5.1) there is no other field **required**, not optional (see §5.1) - there is no other field
representing "the host `ssh` actually connects to"; `--via`'s last hop representing "the host `ssh` actually connects to"; `--via`'s last hop
*is* that field. *is* that field.
@@ -160,8 +167,8 @@ representing "the host `ssh` actually connects to"; `--via`'s last hop
- Each consecutive failed attempt doubles the delay, capped at - Each consecutive failed attempt doubles the delay, capped at
`backoff_max` (default 60s). `backoff_max` (default 60s).
- The backoff counter resets to the base delay once a connection has - The backoff counter resets to the base delay once a connection has
stayed up continuously for 60s an in-memory supervisor detail, not stayed up continuously for 60s - an in-memory supervisor detail, not
persisted to the instance file so one good connection after a flaky persisted to the instance file - so one good connection after a flaky
patch doesn't leave the *next* reconnect waiting a full capped delay. patch doesn't leave the *next* reconnect waiting a full capped delay.
- If `reconnect` is `false`, there is no retry loop at all: a single - If `reconnect` is `false`, there is no retry loop at all: a single
failed attempt goes straight to `state: error` and the supervisor exits. failed attempt goes straight to `state: error` and the supervisor exits.
@@ -185,8 +192,8 @@ profile as effectively broken (`state: error`, stop) rather than retrying
under an unrecognized failure forever. `last_error` always holds the raw under an unrecognized failure forever. `last_error` always holds the raw
`ssh` message either way, for `status` to show. `ssh` message either way, for `status` to show.
A profile that lands in `state: error` stays that way including its A profile that lands in `state: error` stays that way - including its
instance file until the user runs `open` again (fresh attempt, fresh instance file - until the user runs `open` again (fresh attempt, fresh
backoff) or `close` (clears it). It is deliberately *not* self-healing past backoff) or `close` (clears it). It is deliberately *not* self-healing past
a fatal classification. a fatal classification.
@@ -201,13 +208,13 @@ Saves a new profile. Does **not** open it.
| Flag | Arg | Required | Default | Description | | Flag | Arg | Required | Default | Description |
|--------------------|-------------------------------|-------------------|---------------------------|----------------------------------------| |--------------------|-------------------------------|-------------------|---------------------------|----------------------------------------|
| `-l, --local` | `[bind:]port:host:hostport` | one of `-l/-r/-d` | | Local forward: your machine → remote | | `-l, --local` | `[bind:]port:host:hostport` | one of `-l/-r/-d` | - | Local forward: your machine → remote |
| `-r, --remote` | `[bind:]port:host:hostport` | one of `-l/-r/-d` | | Remote forward: remote → your machine | | `-r, --remote` | `[bind:]port:host:hostport` | one of `-l/-r/-d` | - | Remote forward: remote → your machine |
| `-d, --dynamic` | `[bind:]port` | one of `-l/-r/-d` | | Dynamic forward (SOCKS proxy) | | `-d, --dynamic` | `[bind:]port` | one of `-l/-r/-d` | - | Dynamic forward (SOCKS proxy) |
| `--via` | `[user@]host[:port]` | **yes** | | One hop chain entry; repeatable (`--via a --via b`) and/or comma-separated (`--via a,b`) - the last hop is the `ssh` connection target, any before it are `-J` jumps (§3.1) | | `--via` | `[user@]host[:port]` | **yes** | - | One hop chain entry; repeatable (`--via a --via b`) and/or comma-separated (`--via a,b`) - the last hop is the `ssh` connection target, any before it are `-J` jumps (§3.1) |
| `-u, --user` | `user` | no | current user / ssh_config | Default user for the final target and any `--via` hop that doesn't specify its own | | `-u, --user` | `user` | no | current user / ssh_config | Default user for the final target and any `--via` hop that doesn't specify its own |
| `-i, --identity` | `path` | no | ssh_config default | | | `-i, --identity` | `path` | no | ssh_config default | |
| `-p, --port` | `port` | no | `22` | SSH port on the final target only a `--via` hop needs its own inline `:port` if it isn't 22 | | `-p, --port` | `port` | no | `22` | SSH port on the final target only - a `--via` hop needs its own inline `:port` if it isn't 22 |
| `--reconnect` | `bool` | no | `true` | Auto-reconnect on drop (§4) | | `--reconnect` | `bool` | no | `true` | Auto-reconnect on drop (§4) |
| `--retry-interval` | `seconds` | no | `5` | Base reconnect delay (§4.1) | | `--retry-interval` | `seconds` | no | `5` | Base reconnect delay (§4.1) |
| `--backoff-max` | `seconds` | no | `60` | Cap on the doubling reconnect delay (§4.1) | | `--backoff-max` | `seconds` | no | `60` | Cap on the doubling reconnect delay (§4.1) |
@@ -221,7 +228,7 @@ error.
`[a-z0-9_-]+`. `[a-z0-9_-]+`.
- `mapping` port syntax validated against the same grammar `ssh` accepts; - `mapping` port syntax validated against the same grammar `ssh` accepts;
stored verbatim as whichever `-l/-r/-d` payload was given (without the stored verbatim as whichever `-l/-r/-d` payload was given (without the
flag itself) `kind` records which one it was, so re-deriving the right flag itself) - `kind` records which one it was, so re-deriving the right
`-L`/`-R`/`-D` flag at `open` time is a lookup, not a re-parse. `-L`/`-R`/`-D` flag at `open` time is a lookup, not a re-parse.
- `--via` requires at least one hop (its last entry is the mandatory - `--via` requires at least one hop (its last entry is the mandatory
connection target, see §3.1); hosts are resolved/checked against connection target, see §3.1); hosts are resolved/checked against
@@ -246,7 +253,7 @@ Starts a saved forward as a background-supervised process (§3).
|--------------------|--------------------------------------------------------------------------------------------------------------------| |--------------------|--------------------------------------------------------------------------------------------------------------------|
| `-f, --foreground` | Run attached in current shell instead of detaching. Ctrl-C closes it cleanly (removes the instance file, same as `close`). | | `-f, --foreground` | Run attached in current shell instead of detaching. Ctrl-C closes it cleanly (removes the instance file, same as `close`). |
| `--once` | Open without auto-reconnect, regardless of profile setting (§4.1). | | `--once` | Open without auto-reconnect, regardless of profile setting (§4.1). |
| `--all` | Ignore `<name>`; open every profile with `reconnect: true` that isn't already running. Per-profile failures are warnings, not a whole-batch failure this exists specifically as the hook for external autostart mechanisms, see §8. | | `--all` | Ignore `<name>`; open every profile with `reconnect: true` that isn't already running. Per-profile failures are warnings, not a whole-batch failure - this exists specifically as the hook for external autostart mechanisms, see §8. |
**Behavior:** **Behavior:**
- If already open (a live supervisor pid holds `<name>.lock`): no-op, - If already open (a live supervisor pid holds `<name>.lock`): no-op,
@@ -254,7 +261,7 @@ Starts a saved forward as a background-supervised process (§3).
- If the instance file exists but its pid is dead (crash, or the machine - If the instance file exists but its pid is dead (crash, or the machine
rebooted): treated as not-running, proceeds to spawn a fresh supervisor. rebooted): treated as not-running, proceeds to spawn a fresh supervisor.
- If port bind fails (already in use): exit non-zero with the conflicting - If port bind fails (already in use): exit non-zero with the conflicting
process info if discoverable (`lsof`-style lookup best-effort, degrades process info if discoverable (`lsof`-style lookup - best-effort, degrades
to a plain "port in use" message if `lsof`/`ss` isn't on `PATH`), don't to a plain "port in use" message if `lsof`/`ss` isn't on `PATH`), don't
silently retry. silently retry.
- `open` blocks briefly (bounded, a few seconds) waiting for the detached - `open` blocks briefly (bounded, a few seconds) waiting for the detached
@@ -284,7 +291,7 @@ is untouched.
| `--force` | SIGKILL the supervisor (and its `ssh` child) immediately instead of graceful SIGTERM + wait | | `--force` | SIGKILL the supervisor (and its `ssh` child) immediately instead of graceful SIGTERM + wait |
`close` on a profile that's already stopped (no live pid) is a no-op, exit `close` on a profile that's already stopped (no live pid) is a no-op, exit
0 it still clears a stale instance file left over from a crash, same as 0 - it still clears a stale instance file left over from a crash, same as
the crash-recovery path in `open`. the crash-recovery path in `open`.
**Examples:** **Examples:**
@@ -297,13 +304,13 @@ porthole close db --force
### 5.4 `porthole edit <name> [flags]` ### 5.4 `porthole edit <name> [flags]`
Updates a saved profile. Accepts the same flags as `add` (all optional Updates a saved profile. Accepts the same flags as `add` (all optional -
only provided flags are changed). only provided flags are changed).
**Decision:** `edit` never restarts a running instance, and there is no **Decision:** `edit` never restarts a running instance, and there is no
`--restart` flag. If `<name>` is currently running, `edit` prints a warning `--restart` flag. If `<name>` is currently running, `edit` prints a warning
that the change won't take effect until the next `open`/`close` cycle and that the change won't take effect until the next `open`/`close` cycle and
exits 0 consistent with vmic's `edit`, which never auto-migrates a live exits 0 - consistent with vmic's `edit`, which never auto-migrates a live
topology without telling the user exactly what to run instead. Keeping topology without telling the user exactly what to run instead. Keeping
this explicit avoids a footgun where editing a profile silently bounces a this explicit avoids a footgun where editing a profile silently bounces a
tunnel someone else might be relying on. tunnel someone else might be relying on.
@@ -327,11 +334,11 @@ Deep-dive health for one forward.
**Output includes:** **Output includes:**
- Profile summary (kind, mapping, via, user) - Profile summary (kind, mapping, via, user)
- Current state (`up` / `reconnecting` / `error`, or `closed` if no - Current state (`up` / `reconnecting` / `error`, or `closed` if no
instance file exists at all §2.2) instance file exists at all - §2.2)
- **Session uptime**: elapsed time since `opened_at` (the original `open` - **Session uptime**: elapsed time since `opened_at` (the original `open`
call), regardless of intervening reconnects call), regardless of intervening reconnects
- **Connection uptime**: elapsed time since `connected_at` (the current - **Connection uptime**: elapsed time since `connected_at` (the current
unbroken connection) resets on every reconnect, absent while unbroken connection) - resets on every reconnect, absent while
`reconnecting`/`error` `reconnecting`/`error`
- Reconnect count and timestamp of last reconnect - Reconnect count and timestamp of last reconnect
- Last error message, if any - Last error message, if any
@@ -347,7 +354,7 @@ porthole status db
### 5.6 `porthole list` ### 5.6 `porthole list`
Alias: `ls` Alias: `ls`
All saved profiles with live status. Fast, scannable no deep diagnostics All saved profiles with live status. Fast, scannable - no deep diagnostics
(use `status` for that). (use `status` for that).
**Columns:** `NAME KIND MAPPING VIA STATE UPTIME` **Columns:** `NAME KIND MAPPING VIA STATE UPTIME`
@@ -382,7 +389,7 @@ Deletes a saved profile. Closes it first if running.
An instance left running via `--keep-running` is no longer visible to An instance left running via `--keep-running` is no longer visible to
`list`/`status` (its profile is gone), but is still caught by `wipe` `list`/`status` (its profile is gone), but is still caught by `wipe`
(§5.8), which matches by supervisor process signature rather than tracked (§5.8), which matches by supervisor process signature rather than tracked
state same as vmic's `wipe`. state - same as vmic's `wipe`.
--- ---
@@ -391,7 +398,7 @@ Alias: `reset`
Closes and deletes **every** forward, including any supervisor/`ssh` Closes and deletes **every** forward, including any supervisor/`ssh`
processes matching porthole's signature that aren't in the current profile processes matching porthole's signature that aren't in the current profile
store (e.g. orphaned after a crash). Confirmation prompt unless `--yes` store (e.g. orphaned after a crash). Confirmation prompt unless `--yes` -
unlike vmic's `wipe` (no prompt), porthole's tears down active network unlike vmic's `wipe` (no prompt), porthole's tears down active network
tunnels rather than just audio routing, so the extra confirmation is a tunnels rather than just audio routing, so the extra confirmation is a
deliberate, not accidental, difference. deliberate, not accidental, difference.
@@ -422,8 +429,8 @@ Generates a shell completion script. `<shell>` ∈ `bash`, `zsh`, `fish`.
| Code | Meaning | | Code | Meaning |
|------|--------------------------------------------------------------------| |------|--------------------------------------------------------------------|
| `0` | Success | | `0` | Success |
| `1` | Error see the printed message | | `1` | Error - see the printed message |
| `2` | CLI usage error (bad/missing arguments clap's own exit code) | | `2` | CLI usage error (bad/missing arguments - clap's own exit code) |
Kept deliberately flat, matching vmic: granular per-failure codes (not Kept deliberately flat, matching vmic: granular per-failure codes (not
found vs. already-exists vs. bind conflict, etc.) only pay for themselves found vs. already-exists vs. bind conflict, etc.) only pay for themselves
@@ -437,25 +444,11 @@ that yet. Every error still gets a specific, greppable message on stderr.
**Explicitly out of scope for v0.1**, and worth calling out since it's **Explicitly out of scope for v0.1**, and worth calling out since it's
adjacent to the overview's own motivating problem: the supervisor is a adjacent to the overview's own motivating problem: the supervisor is a
plain process, not a system service, so a full reboot or logout kills it plain process, not a system service, so a full reboot or logout kills it
along with everything else reconnect (§4) covers network blips and along with everything else - reconnect (§4) covers network blips and
sleep/wake, not "the machine came back up." sleep/wake, not "the machine came back up."
The only piece porthole commits to now is `open --all` (§5.2), which exists The only piece porthole commits to now is `open --all` (§5.2), which exists
specifically so an external mechanism can drive it a systemd `--user` specifically so an external mechanism can drive it - a systemd `--user`
unit, a login item, a cron `@reboot` line. porthole does not register, unit, a login item, a cron `@reboot` line. porthole does not register,
manage, or template any of those itself; that's a v0.2+ decision (§9) once manage, or template any of those itself; that's a v0.2+ decision (§9) once
it's clear which one people actually want. it's clear which one people actually want.
---
## 9. Open questions for v0.2
- Autostart: if/when to commit to a specific mechanism (systemd user unit
template? login item?) now that `open --all` exists as the hook.
- Multi-hop `--via`: confirmed as a straight passthrough to `ssh -J`
(§3.1/§5.1) for v0.1; revisit only if per-hop supervised status ever
becomes a real ask.
- Templating (`dbtun`-style): saved "kind" templates (e.g. `--template
postgres` implies port 5432) — worth adding as sugar over `add`, or
scope creep?
- Config export/import for moving profiles between machines.