@dmsdc-ai/aigentry-telepty 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,335 @@
2
2
 
3
3
  All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.4.4] - 2026-05-25
8
+
9
+ ### Added — TELEPTY_SUBMIT_FORCE_DEFAULT env var (task #453)
10
+
11
+ - **Environment default for forced submit** —
12
+ `TELEPTY_SUBMIT_FORCE_DEFAULT=1` makes `telepty inject --submit` behave as
13
+ if `--submit-force` was passed, without changing behavior for users who leave
14
+ the env var unset. Accepted truthy values are `1`, `true`, `yes`, and `on`
15
+ after whitespace trimming and case normalization.
16
+ - **Per-call opt-out** — `telepty inject --submit --no-submit-force ...`
17
+ restores the normal gated submit behavior even when the environment default
18
+ is enabled. Explicit `--submit-force` remains valid and wins when supplied.
19
+ - **Automation caveat** — this is intended for orchestrators that already know
20
+ their targets are real, initialized REPLs. It bypasses the safety gate that
21
+ prevents submit during target boot, avoiding the transient 504
22
+ `bootstrap_not_ready` path where text lands in the input box but Enter is not
23
+ sent.
24
+ - **Observability** — env-driven calls emit
25
+ `[telepty inject] submit-force=env-default (TELEPTY_SUBMIT_FORCE_DEFAULT=1)`
26
+ to stderr before posting `/submit`.
27
+
28
+ ### Notes — TELEPTY_SUBMIT_FORCE_DEFAULT env var
29
+
30
+ - **Test suite** — `npm test --silent` passes 403 / 403, including the new
31
+ `test/inject-submit-force-env.test.js` coverage for env-off, env-on,
32
+ `--no-submit-force`, explicit `--submit-force`, and value normalization.
33
+ - **Snyk SAST** — the requested `snyk_code_scan` MCP tool was not available in
34
+ this session, so the installed Snyk CLI was used. After replacing the new
35
+ localhost HTTP fixture with a loopback `net` test server and generating the
36
+ test auth token at runtime, `test/inject-submit-force-env.test.js` has 0
37
+ findings and the changed `cli.js` line ranges have 0 findings. The full repo
38
+ CLI scan still reports the pre-existing baseline findings in legacy `cli.js`,
39
+ `daemon.js`, existing HTTP test fixtures, and `scripts/bridge-phase1.js`. No
40
+ suppressions were added.
41
+
42
+ ### Added — Idle session cleanup (issue #34)
43
+
44
+ - **Idle visibility in `telepty list`** — session rows now append
45
+ `💤 idle (Xh Ym)` when `lastActivityAt` is more than 60 seconds old.
46
+ `telepty list --json` preserves `lastActivityAt` and adds
47
+ `idle_seconds` for machine consumers.
48
+ - **Transport-agnostic lifecycle helpers** — `src/lifecycle.js` centralizes
49
+ duration parsing, idle-victim selection, older-than cleanup selection, and
50
+ PTY/process-level teardown. It uses native POSIX signals and the existing
51
+ Windows `taskkill` helper; it does not call cmux or any workspace-host API.
52
+ - **`telepty kill <id> [--force] [--timeout <sec>]`** — graceful teardown
53
+ sends SIGTERM, waits up to the configured timeout, then escalates to
54
+ SIGKILL. `--force` sends SIGKILL immediately. Successful teardown removes
55
+ the daemon registry entry and session socket artifacts.
56
+ - **Opt-in idle TTL** — daemon config loads `~/.telepty/config.json` or a
57
+ simple `config.yaml` / `config.yml` with `idle_ttl_default` (`off` by
58
+ default). `telepty allow --idle-ttl <duration|off>` stores a per-session
59
+ override. The daemon reaper emits a `tracing` event with
60
+ `action: "idle_ttl_auto_kill"` before auto-teardown.
61
+ - **`telepty clean --older-than <duration> [--idle] [--dry-run]`** — default
62
+ ghost-only cleanup remains unchanged. The new opt-in path removes sessions
63
+ older than the threshold by `createdAt`, or by `lastActivityAt` when
64
+ `--idle` is set; `--dry-run` reports targets without deleting them.
65
+
66
+ ### Notes — Idle session cleanup
67
+
68
+ - **Test suite** — `npm test` passes 397 / 397, including transport-agnostic
69
+ lifecycle coverage for headless and cmux-backed fixtures.
70
+ - **Snyk SAST** — the requested `snyk_code_scan` MCP tool was not available in
71
+ this session, so the installed Snyk CLI was used. New standalone files
72
+ (`src/lifecycle.js`, `src/config-file.js`, and the new lifecycle tests)
73
+ scan with 0 findings. Scanning changed legacy entrypoints (`daemon.js` and
74
+ `cli.js`) still reports the repo's pre-existing baseline findings
75
+ previously noted for Phase 2: CLI path-traversal / command-injection flows
76
+ and daemon route-level prototype-pollution / throttling / command-execution
77
+ warnings. No suppressions were added.
78
+
79
+ ### Added — Phase 5a-prime (task #430 P5a-prime)
80
+
81
+ - **`crates/telepty-cross-machine/`** — standalone Rust library plus
82
+ `telepty-cross-machine-bin` for manual HTTP peer operations only. Scope is
83
+ deliberately reduced from the two rejected Phase 5 drafts: no JS bridge, no
84
+ subprocess envelope contract, no outbox queue, no npm distribution, no SSH
85
+ transport, and no `cli.js` / `daemon.js` / `cross-machine.js` changes. This
86
+ follows the review basis in
87
+ `docs/reports/2026-05-24-phase5-spec-codex-review.md` and
88
+ `docs/reports/2026-05-24-phase5a-spec-codex-rereview.md`, which identified
89
+ bridge-consumed binary contracts as the unstable surface.
90
+ - **Manual HTTP subcommands** — `connect-http`, `list-peer-sessions`,
91
+ `inject-peer`, `list-peers`, and `remove-peer`. `connect-http` probes
92
+ `/api/health`, treats `/api/meta` as non-fatal, persists token-backed HTTP
93
+ peers to `~/.telepty/peers.json`, and prints human-friendly status. List
94
+ commands support free-form `--json` output without an envelope contract.
95
+ `inject-peer` fails fast on unreachable peers; there is no queueing.
96
+ - **Backward-compatible `peers.json` handling** — missing `transport` defaults
97
+ to SSH and round-trips without injecting a `transport` field, preserving the
98
+ JS-era legacy schema used by the SSH path. HTTP operations on SSH peers exit
99
+ 4 with the explicit JS-path diagnostic required by Phase 5a-prime.
100
+ - **Addressing and atomic-write parity** — Rust host parsing mirrors
101
+ `host-spec.js` URL stripping, embedded-port, and IPv6 behavior. Peer updates
102
+ use the fsync-backed `tmp + fsync(tmp) + rename + fsync(parent_dir)` pattern
103
+ copied from `crates/telepty-supervisor-core/src/manifest.rs`.
104
+ - **Build metadata** — `build.rs` embeds git hash, dirty flag, and build
105
+ timestamp. `telepty-cross-machine-bin --version` prints
106
+ `telepty-cross-machine 0.0.1 (<git-hash>[, dirty])`.
107
+
108
+ ### Added — Phase 2 Node↔Rust IPC bridge (task #430 P2)
109
+
110
+ - **`src/bridge/supervisor-ipc.js`** — Node `BridgeClient` speaking NDJSON
111
+ over the per-session UDS (`~/.telepty/sessions/<sid>/supervisor.sock`).
112
+ Surface: `connect(socketPath)` → client; `send(frame)` fire-and-forget;
113
+ `request(frame, {timeoutMs})` with trace_id correlation (resolves on
114
+ matching `pong`, rejects on matching `error` with the supervisor's
115
+ `ERR_*` code preserved, rejects `ERR_TIMEOUT` on drift); `subscribe({sid,
116
+ signal})` returning an `AsyncIterator<Frame>` that respects `AbortSignal`
117
+ and `iterator.return()` for clean unsubscribe; `close()` idempotent and
118
+ rejects any pending requests with `ERR_SUPERVISOR_GONE`. Per synthesis
119
+ ADR §6.2 (B3), `trace_id` is auto-filled for kinds the supervisor
120
+ mandates it on (`inject`/`output`/`signal`/`kill`/`delete`); pong
121
+ reflects ping `trace_id` so correlation works without server-side
122
+ per-client state. Malformed inbound lines surface as synthetic
123
+ `ERR_BAD_FRAME` to subscribers — the connection survives garbage so a
124
+ later-good frame still flows.
125
+ - **`src/bridge/j3-shim.js`** — 0.3.x→NDJSON translator covering the P2
126
+ subset (`inject` / `output` stream / `list`). `inject(sid, prompt, opts)`
127
+ opens a one-shot connection, sends an inject frame, watches 150 ms for a
128
+ trace_id-correlated `error` frame (catches B3 / `ERR_DUPLICATE_OP` /
129
+ `ERR_SHUTTING_DOWN`), and returns `{ success, trace_id, code?, error?
130
+ }`. `output(sid, {fromSeq, signal})` is an async generator yielding
131
+ `{ data, seq }` per `Frame::output` and a final `{ exit, ... }` on
132
+ `shutdown_drain`; consumer-driven cancellation via `AbortSignal` or
133
+ `break`. `list()` scans `~/.telepty/sessions/*/manifest.json` and
134
+ surfaces only `ready` / `draining` sessions (tombstones excluded — they
135
+ lack a usable socket; operators still see them via
136
+ `telepty-supervisor-bin --list`). Sessions root is resolved lazily so
137
+ `TELEPTY_SESSIONS_DIR` redirects work without re-requiring the module.
138
+ - **`src/bridge/supervisor-launcher.js`** — per-session Rust supervisor
139
+ process lifecycle. `resolveBinary({env})` chains
140
+ `TELEPTY_SUPERVISOR_BIN` (env override) → `./target/release/telepty-
141
+ supervisor-bin` (repo-relative) → `./target/debug/...` → `which
142
+ telepty-supervisor-bin` (PATH) and throws `ERR_BIN_NOT_FOUND` otherwise.
143
+ `spawn({sid, argv, cwd?, binary?, env?, stdio?})` shells out to the
144
+ binary with `stdio: ['ignore', 'ignore', 'pipe']` (default) so the
145
+ supervisor's M1/M2 stdout PTY-mirror doesn't bleed into the parent.
146
+ `waitReady(sid, {timeoutMs, pollMs})` gates on BOTH manifest status
147
+ (`ready`/`draining`) AND `fs.existsSync(socket)` — supervisor.rs writes
148
+ the manifest *before* `ipc::bind_socket`, so a manifest-only gate races
149
+ the bind; checking both closes the window without touching the
150
+ supervisor crate. `isAlive(sid)` cross-checks manifest pid via
151
+ `process.kill(pid, 0)`.
152
+ - **`cli.js` minimal-touch wiring** (Rule 29 surgical, +27 LOC, no
153
+ refactor of adjacent code):
154
+ - `cmd === 'list'` (L915): merges `bridgeShim.list()` into the daemon-
155
+ discovered session set, de-duplicated by `id`. Daemon entries remain
156
+ source-of-truth when both surfaces report the same session; bridge
157
+ entries fill the gap when daemon is down. Wrapped in a defensive
158
+ `try/catch` so any bridge failure leaves the daemon list intact.
159
+ - `cmd === 'inject'` LOCAL path (L1755): bridge-first attempt when
160
+ `!useSubmit && bridgeShim.findSupervisorManifest(target.id)` is
161
+ truthy. On bridge success, prints the existing
162
+ `✅ Context injected successfully into '...' (bridge).` line and
163
+ returns; on bridge failure, falls through to the unchanged daemon
164
+ HTTP path so caller-visible behavior never degrades. The gated
165
+ `--submit` semantics (render-gate / retry / `--submit-force`) stay
166
+ on `daemon.js` for the migration window — P2 wire does not carry
167
+ render-gate yet.
168
+ - **`cross-machine.js` UNTOUCHED** — P2 scope is local bridge only;
169
+ remote SSH / HTTP transport stays on the existing path. P3+ owns the
170
+ remote→bridge story.
171
+
172
+ ### E2E acceptance — `telepty spawn → inject → output` works with daemon.js stopped
173
+
174
+ - `test/bridge-e2e.test.js` drives the supervisor binary directly through
175
+ `supervisor-launcher` + `j3-shim` in an isolated `HOME` so the live
176
+ daemon (if any) is never touched. The headline test launches a real
177
+ `cat -u` under the supervisor, subscribes to the output stream, injects
178
+ `ping-echo\n`, and asserts the echo arrives — proving the bridge alone
179
+ is sufficient for the primary three operations per dispatch §2.4. The
180
+ test self-skips with a clear hint when
181
+ `target/release/telepty-supervisor-bin` is absent (binary not built
182
+ yet), keeping CI without Rust toolchain green.
183
+
184
+ ### Notes — Phase 2 bridge
185
+
186
+ - **No new npm dependencies** (Constitution §17 무의존). NDJSON parsing
187
+ via `readline.createInterface` from the Node stdlib; UDS connection via
188
+ `net.createConnection({ path })`; UUIDs via `crypto.randomUUID()`. Adds
189
+ zero packages to `package.json` `dependencies`.
190
+ - **Test suite** — `npm test` 375 / 375 pass in ~24 s (343 baseline
191
+ preserved per Rule 29 + 32 new bridge tests: 14 `BridgeClient` units,
192
+ 14 `j3-shim` units, 4 E2E). Test file ratio is ~1:1 with prod LOC
193
+ (~787 prod / ~797 tests).
194
+ - **Snyk SAST** — `snyk_code_scan` on `src/bridge/` + new test files →
195
+ **0 findings**. Pre-existing `cli.js` findings (3× path-traversal on
196
+ CLI arg → `fs.readFileSync` / `fs.readdirSync` at L2345/L2347/L2656;
197
+ 2× command-injection on CLI arg → `execSync` / `node-pty.spawn` at
198
+ L471/L1116) are unchanged by this work — they live in dataflows
199
+ unrelated to the L915/L1755 bridge insertions and are tracked
200
+ separately (consistent with the v0.4.3 baseline). No new Snyk findings
201
+ attributable to this phase.
202
+ - **Path budget** — bridge prod 787 LOC + bridge tests 797 LOC =
203
+ ~1.6 kLOC, well within the dispatch envelope (bridge ~400-700 + shim
204
+ ~200-400 + tests ~300-500). cli.js delta is +27 LOC pure additions
205
+ with no edits to existing lines, satisfying the minimal-touch
206
+ directive.
207
+ - **Cross-platform** — UDS path is POSIX-only in P2 (Windows native
208
+ pipe = P4 per dispatch §2). Bridge unit tests and E2E gracefully
209
+ skip on `process.platform === 'win32'`; the launcher still resolves
210
+ the binary path on Windows so the eventual P4 wiring has a stub to
211
+ extend.
212
+
213
+ ### Carry-overs — Phase 2
214
+
215
+ 1. **`telepty spawn` cli command bridge wiring** — out of P2 scope per
216
+ dispatch §Goal item 4 ("inject / output / list paths"). P3 owns the
217
+ refactor that lets `telepty spawn` route through
218
+ `supervisor-launcher.spawn` for supervisor-managed sessions.
219
+ 2. **Render-gated `--submit` over bridge** — daemon.js stays as the
220
+ submit gate for the migration window. Bridge inject currently
221
+ appends a literal `\r` to the data (matching the legacy
222
+ `no_enter: false` default) without REPL readiness detection.
223
+ 3. **Single-binary `telepty supervisor` mode** — P2 still spawns the
224
+ `telepty-supervisor-bin` standalone bin. The `telepty supervisor`
225
+ subcommand mode per orchestrator decision §6.6 A is post-P2.
226
+
227
+ ### Added — Phase 1 supervisor-core-finish (task #430 P1)
228
+
229
+ - **A5 detach/reattach via UDS reconnection + log offset replay** —
230
+ `wire::Kind::Resume` frame with optional `from_seq: u64` lets a
231
+ reconnecting client request replay of `Output` frames whose `seq` is
232
+ greater than `from_seq` from `~/.telepty/sessions/<sid>/log.jsonl`
233
+ before subscribing to the live broadcast. Replay is per-connection
234
+ sequential (handler-local) — no broadcast race. Seq-less audit
235
+ frames (`shutdown_drain`) are forwarded unconditionally so a late
236
+ reattach observes terminal state.
237
+ - **A7 list discovery via filesystem manifest scan** —
238
+ `manifest::scan_sessions()` walks `~/.telepty/sessions/*/manifest.json`
239
+ with atomic per-file reads; missing / unparseable manifests are
240
+ skipped (never panics). New `telepty-supervisor-bin --list` flat
241
+ flag emits a JSON array of `Manifest` to stdout. Output shape is
242
+ the **supervisor-owned** view; the legacy `telepty list --json`
243
+ daemon view will be reconciled by the P3 cli refactor (per dispatch
244
+ §6.1). `--list` is mutually exclusive with run-mode argv.
245
+ - **A8 delete graceful drain integration test** —
246
+ `tests/delete_drain.rs` end-to-end (no goldens): graceful (SIGTERM)
247
+ and forced (SIGKILL) variants both assert manifest unlinked + socket
248
+ unlinked + `log.jsonl` contains `shutdown_drain` with correct
249
+ `exit_reason` + supervisor exits within 3 s. Production code already
250
+ existed in `supervisor::run` (kill_outcome → unlink_clean branch).
251
+ - **B3 trace_id enforcement extended to signal/kill/delete** —
252
+ `wire::validate_incoming` now rejects `Kind::Signal`, `Kind::Kill`,
253
+ `Kind::Delete` lacking `trace_id` with explicit error codes
254
+ (`signal_missing_trace_id`, `kill_missing_trace_id`,
255
+ `delete_missing_trace_id`) per C3 spec §1002 audit linkage
256
+ (`kind:"signal"` event matches originating injector trace_id;
257
+ `kind:"shutdown_drain"` carries parent_trace_id). Rejection reason
258
+ is ALSO appended to `log.jsonl` so the audit trail captures *why* a
259
+ frame was rejected even if the client disconnects before reading
260
+ the error response.
261
+ - **F3 atomic manifest write contract test** —
262
+ `tests/atomic_manifest.rs` (5 tests). Headline test
263
+ `concurrent_readers_never_observe_partial_json` runs 1 writer thread
264
+ + 6 reader threads × 800 ms; readers always see a complete-old or
265
+ complete-new manifest, never partial JSON (the rename-atomicity
266
+ guarantee). Plus golden tests for `.json.tmp` cleanup, missing-
267
+ parent-dir creation, `unlink_clean` idempotency, and tombstone
268
+ audit-field roundtrip.
269
+ - **G3 audit trail expansion** — `dispatch_ingest` now logs each
270
+ validated ingest event (`Inject` / `Signal` / `Kill` / `Delete`) to
271
+ `log.jsonl` right after `validate_incoming` passes. Ping is
272
+ intentionally skipped (heartbeat noise). Validation rejections are
273
+ also logged (closes the silent-drop gap from earlier milestones).
274
+ `audit.rs` was extended (single module per Constitution §1
275
+ lightweight) rather than fragmenting into a new audit/ submodule.
276
+ R4 TelemetryEvent translation deferred to the P3 cli bridge per
277
+ orchestrator Phase 4 decision.
278
+ - **§8.A1 Normal termination contract test** —
279
+ `tests/normal_termination.rs` (2 tests). Covers child exits 0
280
+ (assert `exit_reason: normal`, `exit_code: 0`, escalated false,
281
+ manifest unlinked) and nonzero-but-natural exit (`sh -c 'exit 7'`
282
+ — assert exit_code propagated; `Normal` is the exit *mechanism*,
283
+ not the exit *code*).
284
+
285
+ ### Performance — E1 local-inject latency bench
286
+
287
+ - New `crates/telepty-supervisor-core/benches/inject_e1.rs` custom
288
+ harness (no criterion — Constitution §17 no new Rust deps).
289
+ `[[bench]] harness = false`. Run with `cargo bench --bench inject_e1`.
290
+ 100 warmup + 1000 measured roundtrips through real supervisor
291
+ wrapping `cat`.
292
+ - **E1-p50: 0.025 ms** (p90 0.057 ms, p99 0.091 ms) on
293
+ macos/aarch64 / Mac16,8 / Apple M4 Pro — **40× under the 1 ms
294
+ target**.
295
+ - Exit code 0 iff p50 < 1 ms (CI-gateable).
296
+
297
+ ### Notes — Phase 1 supervisor-core-finish
298
+
299
+ - **No new Rust deps** (Constitution §17). `tokio` `fs` feature
300
+ enabled in workspace deps (feature flag only). `serde_json` added
301
+ to `telepty-supervisor-bin` (was already a workspace dep).
302
+ - **Rule 29 surgical** — changes scoped to
303
+ `crates/telepty-supervisor-core/` (src + tests + benches) and
304
+ `crates/telepty-supervisor-bin/`. No daemon.js / cli.js changes
305
+ (those land in P2/P3). No Windows code paths (P4 scope; cargo
306
+ features can gate but no implementation in this phase).
307
+ - **Tests** — 42 / 42 pass (23 baseline preserved + 19 new):
308
+ - Unit: +4 wire B3 (signal/kill/delete trace_id), +2 audit, +1
309
+ scan_sessions
310
+ - Integration: +3 reattach_replay (A5), +2 delete_drain (A8), +5
311
+ atomic_manifest (F3), +2 normal_termination (§8.A1)
312
+ - **Snyk SAST** — `snyk_code_scan` on `crates/` → 0 findings across
313
+ both crates. Run at each phase boundary (Phase 4+).
314
+ - **§8.A contract test parity** (per C3 spec
315
+ `docs/specs/2026-05-10-supervisor-c3-kill-gate-spec.md` §8.A): 7
316
+ of 13 Bucket-A scenarios covered + 2 extras (A5 reattach, F3
317
+ atomic) + 4 correctly deferred (Windows = P4 / Bucket B =
318
+ controlled-host out of Phase 1 spike scope). **2 follow-up
319
+ carry-overs documented**: §8.A3-tree (grandchild-cascade
320
+ killpg semantics — code already correct; explicit fixture
321
+ needed) and §8.A-reactor-stall (single-thread reactor
322
+ non-blocking invariant — code-review-only invariant; runtime
323
+ probe optional).
324
+ - **Sources of truth** — the synthesis ADR referenced in dispatch
325
+ (`docs/adr/2026-05-10-telepty-l2-architecture-q-prime-bis.md`)
326
+ and 6-phase plan (`docs/reports/2026-05-23-telepty-l2-supervisor-plan.md`)
327
+ live in the orchestrator repo, not visible from this repo.
328
+ Per Phase 1 CLDR + orchestrator hybrid (b)+(c) decision, this
329
+ work derives §19.2 contract requirements from the local
330
+ `docs/specs/2026-05-10-supervisor-c3-kill-gate-spec.md` (which
331
+ the code already cited as `SPEC-C3-r1`) plus the dispatch text
332
+ itself.
333
+
5
334
  ## [0.4.3] - 2026-05-23
6
335
 
7
336
  ### Fixed
package/README.md CHANGED
@@ -68,6 +68,23 @@ telepty broadcast "status report"
68
68
  | `telepty layout [grid\|tall\|stack]` | Arrange kitty windows |
69
69
  | `telepty update` | Update to latest version |
70
70
 
71
+ ## Environment variables
72
+
73
+ | Variable | Values | Default | Description |
74
+ |----------|--------|---------|-------------|
75
+ | `TELEPTY_SUBMIT_FORCE_DEFAULT` | `1`, `true`, `yes`, `on` to enable; unset, `0`, or `off` to disable | unset | Makes `telepty inject --submit <id> "text"` behave as if `--submit-force` was passed. |
76
+
77
+ `TELEPTY_SUBMIT_FORCE_DEFAULT=1` is for orchestrators and automation that
78
+ already know their targets are real, initialized REPLs. It avoids the transient
79
+ 504 `bootstrap_not_ready` path where injected text lands in the target input box
80
+ but the render-gated submit refuses to press Enter while the target session is in
81
+ a temporary working state.
82
+
83
+ This bypasses the safety gate that protects sessions still booting. Set it only
84
+ when you understand that trade-off. Use `--no-submit-force` on a specific
85
+ `telepty inject --submit` call to restore the gated behavior even when the
86
+ environment default is enabled.
87
+
71
88
  ## Cross-Machine Sessions
72
89
 
73
90
  telepty auto-discovers sessions across your Tailnet. All commands (`list`, `attach`, `inject`, `rename`, `multicast`, `broadcast`) work seamlessly across machines.