Files
alkssh/tasks/implementation/w3-decline-tracing.md
T
glm-5.3-flash 58d0b86ec5 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.
2026-10-28 17:50:49 +00:00

11 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
w3-decline-tracing Wave 3 — decline-path tracing (the wave-2 gate's booked finding) completed
w3-seam-config
narrow low component implementation

Description

Land the wave-2 review gate's booked finding: observability on the decline paths. tracing rides the manifest per the ADR-002 toolkit but so far nothing in src/ emits — a request-level decline's "reason" is today only the routing decision documented per build. The real serving paths land in this wave's mapping tasks, where serve/decline tracing matters — so the event vocabulary lands first and the mappings inherit it.

  • The family-convention check first — before choosing the event shape, read the family's established tracing posture (alktty / alkgit / alktunnels / alkcall src/: levels, event/field naming, what gets emitted on error and decline paths) and adopt it — no alkssh-private dialect; record what was read and chosen.
  • The event vocabulary — the deciding context on every decline: row + feature build + channel id (+ the decline class — open-level vs request-level vs serving-path, and where applicable the failure shape carried). The mapping tasks then emit at their sites.
  • Instrument what exists now — the open-level declines (unfeatured / never-feature / cap-exceeded), the request-level capability declines (subsystem/env/x11/agent, the unfeatured-parse routes), the seam-stub serving declines, and the serve-path skeleton (spawned path started / ended, failure) — the stubs' events carry into the mapping tasks' replaced paths.
  • Safety posture (conventions 6, OQ-03): no secret material ever enters an event, never the username; identity/fingerprint exposure on decline events is decided here against the family posture and recorded (auth.md: log exposure is deployment policy — the crate's events must be safe-by-default at every level).

Small, cross-cutting unit — one pass over the landed handler/dispatch sites with the vocabulary, so the three mapping branches never land inconsistent events.

Acceptance Criteria

  • 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 mapping tasks' spawned paths inherit shaped rows
  • 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 doc-test convention the family uses — decide + record)
  • cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt --check, cargo doc --no-deps pass

References

  • docs/plans/implementation.md §Wave 2 (the booked finding, review-wave-2 §Notes)
  • docs/architecture/mapping.md §Wire decline reasons (the two decline classes), §Decline matrix
  • docs/architecture/auth.md §Dispositions (log exposure posture), docs/architecture/decisions/002-russh-pin.md (the toolkit)
  • sibling src/ tracing usage — the convention read this task adopts

Notes

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 ✓.