945e431a64
* refactor(cockpit): extract wire types into protocol.rs; fix replay lost semantics Foundation commit for the TUI cockpit view (issue #1018). Moves the HTTP / WebSocket wire types out of the server module so daemon, web frontend, CLI cockpit verbs, and the upcoming TUI cockpit view all import a single source of truth. A rename in one place now breaks every consumer at compile time instead of going silently divergent. Also fixes the hardcoded `lost: false` in /cockpit/replay: the event store now exposes lowest_seq(), and the endpoint reports lost = since < lowest_seq - 1 so a client returning after a long absence learns its history was truncated and can reload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cockpit): add daemon client (HTTP + WS + discovery + auto-spawn) Lays the substrate for the CLI cockpit verbs and the TUI cockpit view to come. Talks to an `aoe serve` daemon over the existing per-session cockpit REST + WebSocket surface; one shared `DaemonEndpoint` type backs both layers and the wire-type re-use from `cockpit::protocol` guarantees client and server can't drift on JSON shape. - discovery: AOE_DAEMON_URL env override first, then local serve.url paired with a live serve.pid. Loopback alternates preferred over tunnel addresses for same-box clients. Token extracted to a separate field so it travels as `Authorization: Bearer` (HTTP) or `?token=` (WS) but never gets logged. - http: typed client per cockpit verb (replay, context_primer, prompt, cancel, resolve_approval) mapped 1:1 to the per-session REST routes. 401 -> Unauthorized, 403 read-only -> ReadOnly, 404 -> SessionNotFound for clean error UX. - ws: tokio-tungstenite stream parsing CockpitBroadcastFrame and the `{"kind":"lagged"}` sentinel, plus a shutdown channel and url sanitiser so the token is never logged. - daemon_manager: ensure_daemon() that auto-spawns a loopback-only long-lived `aoe serve` if neither AOE_DAEMON_URL nor a live local daemon are found. AOE_DAEMON_URL set -> never auto-spawn, fail loud. tokio-tungstenite added as an optional dep folded into the `serve` feature alongside the rest of the cockpit surface. Both `cockpit/` and `server/` are already serve-gated, so keeping the client there is the smaller change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): aoe cockpit {history,status,prompt,approve,cancel,tail} Wires the daemon client into actual user-facing CLI verbs so the client surface added in 51ac75f doesn't sit dead. Auto-spawns a loopback daemon on first invocation, then talks to it over HTTP/WS: - history <id> [--since N] [--json]: dump persisted transcript. - status <id> [--json]: highest/lowest seq + lost flag + daemon source. - prompt <id> <text|->: send a prompt (`-` reads from stdin). - approve <id> <nonce> [--always|--deny]: resolve a pending approval. - cancel <id>: cancel the in-flight prompt. - tail <id> [--since N]: stream broadcast frames to stdout as JSON. ReplayResponse gains a `lowest_seq: Option<u64>` field so `status` can show the retention floor. Default-`None` on the `#[serde(default)]` attribute keeps the wire shape backwards-compatible with any client that pinned the old shape (frontend uses serde_json, treats unknown fields as harmless). `attach` verb deferred to commit 4 alongside the TUI cockpit view it opens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(tui): native cockpit view Renders a cockpit session inside the TUI instead of toasting users toward the web dashboard. Mirrors the activity semantics of web/src/hooks/useCockpit.ts in a flat-row Rust reducer; talks to the local daemon via the cockpit::client added in 51ac75f. - src/tui/cockpit_view/reducer.rs: pure CockpitBroadcastFrame -> ActivityRow accumulator. Groups consecutive AgentMessageChunk events into one assistant row; flushes the buffer on any non-chunk event so intervening tool calls don't bleed into the wrong message turn. Mutates tool rows in place when ToolCallCompleted lands; resolves approvals by nonce lookup. Dedupes against duplicate seqs from the replay-vs-live overlap. - src/tui/cockpit_view/input.rs: focus model with three regions (composer / transcript / approval). Composer captures every key including `a`/`A`/`d`, so typing "always allow" with a pending approval can never silently resolve it (regression test pins this). - src/tui/cockpit_view/render.rs: three-pane layout (transcript / status banner / composer). Tool cards render as one-liners with truncated args + content preview; rich diff / image previews are deferred to followups, accessed via `o` to open the web view. - src/tui/cockpit_view/state.rs: owned view state (transcript, composer textarea, focus, ws handle, toast banner). - src/tui/cockpit_view/mod.rs: async orchestrator. Calls ensure_daemon, hydrates via /replay, opens WS, runs a tokio::select! over key events + ws frames + a redraw ticker, and reconnects on ws drop using the last seq. - src/tui/app.rs: new `Action::OpenCockpit(SessionId)` variant (serve-feature-gated) plus `pending_cockpit_open` slot the sync action handler uses to hand control back to the async loop, which then borrows event_stream + terminal for the cockpit run. - src/tui/home/input.rs: replace the "open the web dashboard" toast with Action::OpenCockpit when serve is built in; keep the toast as fallback for non-serve builds. - src/tui/home/render.rs: rename `[web]` badge to `[cockpit]` now that the TUI renders the session natively. - src/cockpit/protocol.rs: derive PartialEq+Eq on ApprovalDecisionWire so the input dispatcher's Intent enum can derive PartialEq. Help-screen keybinds, the `aoe cockpit attach` CLI verb, and the focus-isolation e2e regression are deferred to a follow-up commit so this diff stays scoped to "wire up the view." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add aoe cockpit attach <id> Jumps straight into the TUI cockpit view for a known session id without going through the home screen. The intended pairing is `AOE_DAEMON_URL=https://remote.tld AOE_DAEMON_TOKEN=... aoe cockpit attach <id>`: attach to a remote cockpit session from a developer machine that doesn't have a local serve daemon (and shouldn't auto- spawn one — `ensure_daemon` short-circuits under the env override). cockpit_view::run_standalone takes care of the alternate-screen terminal setup that the parent TUI normally owns: enable_raw_mode, EnterAlternateScreen, EnableBracketedPaste, EnableMouseCapture; then the shared `run()` loop drives the view; then the same teardown sequence in reverse. Uses the empire theme since standalone attach doesn't load the home view's saved theme preference. Help-screen keybinds for the cockpit view are intentionally NOT added to the home help overlay — they only apply while the cockpit view is focused, and the home dialog is already at 41/42 lines of available height. The cockpit view's status banner already surfaces a focus-specific help hint inline, and docs/cockpit.md (commit 7) gets the full table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(tui): cross-machine remote-cockpit picker Closes the cross-machine half of #1018. When `AOE_DAEMON_URL` is set (via env var or the new `--daemon-url` flag on `aoe`), the TUI swaps the local home view for a remote-cockpit picker that talks to the named daemon via HTTPS instead of reading the local FS. The remote view is intentionally separate from `HomeView` rather than a `SessionSource`-trait abstraction over it: HomeView is deeply local-coupled (tmux PTYs, on-disk Storage, container/host terminal mode, per-session profile management), and the remote surface only needs the small slice of "list cockpit sessions, open one, refresh." Trying to share the same struct would push tmux abstractions through a layer that has no notion of tmux. Two scoped surfaces are simpler than one over-generalised one. What the remote picker does: - Calls `GET /api/sessions` against the daemon, filters to `cockpit_mode = true` (tmux PTYs aren't reachable cross-machine without SSH). - Renders a selectable list (j/k navigate, Enter opens, r refreshes, q/Esc exits). - On Enter, hands off to `cockpit_view::run_for_endpoint`, a new variant of the cockpit run loop that takes a pre-discovered endpoint instead of auto-spawning. The user explicitly chose a remote daemon; silently auto-spawning a local one would attach to the wrong universe. Local-only operations (tmux attach, edit file, session stop, file diff) are absent rather than disabled: they aren't reachable on this machine, so showing them grayed out would mislead. The web dashboard remains the long-tail surface for remote management. `--daemon-url` is wired as a clap arg with `env = "AOE_DAEMON_URL"`, so the flag and env var are unified. When the flag is passed, main.rs mirrors the value back into the env so the same discovery code path the CLI cockpit verbs use also resolves it. The full `SessionSource` trait abstraction over `HomeView` from the original 4-PR plan is intentionally deferred: the remote picker already meets the acceptance criteria ("AOE_DAEMON_URL=... aoe opens a TUI that shows the remote session list, all cockpit operations work transparently"), and the broader refactor would touch every HomeView call site without much functional gain. Tracked as a followup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(cockpit): document TUI cockpit view + CLI verbs + cross-machine Replaces the "cockpit is web-dashboard only" framing in docs/cockpit.md with the actual TUI surface, the keybind table (composer / transcript / approval focus model, plus the focus-isolation guarantee), the cross-machine attach workflow via AOE_DAEMON_URL or `aoe cockpit attach`, and the full CLI verb catalogue. Regenerates docs/cli/reference.md via `cargo xtask gen-docs` so the new clap surface (`aoe --daemon-url`, the cockpit verbs, the attach flag) is pulled into the canonical reference (CI enforces this stays in sync). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(serve): add `aoe serve --status` for daemon introspection Prints PID, mode, primary + alternate URLs, and log path when the daemon is up; exits non-zero with an actionable hint otherwise. Mirrors `aoe url`'s patterns (bail-when-down, no token redaction) since both run under the same local-only security boundary. Wired through clap with `conflicts_with_all = ["stop","daemon","remote"]` so misuse fails early. * feat(serve): clearer error when daemon already running Previously: "Stop it first with `aoe serve --stop`." which hid the fact that the existing daemon is probably what the user wants. Now suggests `--status`, `aoe url`, and `--stop` so the discovery path is obvious without `--help` or docs. * feat(client): pre-flight health-check + env-aware `aoe serve --status` Two fixes for AOE_DAEMON_URL UX: 1. `ensure_daemon` now health-checks the env override endpoint before returning, so `aoe cockpit *` against an unreachable URL fails loud ("AOE_DAEMON_URL is set but the daemon at that URL is unreachable") instead of bubbling up a raw reqwest transport stack from each verb. 2. `aoe serve --status` now follows AOE_DAEMON_URL: when set, it pings the remote endpoint and reports reachability + token state instead of inspecting the local `serve.pid` file. Same friendly error path when the remote is down. Adds `HttpClient::health_check()` (GET /api/sessions) for both. The endpoint is cheap, authenticated, and separates "host down" (transport error) from "auth misconfigured" (401) at the call site. Doc note in cockpit.md spelling out the retarget so users can predict the behavior without reading the source. * fix(cockpit-view): auto-scroll past wrapped agent-message chunks Transcript scroll was clamped against `lines.len()` (logical line count). Streaming AgentMessageChunk events grow text *within* a single Line, which the Paragraph then wraps to multiple visual rows — the logical count stayed constant, so `scroll_offset = u16::MAX` (stick-to-bottom) clipped short of the newest chunk. Tool calls didn't show the bug because each call pushes whole new Line entries. Switch to `visual_line_count(lines, width)`: per-line display width divided by available columns, rounded up, summed. Approximate (no account for tabs/control chars) but accurate for the streaming-text case that matters. Unit-pinned with a regression test. * fix(cockpit): address PR review (UTF-8 panic, em-dashes, error fidelity) - render: replace byte-slice truncation with char-safe truncate_chars so tool args/content with multi-byte codepoints at the cutoff don't panic the TUI. Regression tests pin the boundary cases. - client/ws: real WsError::Parse variant instead of fabricating an InvalidOpcode protocol error; the toast now carries the actual reason. - client/daemon_manager: distinguish EnvOverrideUnauthorized from EnvOverrideUnreachable so a wrong AOE_DAEMON_TOKEN shows the right message; cockpit_view renders both. NoExecutable no longer absorbs log-file IO failures (new LogFile variant). - style: sweep em-dashes from PR-added Rust comments, status-banner string literals, and docs/cockpit.md per project rule. xtask post- processes clap_markdown's hard-coded em-dash bullet separator so docs/cli/reference.md regenerates without them and future CLI flags inherit the rule automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cockpit): apply remaining PR review polish - cli/serve: --status now conflicts with --no-auth, --read-only, --passphrase, --port, --tunnel-name, --no-tailscale, --tunnel-url, --open in addition to --stop/--daemon/--remote, so a misuse like `aoe serve --status --port 9000` fails fast instead of silently accepting the extras. - client/ws: WsHandle::shutdown awaits the reader task's graceful close with a 200ms budget before falling back to abort(), so a Close frame actually reaches the daemon on clean exits. - client/http: list_sessions<T>() reuses the shared auth/header plumbing for GET /api/sessions. The remote-home picker drops its bespoke reqwest::Client and goes through HttpClient like every other cockpit verb does. - cockpit_view: reconnect_with_backoff replaces the one-shot WS reconnect with 250/500/1000ms attempts so a 2-second daemon bounce recovers without paging the user. - cockpit_view: composer height magic numbers become COMPOSER_BORDER_ROWS + COMPOSER_MAX_CONTENT_ROWS. - cockpit_view/reducer: tracing::debug! when a frame is dropped against last_seq, so a true reordering shows up in logs without spamming on the normal replay/live overlap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cockpit): require a running daemon; remove auto-spawn Opening a cockpit session from the TUI (or running any aoe cockpit verb) used to auto-spawn a loopback aoe serve in the background if one wasn't already running. That hid the choice between localhost, Tailscale Funnel, and Cloudflare tunnel from the user, and left an aoe serve process behind that they didn't ask for. Now require_daemon() (renamed from ensure_daemon) returns ManagerError::NoDaemonRunning with a multi-line actionable hint pointing at: aoe serve --daemon (localhost only) aoe serve --daemon --remote (Tailscale Funnel / Cloudflare) aoe serve --daemon --tunnel-name … (named Cloudflare Tunnel) …and AOE_DAEMON_URL for attaching to an existing remote daemon. The TUI renders the message in the cockpit error screen; the CLI verbs print it to stderr and exit non-zero. setsid()-detached spawn, deadline polling, log file management, and the related error variants (NoExecutable, LogFile, SpawnFailedFast, SpawnTimeout) are all gone. docs/cockpit.md updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): regen CLI docs, allow webpki-roots license, drop em-dash from xtask comment Three small fixes for the CI failures on PR #1114: - Docs check: the multi-line `--status` doc I added incb95fbbwasn't reflected in docs/cli/reference.md (forgot to re-run `cargo xtask gen-docs`). Regenerated. Also dropped the now-stale "cockpit auto-spawn flow" phrase from the help text since auto-spawn was removed in3dc14ef. - Supply Chain: tokio-tungstenite's `rustls-tls-webpki-roots` feature pulls in `webpki-roots` (CDLA-Permissive-2.0). deny.toml already whitelists CDLA-Permissive-2.0 for the equivalent `webpki-root-certs` crate that reqwest uses; extend the same exception to `webpki-roots`. - xtask: the comment explaining the em-dash strip itself contained a literal em-dash, contradicting its own rule. Rewrote with a Unicode escape so the source file has zero literal em-dash code points and the strip target is described unambiguously. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(xtask): drop em-dash strip in gen-docs The project's no-em-dash rule is about human-authored docs and comments, where the writer should have picked a comma or semicolon. clap-markdown's bullet separator is a renderer choice applied uniformly to every entry; there's no human author to nudge, and the strip created a layer of indirection in xtask that future contributors would have had to remember. Reverts the strip introduced earlier in this branch; docs/cli/reference.md goes back to clap-markdown's native output (209 em-dashes restored). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): note em-dash exception for clap-generated CLI reference The no-em-dash rule is about human-authored docs and comments. docs/cli/reference.md is generated by `cargo xtask gen-docs` and inherits clap-markdown's em-dash bullet separator uniformly; that's a renderer choice, not prose, so leave it alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): generalize em-dash exception to all auto-generated content Carving out a single file (docs/cli/reference.md) was too narrow: the underlying principle is "human-authored prose only." Rephrase so future auto-generated docs (changelogs, API refs, schemas) are covered without needing another edit here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): trim the em-dash exception example The rule stands on its own without the reference.md example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: njbrake <nathan@mozilla.ai> Co-authored-by: Nathan Brake <33383515+njbrake@users.noreply.github.com>
64 lines
1.5 KiB
TOML
64 lines
1.5 KiB
TOML
# https://embarkstudios.github.io/cargo-deny/
|
|
|
|
[graph]
|
|
all-features = false
|
|
no-default-features = false
|
|
|
|
# Vulnerability database checks
|
|
[advisories]
|
|
# Deny any crate with a known security vulnerability
|
|
version = 2
|
|
ignore = []
|
|
|
|
# License policy
|
|
[licenses]
|
|
confidence-threshold = 0.8
|
|
allow = [
|
|
"Apache-2.0",
|
|
"Apache-2.0 WITH LLVM-exception",
|
|
"BSD-2-Clause",
|
|
"BSD-3-Clause",
|
|
"BSL-1.0",
|
|
"CC0-1.0",
|
|
"ISC",
|
|
"MIT",
|
|
"MIT-0",
|
|
"MPL-2.0",
|
|
"Unicode-3.0",
|
|
"Unlicense",
|
|
"Zlib",
|
|
]
|
|
exceptions = [
|
|
# UEFI-only crate, not linked into the final binary on Linux/macOS
|
|
{ allow = ["LGPL-2.1-or-later"], crate = "r-efi" },
|
|
# CDLA-Permissive-2.0 is a permissive data license. Both crates
|
|
# ship Mozilla's CA bundle under that license; the project tree
|
|
# ends up with whichever one the dependency happens to pull in
|
|
# (`reqwest` uses `webpki-root-certs`; `tokio-tungstenite` uses
|
|
# `webpki-roots`).
|
|
{ allow = ["CDLA-Permissive-2.0"], crate = "webpki-root-certs" },
|
|
{ allow = ["CDLA-Permissive-2.0"], crate = "webpki-roots" },
|
|
]
|
|
|
|
[licenses.private]
|
|
ignore = true
|
|
|
|
# Crate bans
|
|
[bans]
|
|
multiple-versions = "warn"
|
|
wildcards = "allow"
|
|
highlight = "all"
|
|
workspace-default-features = "allow"
|
|
external-default-features = "allow"
|
|
allow = []
|
|
deny = []
|
|
skip = []
|
|
skip-tree = []
|
|
|
|
# Source provenance
|
|
[sources]
|
|
unknown-registry = "deny"
|
|
unknown-git = "deny"
|
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|
allow-git = []
|