Files
Zexin Yuan a8505c6667 feat(age): add hello capability negotiation to the age agent (#3634)
* feat(buildinfo): expose the gopass build version to internal packages

Internal packages cannot import the ldflags-injected version variable
from package main, and runtime/debug build info cannot serve as a
substitute: its Main.Version is "(devel)" in release builds and the main
module never appears in Deps, so debug.ModuleVersion only resolves
dependency versions reliably. main stashes the version in the exported
Version variable at startup; an empty value means unknown (test builds,
library embedders). ModuleVersion() wraps the lookup: the stash is
authoritative, the build-info module version is a fallback.

The first consumer (the age agent hello version token) follows in the
next commit.

Assisted-by: Claude-Code:GLM-5.3

Signed-off-by: Zexin Yuan <git@yzx9.xyz>

* feat(out): add Untrusted and Truncated display wrappers

Strings echoed to the terminal from untrusted origins - concretely the
age agent's hello payload, which comes from whatever process owns the
agent socket, a third party the protocol explicitly welcomes - would
reach fmt's %s verbatim: control characters are not filtered, so ESC/OSC
sequences could drive the terminal, and a hostile payload can be
arbitrarily large (the response line limit is 16 MiB).

Add two orthogonal fmt.Stringer wrappers to internal/out so every sink
(terminals, debug logs, anything else that formats) renders them safely
with zero plumbing in the print functions: Untrusted strips C0/C1
control characters, neutralizing escape sequences into inert text;
Truncated bounds the rendered length at a rune boundary, shaped after
io.LimitedReader and composable over any Stringer. Untrusted accepts a
bare string on purpose: stripping is only semantically safe on raw
bytes - a legitimate Stringer may intentionally emit ANSI - so the types
keep the two operations in their safe domains.

Assisted-by: Claude-Code:GLM-5.3

Signed-off-by: Zexin Yuan <git@yzx9.xyz>

* refactor(age): remove the dead Client.Remove

Client.Remove had a client-side implementation since the agent's
introduction, but the agent never implemented the remove command and no
caller ever invoked the method. Delete it rather than keep the illusion
of a supported command.

Assisted-by: Claude-Code:GLM-5.3

Signed-off-by: Zexin Yuan <git@yzx9.xyz>

* feat(age): add hello capability negotiation to the age agent

Implements a stateless, voluntary hello exchange: the agent advertises
its capability set, and pre-hello agents keep working unchanged.

- agent: dispatch via a handler table of bound methods returned by an
  Agent method; dispatch and the advertised capability list derive from
  the same table, so they cannot drift apart. Adds the hello command
  (command tokens + maxline + diagnostic version tokens). The session
  and ssh-identity commands dispatch through the same table and are
  advertised like every other command.
- client: Capabilities() performs the handshake; ANY error (connection
  failure or an ERR from a pre-hello agent) means legacy and callers keep
  the pre-hello behaviour - judged by the error's existence, never its
  text. Response reads are bounded by the agent's own line limit,
  duplicate tokens resolve first-wins, and Info() fetches status and
  capabilities over a single connection.
- gopass age agent status: extracted to a testable method, now reports
  the agent version and capabilities (or a legacy note), with
  agent-supplied strings stripped of terminal control characters before
  display.
- tryStartAgent: logs agent capabilities behind the existing
  debug.IsEnabled() gate so the hot path pays no round trip unless
  debugging.
- version sourcing: the hello version token and the client hello
  argument read internal/buildinfo.ModuleVersion.
- changelog: record the capability negotiation under Unreleased.

Invariants frozen by tests: unknown commands yield a single-line ERR,
change no state and do not close the connection.

Assisted-by: Claude-Code:GLM-5.3

Signed-off-by: Zexin Yuan <git@yzx9.xyz>

* docs(age): document the age agent protocol

Adds a complete specification of the agent's line protocol as its own
document (docs/backends/age-agent-protocol.md): transport and framing,
socket location rules per platform, per-command syntax, responses and
errors, the lock state model including auto-lock semantics, the
error-handling contract (judge by ERR presence, never by text), the
hello capability exchange with its legacy-detection rule, and an example
session. The token grammar and the client version argument may still
change. age.md keeps a short overview and links to the specification.

Assisted-by: Claude-Code:GLM-5.3

Signed-off-by: Zexin Yuan <git@yzx9.xyz>

---------

Signed-off-by: Zexin Yuan <git@yzx9.xyz>
2026-10-07 20:17:55 +02:00
..
2025-11-12 21:09:26 +01:00
2025-10-05 20:16:33 +02:00