Files
NetMesh/docs/plugin-platform/terminal-providers.md
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

290 lines
17 KiB
Markdown

# Terminal Provider API
PR 5 adds the host-owned terminal Provider registry on top of the isolated
runtime and permission boundary. Provider declarations remain immutable
manifest data. Listing Providers never starts a plugin; first invocation uses
the existing idempotent `onProvider:<id>` activation seam and revalidates the
active plugin version and runtime identity after the response.
## Runtime registration
An activated plugin registers only contributions owned by its exact plugin ID:
```ts
context.subscriptions.add(context.providers.register(
"com.example.shell.completion",
"terminal.completion",
async ({ payload, cancellationToken }) => {
if (cancellationToken.isCancellationRequested) return { items: [] };
return { items: [{ text: "git status", displayText: "git status", score: 100 }] };
},
));
```
Registration is activation-owned and disposable. A stale disposable cannot
remove a replacement registration. Invocation carries the declared Provider
ID/kind, an operation, a host-generated request ID, a bounded JSON payload, the
deadline, and a cooperative cancellation token. Results use the canonical
`ok`/`cancelled`/`failed` Provider result union and are validated again by the
main process before renderer use.
Each invocation reauthorizes the Provider kind's least-privilege permission
set against the current runtime identity before sending a session snapshot or
request payload. Required grants are reused; optional declarations prompt at
first use and denial/cancellation returns no terminal data to the runtime.
## Terminal snapshots and lifecycle
Providers receive immutable metadata snapshots containing only stable session
identity and presentation context: session/host/workspace IDs, protocol,
connection status, cwd, title, shell type, dimensions, and alternate-screen
state. Active runtimes can subscribe with `context.terminals.onDidChange()`.
Protocol values preserve the actual built-in transport (`ssh`, `mosh`, `et`,
`telnet`, `local`, or `serial`) and accept bounded namespaced identifiers for
future connection Providers instead of collapsing non-SSH transports to SSH.
Immediately before an invocation, a lazily activated Provider receives a
`snapshot` event for the current session so it does not depend on lifecycle
events that occurred before activation.
Lifecycle events cover creation, connection/reconnection, cwd/title/resize/
alternate-screen changes, command submission, host-detected command completion,
disconnect, and disposal. Completion events contain no command text or raw
output and are emitted from OSC 133 completion markers when available, with a
conservative next-prompt fallback for shells without integration markers.
Connection-scoped cwd, title, and alternate-screen metadata is cleared before
disconnect and reconnect publication; viewport dimensions remain available.
Ongoing lifecycle delivery begins only after a successful invocation with a
non-`once` `provider.terminal` grant. Each event rechecks that grant without
opening a new prompt and remains bound to the exact plugin version, runtime ID,
runtime kind, and security principal that received the authorized invocation.
One-use grants receive only the invocation snapshot and payload.
PR 5 intentionally omits command text, password/prompt content, raw terminal
output, xterm objects, backend handles, and terminal-worker ports. The ordinary
JSON-RPC Provider path is not suitable for hot interception. PR 6 owns the
separate permission-gated MessagePort fast path for input/output interceptors,
sensitive-input bypass, circuit breaking, and the 4 ms interceptor budget.
## Privileged terminal data pipeline
PR 6 implements the two declared raw kinds without exposing xterm, Electron
IPC, backend streams, or the general plugin control plane. Only an advanced
utility runtime with `provider.terminal` and the matching
`terminal.intercept.input` or `terminal.intercept.output` grant can be attached.
Authorization is bound to the exact plugin version, runtime ID, runtime kind,
security principal, terminal session, direction, and declared Provider.
Because the transferred port is a long-lived capability, both permissions must
resolve to a session, application, or persistent grant; a one-use grant is
rejected before either port endpoint is published.
Browser runtimes are rejected before a port is transferred. Publisher
signature eligibility remains a distribution-policy decision owned by PR 9;
the advanced runtime and explicit high-risk permission boundary is already
enforced here.
An activated utility plugin uses the same registration owner and receives a
specialized SDK invocation:
```ts
context.subscriptions.add(context.providers.register(
"com.example.filter.input",
"terminal.interceptor.input",
async ({ data, session, sequence }) => {
// The transferred UTF-8 Uint8Array is owned by this invocation.
return data;
},
));
```
For each terminal session, Netcatty permits at most one arbitrary interceptor
per direction. A single candidate can be selected automatically; competing
candidates require an explicit host-owned user choice and "No interceptor" is
the default/cancel action. The choice is session-local and is discarded on
session disposal, contribution withdrawal, runtime replacement, crash, or
quarantine. The requesting renderer must own the terminal session before any
authorization or activation work occurs.
The main process transfers the two ends of one `MessageChannelMain` directly
to the terminal worker and selected plugin utility process. The utility-side
attachment is established by a transfer-aware `PluginRpcRouter` request, so
the existing router owns correlation, deadline, cancellation, validation,
late-response retirement, close cleanup, and protocol-failure containment.
Only the accepted long-lived byte path leaves the control plane. Data messages
contain a monotonic sequence, direction, bounded credit information, and one
transferable `ArrayBuffer`; the main process never copies terminal payloads.
Ready, chunk, successful-result, and failed-result metadata use the canonical
`TerminalInterceptorFrame` union. Both worker and utility peers validate it
from the generated contract bundle, and the shared MessagePort envelope rejects
missing, unexpected, detached, oversized, or byte-length-mismatched transfers.
The worker serializes chunks, caps each transfer at 64 KiB, and limits queued
output to a 256 KiB credit window. Output remains ordered and host output taps
retain the original data. Renderer flow acknowledgements use the original
ingress count even when a plugin expands, contracts, or completely suppresses
visible output. Host-bypassed sensitive input and protocol replies still wait
behind earlier ordinary input so bypass cannot reorder the terminal stream.
Input requests have a 4 ms worker-owned deadline. Output requests have a
bounded 50 ms deadline and a 256 KiB queued-output window. A timeout, malformed
response, invalid UTF-8 result, closed port, runtime exit, or credit-window
overflow trips the circuit breaker immediately: the original chunk fails open,
the interceptor is disabled for that session/direction, and Netcatty displays
a host-owned warning. An interceptor cannot suppress that warning or re-enable
itself without a fresh host authorization path.
These budgets are containment limits, not production performance acceptance
evidence. PR 9 owns the reproducible benchmark harness, supported hardware and
operating-system matrix, and release gate proving no more than 1% no-plugin
throughput regression plus approximately 4 ms p95 / 8 ms p99 added input
latency before the development gate can be removed.
Credential protection is outside plugin control. Input that the host marks as
sensitive/no-echo bypasses the port before buffer creation, including every
character entered while the password-prompt state is active and confirmed
sudo/su credential autofill. Recorded automation credentials use a password
dialog, remain redacted from script activity/logs, and carry the same sensitive
marker through the script bridge. The terminal worker also recognizes authentication
challenges from bounded original-output tails before output interception, so an
output plugin cannot expose a password by hiding or rewriting its prompt.
Generic PTY protocols do not expose an authoritative live echo-mode signal.
Consequently, a custom or promptless program that disables echo may not be
recognized by the host classifier. The native permission dialog states this
limit before granting input interception, and public enablement remains blocked
until PR 9 restricts the capability to explicitly approved signed advanced
plugins. This is a deliberate limitation of the first terminal data path, not
an absolute no-echo confidentiality guarantee.
Sensitive input is also excluded from terminal broadcast. Terminal protocol replies, urgent interrupts, transfer input gates,
transport encoding, Telnet IAC escaping, host logs, renderer flow accounting,
and marker/safety parsing remain host-owned. Output interceptors may create or
suppress visible byte sequences that affect output-derived lifecycle signals
such as OSC 133. Netcatty owns the parser, marker objects, validation, and
cleanup, but deliberately derives those signals from the transformed visible
stream; credential-prompt classification remains based on bounded original
host output before interception. With no active interceptor, the
worker uses the existing synchronous output path and performs no interceptor
Promise, transfer, or payload allocation.
## Host adapters
Netcatty's built-in autocomplete engine and keyword highlighter use the same
application Provider adapters as plugins:
- completion requests run built-in and plugin Providers concurrently;
- one active request exists per session and Provider kind; a newer request
cancels and suppresses the older result;
- Provider ordering is deterministic and can honor a host-owned preference
list; completion items are score-ranked and text-deduplicated;
- one Provider failure is contained and does not suppress other Providers;
- plugin completion responses are capped and normalized before rendering;
- completion insertion/display text rejects control and bidirectional override
characters before it can reach terminal input or suggestion UI. The host
always renders the exact insertion text for third-party completions, so a
friendly label cannot conceal a different command on previewless terminals;
- decoration Providers return declarative rules only. Rule IDs are namespaced,
counts and strings are bounded, colors must be explicit hex values, and
unsupported expressions are rejected before reaching the highlighter, and
accepted plugin patterns are compiled and executed by the linear-time RE2JS
engine with global, case-insensitive matching;
- decoration results are capped again after Provider fan-out at 16 active
rules and 32 total patterns. Plugin matching examines at most the first 4096
characters of each incoming text segment and retains at most 256 plugin
matches per terminal write. Highlight colors are applied to already-parsed
cells; ordinary input and output only rematch dirty rows. When rules change,
the host restores original cell colors and recolors the visible viewport
immediately, then finishes scrollback in idle slices. Heavy output may skip
matching until one quiet-window catch-up. Patterns that can match an empty
string are rejected because they cannot produce a visible highlight. Normal
boot and hibernate wake share the same CWD-triggered decoration refresh path;
- link and hover Providers receive one bounded physical xterm line and return
exact zero-based ranges. Links are restricted to credential-free HTTP(S)
URLs, reuse the host link-modifier policy, and render hover text with host
DOM nodes rather than plugin HTML. UTF-16 result boundaries are mapped back
to xterm cells so wide and combining characters cannot shift activation or
decoration ranges. Requests pause while the terminal is hidden or
disconnected, and in-flight results are aborted and invalidated on either
transition;
- matcher Providers receive at most the latest 32 parsed logical normal-buffer
lines in one batch. Wrapped physical rows are joined before invocation and
exact logical ranges are split back across host-owned xterm decorations.
Each result identifies a host-provided `lineId`; ranges are validated against
that exact line, the combined request text is capped below the 128 KiB
Provider envelope, and at most 64 logical matches remain visible.
Alternate-screen output is excluded;
- semantic Providers receive only a bounded command submitted from a
positively confirmed shell prompt (or an explicitly identified network
device prompt) and require `terminal.input`. Authentication challenges,
REPL input, and other untrusted prompt-shaped input never reach ordinary
Providers. Prompt Providers receive no command or raw output. Their
bounded annotations are rendered at host-detected command completion. A
prompt line is included only when the shared host detector confirms an empty
shell prompt, so the last output line is never mislabeled as prompt context;
- background Providers return at most four solid-color presentation layers.
Per-layer opacity and the combined host overlay are capped at 0.35, plugin
HTML/CSS/images are never accepted, and the request includes the current
terminal background color for contrast-aware results. An omitted layer
opacity uses the host-owned safe default of 0.15. Providers may request
a 250-60000 ms host refresh cadence; refresh pauses while the terminal is
hidden or disconnected and is disabled when reduced motion is requested;
- theme Providers receive the complete current host palette and may return a
bounded partial palette of explicit colors. Providers are merged in the same
deterministic preference order as enumeration, with the first value for each
color winning; host colors remain authoritative for omitted values;
- every ordinary visual adapter applies a renderer-owned end-to-end wait bound
around lazy activation, authorization, and runtime work. Stale generations,
disconnects, contribution changes, runtime replacement, and terminal
disposal cannot reapply old visual results. Provider availability is cached
from immutable enumeration without activation, stale enumeration generations
cannot overwrite newer contribution state, and enumeration errors fail
closed. Autocomplete, decoration, link, hover, matcher, and background paths
therefore perform no plugin RPC work when those contribution kinds are
absent or the development-gated host is disabled.
The operation payload/result shapes for the ordinary adapters are intentionally
declarative. Every payload also contains the immutable `session` snapshot for
the exact invocation:
- `terminal.completion/provideCompletions`: bounded input, cursor, host OS,
CWD source, and result limit -> bounded completion items;
- `terminal.decoration/provideDecorations`: a host refresh reason -> bounded
declarative highlight rules;
- `terminal.link/provideLinks`: `{ line, bufferLineNumber }` ->
`{ links: [{ start, length, uri, label? }] }`;
- `terminal.hover/provideHovers`: `{ line, bufferLineNumber }` ->
`{ hovers: [{ start, length, contents }] }`;
- `terminal.matcher/provideMatches`: `{ lines: [{ lineId, line,
bufferLineNumber }] }` -> `{ matches: [{ lineId, start, length, label,
severity?, color? }] }`;
- `terminal.semantic/provideSemantics`: `{ command }` -> classification,
destructive/idempotent flags, and bounded annotations;
- `terminal.prompt/provideAnnotations`: a host reason -> bounded annotations;
- `terminal.background/provideBackgrounds`: a host reason and optional current
terminal background -> bounded solid-color layers plus optional
`refreshAfterMs`.
- `terminal.theme/provideTheme`: a host reason and complete current host palette
-> a validated partial terminal palette.
The SDK exports and infers the matching payload, item, operation, and result
interfaces for all nine ordinary Provider kinds, including the immutable
host session snapshot attached to every invocation. The generic registration
overload remains available for later Provider kinds, so plugins do not need
application-internal renderer types and PRs 6-9 can add their own typed maps.
The control-plane JSON budget remains 1 MiB, while each terminal Provider
payload and result is additionally limited to 128 KiB. Default terminal
Provider requests have a 1.5 second deadline; autocomplete uses a shorter 750
ms runtime deadline plus an 800 ms renderer-owned end-to-end wait bound that
also covers lazy activation and first-use authorization. Built-in suggestions
therefore remain available when a plugin prompt is unanswered. Renderer
request cancellation is owned by the requesting
WebContents and all outstanding work is aborted when that sender is destroyed.
A single renderer may retain at most 64 active terminal requests, and one
fan-out invokes at most the first 32 deterministically ranked Providers.
## Downstream compatibility
The registry uses the existing generic Provider request/result envelopes,
runtime identity, cancellation, progress, permission names, and stream
protocol. PR 6 added its direct interceptor transport without changing the
ordinary registry. PR 7 reused that registration and runtime lifecycle for
connection, authentication, and importer Providers, with operation-specific
result validators and bounded stream consumers. PR 8 sync Providers (implemented)
and PR 9
rollout can reuse the same boundaries.