feat(observe): land the decline-path event vocabulary (w3-decline-tracing)

The wave-2 gate's booked finding: observability on the decline paths.
`tracing` rides the manifest per the ADR-002 toolkit but nothing in
src/ emitted — a decline's "reason" was only the routing decision
documented per build.

- src/observe.rs: the event vocabulary, adopting the family posture
  read from alktty/alktunnels/alkcall/alkhttp (free tracing fns,
  message-last lowercase, flat fields; no info!/trace!, no macros,
  no spans): feature_build + row + channel_id + decline_class
  (open/request/serving/auth) + reason + exec_route (exec rows);
  declines at debug, the serving-path failure arm the vocabulary's
  one warn
- handler.rs: every decline site emits — auth dispositions
  (non-ed25519 / fingerprint-unresolved / the four unimplemented
  methods), open-level declines (unfeatured / never-feature /
  cap-exceeded / tunnel connect-failed stub), request-level
  capability declines (exec route-aware, subsystem, env, x11, agent,
  the -R listen forms), and the seam-stub serving declines with the
  spawned-path lifecycle (started / ended / failed — the shape the
  wave-3 mappings inherit)
- tests/observe.rs: the event-presence pin — a hand-rolled global
  capture subscriber (the recorded test convention; no event-capture
  precedent exists in the family), field-matching rows across the
  default/git/session/tunnel/--all-features builds, and the safety
  scan (no username, no fingerprint ever in an event)
- helper-level exact shapes pinned in src/observe.rs unit rows

Verification: cargo test (default, --features git/session/tunnel,
--all-features all green), clippy x2 --all-targets -D warnings,
fmt --check, cargo doc --no-deps 0 warnings, taskgraph validate.
This commit is contained in:
glm-5.3-flash committed 2026-10-28 17:50:49 +00:00
1 parent 270365ea4c
commit 58d0b86ec5
6 files changed
+1490 -29

No files matched your search

+155 -7
View File
@@ -1,7 +1,7 @@
---
id: w3-decline-tracing
name: Wave 3 — decline-path tracing (the wave-2 gate's booked finding)
status: pending
status: completed
depends_on: [w3-seam-config]
scope: narrow
risk: low
@@ -46,16 +46,16 @@ inconsistent events.
## Acceptance Criteria
- [ ] The family tracing posture read and recorded; the event
- [x] The family tracing posture read and recorded; the event
vocabulary established (row + build + channel + decline class)
and used at every decline site in `src/`
- [ ] Serve-path skeleton events in place (start/ended/failure) so the
- [x] Serve-path skeleton events in place (start/ended/failure) so the
mapping tasks' spawned paths inherit shaped rows
- [ ] The safety posture recorded (no secrets, no username; what
- [x] The safety posture recorded (no secrets, no username; what
identity detail may appear decided)
- [ ] A test pins the event presence at the decline sites (or the
- [x] A test pins the event presence at the decline sites (or the
doc-test convention the family uses — decide + record)
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check`, `cargo doc --no-deps` pass
## References
@@ -70,4 +70,152 @@ inconsistent events.
## Notes
> To be filled at implementation time.
### The family posture read (2026-10-28)
Read before choosing the event shape (the task's first bullet), across
the family's `src/`:
- **alktty** (`adapter.rs`, `session.rs`, `channels.rs`,
`local/pipe.rs`, `local/pty.rs`): free `tracing::{debug, warn}`
imports; message-last lowercase sentences with a component prefix
(`"tty: backend stdin write failed: {e}"`); interpolation in
messages, no spans.
- **alktunnels** (`producer.rs`, `consumer.rs`, `params.rs`): the same
free-fn posture, plus structured fields where identity exists
(`listen = …`, `error = %e`, `reason = …`); levels `debug` for
structural observations, `warn` for recoverable failures, `error`
for abnormal pump terminations.
- **alkcall** (`protocol/*`, `channels/*`): the widest use — the same
fields convention (`channel_id`, `request_id`, `error = %e`),
component-prefixed messages (`"channel client: …"`, `"open wrapper:
…"`), `debug`/`warn` only.
- **alkhttp** (the closest structural analog): `warn`/`error` with
`error = %e` (`"channels session ended"`).
- **alkgit**: no tracing at all at this writing.
Never observed: `info!`/`trace!` events, subscriber setup in library
code, macro or span wrappers. Adopted verbatim in `src/observe.rs`:
free `tracing` fn calls, message-last lowercase with the
`"decline: …"` / `"serving path …"` phrasing, flat per-event fields.
### The event vocabulary (landed in `src/observe.rs`)
- **`feature_build`** — the compile-time feature set as a stable
string (`"none"`, `"git"`, `"session+git"`, …; features join in the
order `session, git, tunnel, sftp`), so a captured event is
self-describing per build; `sftp` ships in the string even though it
gates no route (it is part of what the assembly deployed).
- **`row`** — the greppable wire row: the channel type for open-level
(`"session"`, `"direct-tcpip"`, `"x11"`, …), the request name for
request-level (`"pty_request"`, `"tcpip_forward"`, …), the handler
hook name for auth (`"auth_publickey"`, `"auth_none"`, …).
- **`decline_class`** — `"open"` | `"request"` | `"serving"` |
`"auth"`: the two wire decline classes (`mapping.md` §Wire decline
reasons), plus the serving-path class (this wave the seam stub's
decline *is* the serving path; the mappings emit the same class from
their real spawned paths) and the auth class (the ADR-004
disposition declines — added by this task: the auth hooks are
decline sites too, and `auth.md`'s dispositions are the crate's
auth-decline contract).
- **`reason`** — the decline cause, mirroring the architecture's own
classifications: `"unfeatured"`, `"never-feature"` (ADR-003 §5),
`"cap-exceeded"`, `"connect-failed"` (tunnel stub / ADR-006
decision 1), `"seam-stub"` (session-row stubs), `"unimplemented"`
(subsystem rows — OQ-13 — and the wave-3 listen forms),
`"declined-by-design"` (env — OQ-09), and on the auth class
`"method-unimplemented"` / `"cert-unsupported"` / `"non-ed25519"` /
`"fingerprint-unresolved"`.
- **`channel_id`** — the wire channel id (u32), on every decline of a
channel (notably: the event's id is *russh's server-side* id —
`ChannelId` is not publicly constructible, so the helpers take the
raw u32 via `ChannelId::number()`); the auth class and the global
`-R` listen forms carry none (a separate `decline_global_request`
helper — global requests have no channel).
- **`exec_route`** — only on `exec_request`-row events: the parse
route (`"git"` | `"pipe"`), so a git row's decline is
distinguishable from a pipe row's in combined builds. A `None` route
field is not recorded at all by `tracing` (discovered by the unit
rows; recorded in the module docs). The `message` field rides first
(the `event!` macro's shape — the unit rows pin the field order).
Levels: every decline and the lifecycle's started/ended emit at
`debug` — a decline is the door's normal, honest posture (vanilla
clients probe unfeatured rows routinely), matching the family's
structural-observation level. Only [`serving_path_failed`] is `warn`
(a serving path that could not complete its wire reporting is a real
failure — the family's recoverable-failure level).
### The serving-path skeleton
`src/serve.rs` stays the mechanics only (spawn + exit-report); the
lifecycle events hang off the spawned task's body in
`handler.rs`'s `seam_stub_decline`: started (the spawned task's first
action, so "started" is the task's honest start, not the hook's spawn
call) → the serving-path decline (class `serving`, reason `seam-stub`)
→ ended — or failed (`warn`, with the russh protocol error) when a
report send could not complete. The wave-3 mapping branches
(`w3-git-mapping`, `w3-tty-session`, `w3-tunnel-direct`) inherit the
shaped rows by emitting the same helpers from their real serving tasks.
### The safety posture (conventions 6, OQ-03 — recorded)
**No secret material ever enters an event; never the username;
the fingerprint stays out too.** Structurally enforced, not recalled:
the vocabulary helpers take only `&'static str` values and the wire
channel id — no parameter a username, command string, env name/value,
repo path, key, or fingerprint could ride. The `reason` codes carry
the decline's *cause class*, and the deciding context is the row +
build + channel — deliberately *not* the identity behind the wire
attempt. This is the recording of auth.md §Dispositions' standing
rule at the crate level: log exposure is deployment policy, so the
crate's events are safe-by-default at every level — the fingerprint
(and any identity detail) is a deployer-side computation over its own
logs, never crate-emitted. If a future decline site needs
request-argument detail (e.g. the git grammar's decline reasons in
`w3-git-mapping`), it must add an explicitly-vocabulary'd field and
record its safety reasoning — dynamic strings do not ride
`src/observe.rs` by default. The wire rows pin the posture by
scanning every captured event: no username string, no `SHA256:`
fingerprint, no `user*`-named field ever appears.
### The test convention (decided + recorded)
The family has no event-capture precedent to adopt — alktunnels'
`tracing-subscriber` dev-dep drives fmt output, not assertions. The
convention these rows pin: **a hand-rolled minimal
`tracing::Subscriber`** capturing events into a shared buffer,
installed once as the global default (`tests/observe.rs`) — the
server side runs on spawned tasks, so a thread-local
`with_default` capture cannot see them, and a global capture needs no
new dependency. Two levels of pinning:
- Helper-level (`src/observe.rs` unit rows, thread-local
`with_default`): exact field sets and order (message first), the
`exec_route` presence semantics, the no-channel auth shape, the
lifecycle messages, the `warn` failure arm, and the
cfg-derived `feature_build` string.
- Wire-level (`tests/observe.rs`, the loopback harness drives the
real wire; capture installed up front via the mount helpers — a
lazily-installed global loses the earliest emissions): presence by
field-matching at every decline site across the builds — x11 open,
`-R` listen (global), the fingerprint-unresolved and auth_none
dispositions (driven on fresh mounts — a second auth exchange on an
already-authenticated session is not a supported russh surface, as
the probe discovered), the unfeatured session/pipe/pty rows, the
git seam-stub's full lifecycle, the tunnel stub's `connect-failed`,
the cap-exceeded row (zero-cap limits must ride the *server* config
— `serve_config`, not just the client's `Limits`), and the
username/fingerprint safety scan. Build-specific rows follow the
`dispatch_core.rs` per-build cfg convention and were verified in
the default, `git`, `session`, `tunnel`, and `--all-features`
builds.
### Verification (all green)
`cargo test` default build 110 rows across 7 binaries + doctests ·
`cargo test --all-features` ✓ · `cargo test --features git/session/tunnel`
✓ (each build runs its decline rows) ·
`cargo clippy --all-targets -- -D warnings` ✓ ·
`cargo clippy --all-features --all-targets -- -D warnings` ✓ ·
`cargo fmt --check` ✓ · `cargo doc --no-deps` 0 warnings ·
`taskgraph validate` ✓.