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
290 lines
17 KiB
Markdown
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.
|