[Init] Initial commit - NetMesh terminal manager
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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
# Plugin runtime extension boundaries
Status: phase 3 internal architecture review
This document records the host-runtime decisions that later plugin-platform
phases are allowed to depend on. The goal is to keep permission, contribution,
terminal, connection, synchronization, and distribution work out of the
runtime lifecycle core while still giving those phases stable internal seams.
These are host-internal APIs, not the public plugin API. The public contract
remains `0.1.0-internal` until the phase-9 API 1.0 freeze.
## Runtime identity is the authority root
Every activation receives a new host-generated runtime ID. The identity used by
host handlers contains the plugin ID, active version, runtime kind, package
root, manifest, logger, and host-resolved security principal. It is captured when the runtime starts and cannot be
supplied or replaced by plugin messages.
The RPC registry adds this identity to every request, notification, middleware
call, and incoming stream. A later permission decision can therefore bind a
grant to all of the following without trusting payload fields:
- plugin ID and version;
- one activation (`runtimeId`) for once/session grants;
- browser or advanced utility placement;
- declared manifest permissions and resources;
- the unsigned or later verified publisher security principal;
- the request cancellation and deadline context.
Host-to-plugin calls also verify that the recorded activation still matches the
database's enabled active version. An update cannot accidentally deliver a
command or Provider request to the old version after the active-version pointer
has moved.
Placement resolution and activation repeat that version check after every
asynchronous policy or startup boundary. A late crash or startup failure from
an old version is emitted for cleanup and diagnostics but cannot increment the
replacement version's crash counter, quarantine it, or overwrite its runtime
state. If the old immutable version is still installed, the event updates that
version's own runtime and crash state so a later rollback cannot mistake it for
a clean or still-running release.
## One capability registry, two message classes
`PluginHostRpcRegistry` is the composition point for plugin-to-host authority.
It deliberately distinguishes request handlers from notification handlers.
Storage mutations cannot be invoked as fire-and-forget notifications, and the
logging notification cannot be converted into a request with a meaningful
result. Reserved lifecycle and transport methods cannot be registered as
capabilities.
Registrations have unique method ownership and may carry immutable metadata.
Each registration may also provide a synchronous, side-effect-free parameter
validator. It runs before middleware, so resource extraction, permission
decisions, quotas, and audit records always consume the method's normalized
parameter shape instead of attacker-controlled raw input. Middleware then runs
immediately before the final handler with the host identity, method, validated
parameters, metadata, cancellation signal, request ID, and deadline.
Phase 3 installs permission, quota, audit, and fail-closed UI mediation here,
at the final privileged boundary rather than in renderer components.
Handler metadata is recursively copied and frozen when registered. Runtime
identity is checked before middleware, again after any asynchronous middleware
(such as an approval prompt), and once more before a result leaves the route.
An old activation therefore cannot resume a privileged handler after an update,
disable, quarantine, or stop transition.
Asynchronous handlers also receive `context.assertActive()`. A handler that
prepares I/O and then commits a mutation must call it immediately before the
commit and honor `context.signal` while waiting. The guard checks both the
host-owned activation identity and the request cancellation signal, so a
timed-out approval or Provider operation cannot commit merely because the same
plugin version remains active. Cancellation cannot undo a side effect already
issued to an external service, so this commit guard is part of the
capability-handler contract.
A running activation uses a route snapshot. Registering a new host subsystem
does not mutate a live plugin's authority invisibly; it applies on the next
activation. This is important when a new Netcatty build adds a capability or a
grant changes the available surface.
Capability middleware is not the transport quota boundary: reserved progress,
cancellation, lifecycle, and stream frames do not enter a business-method
handler. The supervisor therefore binds an optional synchronous raw-message
guard to the same host-generated runtime identity. It runs before JSON budget
walking and protocol dispatch for every message class. Phase 3 uses this seam
for per-activation rate and resource accounting, while capability middleware
continues to own permission and operation-specific policy. The guard must be
synchronous so an untrusted message cannot accumulate an unbounded queue of
pending quota decisions.
## Bidirectional invocation and validation
`RuntimeSupervisor.request()`, `notify()`, and `openStream()` are the only
general host-to-plugin entrypoints. All three can bind work to the exact
authorized runtime identity; streams repeat that check before opening and
again after the runtime returns the handle. Browser and utility runtimes
implement the same methods over their private router. Later registries do not
reach into a runtime window, utility process, MessagePort, or router.
Router requests may carry a reviewed structured-clone transfer list. This is
used to establish privileged terminal interceptor ports while retaining one
owner for correlation, deadlines, cancellation, validation, late responses,
close cleanup, and protocol-failure containment. After the attachment request
is accepted, terminal bytes travel only on the transferred dedicated port and
do not enter the JSON-RPC control plane.
Outgoing requests accept a method-specific result validator. Command and
Provider adapters must validate their exact public result schema before using
plugin data. The generic JSON boundary remains the first structural limit, not
a substitute for operation-level validation. Connection Providers additionally
validate the operation-specific result map: `resize`, `signal`, `reconnect`,
and `close` must return JSON `null`; `getStatus` must return a bounded
`ConnectionStatusResult` object and may attach structured diagnostics. Status
diagnostics are propagated through the host-owned terminal finish route so
later disconnect, reconnect, or authentication failures remain visible after
the initial `open` response.
Control-plane JSON is limited to 1 MiB. Large command results, importer data,
sync objects, terminal snapshots, and connection traffic must use the bounded
stream transport rather than raising this limit. A cancelled request ID remains
temporarily retired until one possible late response is discarded, so a slow
provider cannot accidentally answer a newer request after ID wraparound.
Lifecycle methods and `$/` transport methods are excluded from the general
entrypoints. Only the supervisor may initialize, activate, deactivate, cancel,
or account for a runtime.
## Stream ownership and the terminal fast path
Incoming stream handlers are registered centrally and receive a bind function
for the matched stream, an abort signal, and the same runtime identity. The
first handler that recognizes a pre-authorized stream ID owns it; unknown
streams are cancelled. Owner selection has the same bounded deadline as RPC,
so a stalled registry cannot retain an unowned stream indefinitely. Frames are
ordered per stream ID rather than through one global queue: a slow consumer
backpressures its own stream without blocking unrelated connection, importer,
or synchronization streams. Once a handler binds ownership, every local reject,
deadline, transport failure, peer close, or host shutdown reaches its `onClose`
cleanup boundary exactly once. The same abort signal remains live for the whole
owned stream and is aborted before cleanup, so long-running Provider work can
stop promptly instead of polling runtime state.
Handlers registered after activation require a restart, matching RPC route
snapshot semantics.
General RPC streams remain bounded control/data channels for importers, sync,
connection Providers, and non-hot terminal results. Phase 6 still creates its
planned direct terminal-worker-to-utility-process `MessagePort`; it must not put
the 4 ms interceptor budget through this general JSON-RPC path.
Every outbound write either reaches the transport or rejects. Port failure,
router close, peer cancellation, and local cancellation settle all queued
writes and invalidate retained receive-credit callbacks. If returning receive
credit itself fails, the incoming stream is removed and its owner is notified
before the error escapes. Normal end/error frames await the owner's asynchronous
close handler; forced synchronous router shutdown contains a rejected cleanup
promise so it cannot become an unhandled process rejection. Later provider code
must still release consumed chunks promptly and must not retain a release
callback as an application-level acknowledgement.
An outgoing stream that has sent its terminal `end` retains only its bounded
credit state until the peer releases the final chunks. Those ordinary late
window updates retire the stream instead of being misclassified as protocol
violations; writes remain closed as soon as `end` is sent.
## Placement, lifecycle, and packaged modules
Runtime placement is selected through an injectable resolver. The default
continues to prefer the sandboxed browser entrypoint. Phase 3 can require an
advanced-runtime grant, and phase 9 can add trust attestation, without changing
activation, crash, or shutdown ownership.
The resolver receives an abort signal. Stop, disable, uninstall, and application
shutdown cancel both a pending placement decision and an activation already in
progress. Cancellation does not count as a plugin crash. A permission prompt
introduced in phase 3 must honor this signal, so shutdown never waits for a
renderer decision and no runtime can appear after the supervisor has closed.
Manager shutdown starts supervisor cancellation before waiting for its serialized
mutation queue, so a mutation currently blocked inside placement or activation
cannot deadlock the quit path that is waiting for that same mutation.
Concurrent manager or supervisor shutdown callers share the same completion
promise. No caller may observe shutdown completion before runtime teardown and
startup cancellation have both settled.
Browser and utility runtimes recheck the same signal after every asynchronous
resource-creation boundary; cancelling only the outer supervisor promise is not
sufficient.
Start and stop also share a per-plugin transition gate. A lazy activation waits
for the previous process to finish stopping, while disable/uninstall persist the
disabled state before teardown. Later activation events therefore cannot race a
management operation and recreate a runtime that the user just disabled.
For an advanced utility runtime, `kill()` is only a termination request. Its
stop promise remains pending until Electron emits the child `exit` event.
Unexpected fatal and protocol failures likewise revoke RPC immediately but are
published to the supervisor only after the process is reaped. Permission,
connection, synchronization, and companion state can therefore treat the stop
event as a real process-containment boundary rather than an intent signal.
If the process ignores graceful termination, the host escalates to an OS-level
forced termination after a bounded grace period and still waits for `exit`.
Failure to reap after escalation disables and quarantines the plugin for the
rest of the application process; no replacement activation is allowed until
Netcatty restarts. This fail-closed state is deliberately in-memory as well as
persisted, so clearing a normal crash quarantine cannot overlap a still-live
advanced process.
Runtime state listeners receive starting, running, stopped, error, and
quarantined transitions with the stable activation identity. Permission scopes,
commands, views, and Provider registries can release their state on one common
stop boundary. Listener failures cannot break plugin shutdown.
Progress notifications have a separate supervisor event. Each event carries
the host-assigned activation identity together with the schema-validated token
and an immutable progress value, so simultaneous Providers from different
plugins or plugin versions cannot collide on a token alone.
Browser import maps and utility-process loader mappings are generated from one
reviewed host-module resource list. Adding `@netcatty/plugin-ui` or another
host-owned SDK package does not expand arbitrary filesystem access or require a
new protocol route. Plugin packages still cannot add mappings themselves.
## Downstream phase matrix
| Phase | Stable seam available to the phase | Work owned by that phase |
| --- | --- | --- |
| PR 3 permissions | RPC middleware, immutable runtime identity, raw-message guard, placement/principal resolver, runtime stop events | principal-bound grants, resource canonicalization, secrets, credentials, companions, quotas (implemented) |
| PR 4 contributions | host-to-plugin request/notify, runtime events, host module resources | implemented: lazy activation, command/settings/view registries, Context Keys, UI SDK and sandboxed views |
| PR 5 terminal Providers | validated host requests, cancellation, lifecycle events | Provider ranking, deadlines, snapshots, built-in highlighter/autocomplete adapters |
| PR 6 terminal pipeline | runtime identity and placement policy | direct MessagePort fast path, sensitive-input bypass, circuit breaker |
| PR 7 connection/auth/import | activation-owned Provider requests, exact result validators, bounded streams, diagnostics, secret leases, credential refs | implemented: connection sessions, authentication challenges, importer preview/commit |
| PR 8 sync | streams, lifecycle identity, namespaced storage boundary | implemented: `kind: "sync"` Providers with `provider.sync`, encrypted-object connect/read/write/delete/capabilities, WebDAV through shared storage surface, non-cascade sidecars for `sync: true` settings and account/CRDT baselines |
| PR 9 distribution | retained immutable versions, compare-and-set restore, placement resolver, module resources | signatures, trust, health checks, audited update and user rollback policy, API 1.0, and the reproducible terminal benchmark harness/environment/release gate for the 1% throughput and 4 ms p95 / 8 ms p99 input-latency targets |
## Data-model decisions that must remain explicit
The phase-2 database retains every installed immutable version and provides a
compare-and-set pointer restore used only when a just-installed version fails
activation. Phase 9 can build audited update, health-check, and user-initiated
rollback policy on this primitive without changing package layout; phase 2 does
not expose that broader policy.
Crash history is keyed by plugin and version. Changing the active version
starts with clean runtime state, while reinstalling identical version bytes
does not clear quarantine. Phase 9 can therefore assess and roll back one bad
release without inheriting or erasing another version's failure history.
Package publication exposes one internal `beforeActivate` commit boundary.
The manager uses it to disable and stop an enabled old activation before the
database pointer changes, and restores that activation if preparation fails.
Phase 9 health checks and rollback must preserve this ordering instead of
writing the active-version pointer directly.
`plugin_kv` is runtime-owned local data and is removed by explicit uninstall.
Phase-3 encrypted secrets, persistent grants and security audit, plus phase-4
settings and view state, use separate non-cascade tables. PR 8 stores encrypted
sync sidecars (`plugin_sync_sidecars`) in the same non-cascade class for
`sync: true` settings plus plugin sync account/CRDT baselines. Missing or
uninstalled plugin code must not cascade-delete those rows or coerce related
sync configuration away.
Activation events are declared by the public manifest. Phase 4 now starts only
`onStartupFinished` plugins during contribution initialization; commands, views,
and Providers call the existing idempotent `start()` boundary at first use.
This implements lazy activation without replacing process supervision.
## Review checklist for changes to this boundary
Before a later phase changes the supervisor or transport, verify:
1. Can the behavior be expressed as a registry handler, middleware, placement
resolver, state listener, validated request, or stream owner instead?
2. Does every privileged operation retain the host-generated runtime identity?
3. Can a request race an update, disable, quarantine, crash, or shutdown and
reach a stale runtime?
4. Are request and notification semantics distinct and exactly validated?
5. Is terminal hot-path work kept off general JSON-RPC?
6. Does missing or uninstalled plugin code preserve user-owned data?
7. Does adding a host SDK module expand only an explicit trusted resource list?
If the answer requires a new public plugin concept, update the canonical JSON
Schema, generated types, SDK, compatibility rules, documentation, and drift
tests together. An internal shortcut must not become an accidental public API.