[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
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:
617
docs/plugin-platform/contract-and-sdk.md
Normal file
617
docs/plugin-platform/contract-and-sdk.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# Netcatty plugin contract and SDK
|
||||
|
||||
Status: internal preview (`0.1.0-internal`)
|
||||
Tracking issue: [#2269](https://github.com/binaricat/Netcatty/issues/2269)
|
||||
|
||||
Phases 2 and 3 consume this contract in the isolated host runtime and secure
|
||||
capability boundary. See
|
||||
[isolated-runtime.md](./isolated-runtime.md) for installation transactions,
|
||||
runtime placement, RPC routing, lifecycle and crash quarantine, and
|
||||
[security-and-permissions.md](./security-and-permissions.md) for grants,
|
||||
credentials and host-mediated capabilities.
|
||||
|
||||
This document describes the canonical contract first delivered by phase 1 and
|
||||
extended before public release. Runtime loading and capability enforcement live
|
||||
in the host rather than the schema package. UI contributions, terminal
|
||||
Providers, terminal interceptors, and connection/authentication/importer
|
||||
Providers have now extended the same internal contract before public release.
|
||||
Synchronization Providers and signed distribution remain later phases.
|
||||
|
||||
## Contract ownership
|
||||
|
||||
`packages/plugin-contract/schema/plugin-contract.schema.json` is the canonical
|
||||
public protocol. It uses JSON Schema 2020-12 and defines:
|
||||
|
||||
- package manifests and entrypoints;
|
||||
- permission declarations;
|
||||
- setting, command, menu, view, and provider contributions;
|
||||
- JSON-RPC requests, notifications, results, cancellation, and errors;
|
||||
- runtime initialization, feature negotiation, and progress notifications;
|
||||
- JSON and binary stream frames with flow-control windows;
|
||||
- companion-process Content-Length framing;
|
||||
- permission requests and decisions;
|
||||
- provider requests and results.
|
||||
|
||||
`npm run generate:plugin-contract` derives two committed artifacts from that
|
||||
file:
|
||||
|
||||
1. TypeScript types exported by `@netcatty/plugin-contract`;
|
||||
2. a self-contained schema bundle under `electron/plugins/generated/` for the
|
||||
future host runtime.
|
||||
|
||||
`npm run check:plugin-contract` compares both outputs byte-for-byte. CI can
|
||||
therefore reject a schema edit whose SDK or Electron representation was not
|
||||
regenerated.
|
||||
|
||||
The contract is intentionally marked internal. Compatibility is not promised
|
||||
until the final rollout PR freezes API 1.0. Review revisions made before this
|
||||
first contract is merged remain `0.1.0-internal`; after a contract revision is
|
||||
merged, every breaking change must update the schema identifier, workspace
|
||||
package versions, and generated artifacts in the same commit.
|
||||
|
||||
## Package layout
|
||||
|
||||
A plugin is a directory with `netcatty.plugin.json` at its root. The manifest
|
||||
declares one or both execution entrypoints:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"id": "com.example.my-plugin",
|
||||
"name": "my-plugin",
|
||||
"version": "0.1.0",
|
||||
"publisher": "example",
|
||||
"engines": {
|
||||
"netcatty": ">=0.0.0",
|
||||
"api": ">=0.1.0-internal <0.2.0"
|
||||
},
|
||||
"features": {
|
||||
"required": ["netcatty.rpc.progress"],
|
||||
"optional": ["netcatty.stream.binary"]
|
||||
},
|
||||
"main": {
|
||||
"browser": "dist/browser.js",
|
||||
"node": "dist/node.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Manifest bytes must be valid UTF-8. Directory validation and archive validation
|
||||
share one fatal UTF-8 parser; invalid byte sequences are rejected instead of
|
||||
being replaced with `U+FFFD` before JSON and semantic validation.
|
||||
|
||||
Paths use relative POSIX syntax and are limited to 128 Unicode code points and
|
||||
512 UTF-8 bytes. The schema and package validator reject absolute paths,
|
||||
drive-letter paths, repeated separators, backslashes, `.` and `..` segments,
|
||||
Windows reserved names, control or platform-special characters, and
|
||||
platform-specific trailing dots or spaces. The semantic package validator also
|
||||
requires NFC-normalized text and uses conservative Unicode compatibility/case
|
||||
folding when detecting path aliases. It applies the same syntax and portability
|
||||
checks after Unicode compatibility normalization, so compatibility characters
|
||||
cannot introduce separators, traversal segments, drive prefixes, reserved
|
||||
names, or trailing dots. Every official host consumer must run it after schema
|
||||
validation because JSON Schema cannot express these filesystem rules.
|
||||
Every entrypoint, view document, package icon, and companion variant must exist
|
||||
in the package.
|
||||
|
||||
Browser and Node entrypoints express placement. A Node entrypoint additionally
|
||||
requires the explicit high-risk `runtime.advanced` declaration and grant; the
|
||||
runtime still evaluates the remaining manifest permissions, trust level, and user grants
|
||||
before activating either entrypoint.
|
||||
|
||||
Each advanced companion has a stable contribution ID and one or more platform
|
||||
variants. A variant binds one package path and SHA-256 digest to one or more
|
||||
compatible OS/architecture targets, allowing a universal script to be shared
|
||||
while macOS, Linux, and Windows native binaries remain distinct. A companion
|
||||
cannot declare the same target platform twice, and no two companion variants
|
||||
may claim the same package path. A manifest with companions must also provide a
|
||||
Node utility entrypoint and declare both `runtime.advanced` and a resource-bound
|
||||
`companion.execute` permission. The first-party placement resolver selects the
|
||||
utility entrypoint for companion manifests even when a browser entrypoint is
|
||||
also present. An ordinary browser placement cannot authorize or launch a
|
||||
companion.
|
||||
|
||||
## Contribution identity
|
||||
|
||||
Every setting, command, view, provider, and companion executable has a globally
|
||||
unique contribution ID. Its exact prefix is the owning plugin ID followed by a
|
||||
dot:
|
||||
|
||||
```text
|
||||
<pluginId>.<localContributionName>
|
||||
com.netcatty.hello.sayHello
|
||||
com.netcatty.hello.settings.greeting
|
||||
```
|
||||
|
||||
The schema requires a namespaced contribution shape. Semantic validation then
|
||||
checks the dynamic relationship to `manifest.id`; a contribution declared by
|
||||
`com.netcatty.hello` cannot use `com.other.plugin.sayHello`. Menu command
|
||||
references and `onCommand:` activation events must resolve to commands declared
|
||||
by the same manifest. This prevents two independently installed plugins from
|
||||
claiming the same command, setting, provider, view, or companion identifier.
|
||||
The same ID also cannot be reused across registry kinds within one plugin, so a
|
||||
command and a view never compete for one routing key.
|
||||
Activation uses one canonical top-level registry. The supported events are
|
||||
`onStartupFinished`, `onCommand:<id>`, `onView:<id>`, and `onProvider:<id>`;
|
||||
every targeted ID must resolve to a contribution in the same manifest. Provider
|
||||
entries do not carry a second, potentially conflicting activation field.
|
||||
|
||||
Commands own their canonical title, description, icon, and enablement state.
|
||||
Menus can override the title or icon for a particular placement and declare an
|
||||
alternate command, visibility, enablement, checked state, ordering, and whether
|
||||
the resolved keybinding is displayed. Keybindings are separate contributions:
|
||||
each binding names its command, portable fallback key, optional macOS/Linux/
|
||||
Windows overrides, Context Key condition, and JSON command arguments. This
|
||||
avoids treating one command-level shortcut as the only binding and permits the
|
||||
host to resolve platform conflicts centrally. Menu and keybinding command
|
||||
references must resolve to commands in the same manifest.
|
||||
|
||||
Icons are discriminated references. A `theme` icon names a host-owned icon; a
|
||||
`package` icon names a required light asset and optional dark asset. Package icon
|
||||
paths receive the same normalization, traversal, archive-presence, and integrity
|
||||
checks as code and view entrypoints. Views also declare placement, order,
|
||||
visibility, icon, and whether their isolated context remains alive while hidden.
|
||||
All `when`, `enablement`, and `checked` values are opaque host-parsed Context Key
|
||||
expressions; plugin code never evaluates or injects them into native UI.
|
||||
|
||||
## Compatibility and feature negotiation
|
||||
|
||||
Both `engines.netcatty` and `engines.api` are node-semver ranges. Schema
|
||||
validation rejects unsafe range characters, while the semantic validator uses
|
||||
the complete node-semver grammar. An exact API version is a valid range, but
|
||||
plugins should normally declare the compatible internal API interval so the
|
||||
intent is explicit. Prerelease versions are not globally enabled: a range must
|
||||
name a compatible prerelease baseline explicitly, so `<0.2.0` does not silently
|
||||
accept `0.2.0-alpha`.
|
||||
|
||||
The optional manifest `features` object separates required and optional feature
|
||||
IDs. Required and optional sets cannot overlap. During runtime initialization,
|
||||
the host sends the `plugin.initialize` JSON-RPC method using
|
||||
`RuntimeInitializeRequest` and `RuntimeInitializeParams` with its exact Netcatty
|
||||
version, API version, and supported features. The plugin answers with
|
||||
`RuntimeInitializeSuccess` and `RuntimeInitializeResult`, including only the
|
||||
features enabled for that runtime.
|
||||
Initialization must fail before activation when either engine range is not
|
||||
satisfied or a required feature is unavailable. Optional features are enabled
|
||||
only when both sides support them.
|
||||
|
||||
The CLI exposes the same algorithm before installation or packaging:
|
||||
|
||||
```bash
|
||||
netcatty-plugin compatibility ./my-plugin \
|
||||
--netcatty 1.4.0 \
|
||||
--api 0.1.0-internal \
|
||||
--features netcatty.rpc.progress,netcatty.stream.binary
|
||||
```
|
||||
|
||||
`checkPluginCompatibility()` is also exported for the phase-2 package manager
|
||||
and runtime. It returns the enabled optional features, missing required features,
|
||||
and deterministic incompatibility reasons rather than a single boolean.
|
||||
|
||||
Before applying a version-specific full schema, a host reads
|
||||
`PluginManifestHeader`. This bootstrap shape deliberately permits unknown fields
|
||||
and future positive `manifestVersion` values while validating identity, plugin
|
||||
version, and engine ranges. The host can therefore select a supported schema or
|
||||
report a precise API/schema incompatibility before the strict full manifest
|
||||
validator rejects unknown fields. The full `PluginManifest` remains closed with
|
||||
`additionalProperties: false` so misspelled security declarations never become
|
||||
silent no-ops.
|
||||
|
||||
## RPC, progress, streams, and companion stdio
|
||||
|
||||
Control messages follow JSON-RPC 2.0. `RpcFailure.id` is nullable for parse and
|
||||
invalid-request errors where the request ID cannot be recovered. Long-running
|
||||
operations use `$/progress` notifications with `begin`, `report`, and `end`
|
||||
values and stable string or integer progress tokens. A report may use an
|
||||
absolute percentage or an incremental percentage, never both.
|
||||
Numeric RPC IDs and progress tokens are restricted to non-negative JavaScript
|
||||
safe integers. Peers that need a larger opaque identifier use the string form;
|
||||
this prevents distinct JSON numbers from collapsing onto one correlation key
|
||||
when Electron or Node parses them.
|
||||
|
||||
One JSON-RPC control message is limited to 1 MiB. The generated
|
||||
`PLUGIN_RPC_MAX_JSON_BYTES` constant exposes that boundary; payloads above it
|
||||
must use the stream protocol. Stream frames use the separate generated
|
||||
`PLUGIN_STREAM_MAX_FRAME_JSON_BYTES` limit (24 MiB), which is large enough for
|
||||
one maximum-size base64 chunk without turning the control plane into an
|
||||
unbounded data channel.
|
||||
|
||||
Stream control envelopes remain schema-valid JSON, but chunk data has three
|
||||
explicit encodings:
|
||||
|
||||
- `json` carries a normal JSON value;
|
||||
- `base64` carries binary bytes over JSON-only transports such as companion
|
||||
stdio;
|
||||
- `transfer` declares that the `MessagePortStreamEnvelope` carries an
|
||||
`ArrayBuffer` in its `transfer` property. The sender passes that same buffer
|
||||
in the structured-clone transfer list.
|
||||
|
||||
The declared `byteLength` is the UTF-8 JSON or unencoded binary byte count. A
|
||||
receiver validates JSON serialization, base64 decoding, or transferred buffer
|
||||
length before accepting credit. The contract exports `createJsonStreamChunk()`,
|
||||
`createBase64StreamChunk()`, `materializeStreamChunk()`, and
|
||||
`createMessagePortStreamEnvelope()` so every host path applies the same checks.
|
||||
`assertStreamChunkData()`, `assertStreamFrame()`, and the envelope helper accept
|
||||
untyped boundary values. Inline JSON and base64 assertions verify the encoded
|
||||
bytes against the declared length before a consumer advances sequence or credit
|
||||
state; transfer chunks defer that comparison until the envelope supplies the
|
||||
actual `ArrayBuffer`. The frame helpers also reject unknown frame kinds, missing
|
||||
or additional properties, malformed chunk/error payloads, and stream IDs
|
||||
outside the Schema-owned 128-character limit. The envelope helper returns a
|
||||
normalized frame assembled only from validated own data properties rather than
|
||||
returning the caller's object unchecked.
|
||||
The open frame is sequence 0 and grants the initial `windowBytes`; data and
|
||||
terminal frames begin at sequence 1. Sequence numbers increase independently in
|
||||
each sending direction and cannot exceed `Number.MAX_SAFE_INTEGER`. A producer
|
||||
must open a replacement stream before exhausting that range. It subtracts every
|
||||
chunk's declared byte length from its credit and must stop at zero.
|
||||
The initial receive window is 1 KiB through 16 MiB, and each
|
||||
`windowUpdate.creditBytes` grant is 1 byte through 16 MiB. The public
|
||||
MessagePort envelope helper enforces the same Schema-owned ranges before
|
||||
returning a frame. The generated runtime constants, including the stream ID,
|
||||
chunk, frame-byte, window, credit, safe-integer, RPC-byte, and error-code limits,
|
||||
are derived from the same Schema and checked for drift. `windowUpdate.creditBytes` grants an
|
||||
additional amount rather than replacing the window, so retries and duplicate
|
||||
control frames cannot be interpreted as an absolute reset. A stdio peer must
|
||||
never emit the `transfer` encoding because stdio has no structured-clone
|
||||
transfer list; the framing encoder rejects it.
|
||||
|
||||
Public encoders validate runtime values instead of trusting TypeScript casts.
|
||||
They reject non-finite numbers, `undefined`, sparse arrays, accessors, symbols,
|
||||
cycles, and non-plain objects before serialization. Serialization reads only
|
||||
validated own data properties and does not invoke inherited `toJSON()` hooks,
|
||||
so prototype mutation cannot change the bytes after validation. Base64 must use
|
||||
canonical RFC 4648 padding bits, and every stream chunk remains bounded to 16
|
||||
MiB even when a caller bypasses JSON Schema validation. This prevents a browser
|
||||
plugin and a native companion from computing different bytes for the same
|
||||
apparent message.
|
||||
|
||||
All public JSON validators also enforce a maximum nesting depth of 128 and a
|
||||
maximum of 100,000 values per message. Manifest validation applies the same
|
||||
structural budget before invoking the recursive JSON Schema validator. Deep or
|
||||
pathologically wide payloads are therefore rejected as ordinary validation
|
||||
failures instead of exhausting the JavaScript call stack or monopolizing the
|
||||
runtime.
|
||||
|
||||
Advanced companion processes exchange UTF-8 JSON using this exact framing:
|
||||
|
||||
```text
|
||||
Content-Length: <decimal UTF-8 byte length>\r\n
|
||||
Content-Type: application/json; charset=utf-8\r\n
|
||||
\r\n
|
||||
<JSON bytes>
|
||||
```
|
||||
|
||||
`Content-Length` is required exactly once. `Content-Type` is optional when
|
||||
decoding but, when present, must be `application/json` with an optional UTF-8
|
||||
charset. Header names are case-insensitive and the default header limit is
|
||||
8 KiB; unknown or duplicate headers,
|
||||
non-ASCII header bytes, invalid UTF-8, malformed JSON, and frames above 16 MiB
|
||||
are rejected. Syntactically valid numbers that overflow JavaScript to a
|
||||
non-finite value are also rejected before a decoded message is returned.
|
||||
Decoder options may lower but never raise the 16 MiB absolute content limit.
|
||||
`encodeContentLengthFrame()` and the incremental
|
||||
`ContentLengthFrameDecoder` implement this contract without shell or line-based
|
||||
parsing. The decoder uses an amortized queue instead of removing array heads,
|
||||
and coalesces small inputs into bounded slabs, so adversarial one-byte
|
||||
fragmentation remains linear without retaining one object per byte. Incoming
|
||||
Node.js `Buffer` data is copied before `push()` returns and cannot mutate a
|
||||
partially buffered frame later.
|
||||
`finish()` detects truncated frames when a process exits.
|
||||
|
||||
JSON-RPC standard failures retain their standard integer codes. SDK
|
||||
`PluginError` values use stable implementation-defined codes in JSON-RPC's
|
||||
reserved server-error range:
|
||||
|
||||
| SDK code | Wire code |
|
||||
| --- | ---: |
|
||||
| `cancelled` | -32001 |
|
||||
| `unknown` | -32002 |
|
||||
| `invalid_argument` | -32003 |
|
||||
| `deadline_exceeded` | -32004 |
|
||||
| `not_found` | -32005 |
|
||||
| `already_exists` | -32006 |
|
||||
| `permission_denied` | -32007 |
|
||||
| `resource_exhausted` | -32008 |
|
||||
| `failed_precondition` | -32009 |
|
||||
| `aborted` | -32010 |
|
||||
| `out_of_range` | -32011 |
|
||||
| `unsupported` | -32012 |
|
||||
| `internal` | -32013 |
|
||||
| `unavailable` | -32014 |
|
||||
| `data_loss` | -32015 |
|
||||
| `unauthenticated` | -32016 |
|
||||
|
||||
`pluginErrorToRpcError()` performs the mapping and includes the stable SDK code
|
||||
in `error.data.pluginCode` so clients can preserve meaning without parsing text.
|
||||
Permission decisions and Provider results are also discriminated unions: an
|
||||
`allow` decision requires a grant scope, denied/cancelled decisions cannot
|
||||
smuggle one, successful Provider results require `result`, and failed results
|
||||
require a stable RPC error.
|
||||
|
||||
Reserved RPC methods cannot fall back to the generic request or notification
|
||||
shape. `plugin.initialize`, `$/progress`, and `$/cancelRequest` must validate
|
||||
against their dedicated schemas at the aggregate `RpcMessage` boundary. RPC
|
||||
responses are validated against the method recorded for their pending request;
|
||||
the generic success envelope alone is not sufficient to validate a
|
||||
method-specific result.
|
||||
|
||||
## TypeScript SDK
|
||||
|
||||
`@netcatty/plugin-sdk` exports the generated contract types and a small set of
|
||||
lifecycle primitives:
|
||||
|
||||
- `definePlugin` keeps exact plugin types while checking the activation shape;
|
||||
- `DisposableStore` gives activation code one cleanup owner;
|
||||
- `CancellationTokenSource` provides cooperative cancellation without exposing
|
||||
host abort controllers;
|
||||
- `PluginError` carries a stable machine-readable error code and JSON details;
|
||||
- `PluginContext` exposes the exact Netcatty/API versions, negotiated feature
|
||||
set, storage, opaque secret references, credential leases, mediated network
|
||||
and filesystem access, companion handles, contribution settings, command
|
||||
registration/execution, Context Keys, view messaging/state, locale/theme/
|
||||
application theme tokens/accessibility environment, logging, and
|
||||
subscriptions.
|
||||
|
||||
The terminal Provider registry, bounded result shapes, lifecycle snapshots,
|
||||
host adapters, and the explicit PR-6 raw-interceptor boundary are documented in
|
||||
[`terminal-providers.md`](./terminal-providers.md).
|
||||
|
||||
Phase-4 contribution methods stay on the same validated control plane. The
|
||||
runtime registers command handlers only after activation; the host routes
|
||||
`plugin.command.execute` back to the owning runtime. Setting reads and writes
|
||||
are scoped by the declaration, view state is namespaced by plugin/view/window,
|
||||
and environment changes arrive as notifications. Custom-view preload APIs are
|
||||
separate from `PluginContext` and cannot acquire the runtime's capability
|
||||
objects.
|
||||
|
||||
Phase-5 Provider handlers are activation-owned SDK registrations. The host
|
||||
performs immutable enumeration without activation, authorizes the exact
|
||||
Provider permission set at first use, invokes through `RuntimeSupervisor`, and
|
||||
validates the canonical Provider result plus the terminal operation's bounded
|
||||
result shape before application use. Phase 6 reuses the same declarations and
|
||||
registration ownership but moves raw terminal bytes onto a dedicated
|
||||
worker-to-utility MessagePort; they never traverse the JSON-RPC control plane.
|
||||
The ready, chunk, successful-result, and failed-result frame metadata is owned
|
||||
by `TerminalInterceptorFrame` in the canonical Schema. Both peers validate the
|
||||
generated Schema shape and the shared transfer envelope verifies that only
|
||||
chunk and successful-result frames carry a real attached `ArrayBuffer` whose
|
||||
length exactly matches the declared bounded `byteLength`.
|
||||
|
||||
The PR 7 implementation adds connection, authentication, and importer Providers without changing
|
||||
the Provider ownership model. Connection Provider result types are operation
|
||||
specific. The SDK registers connection Providers as an operation-keyed handler
|
||||
map so TypeScript binds each invocation to its exact result: `validateConfiguration`,
|
||||
`probe`, `open`, and `getStatus` each return their named result object, while
|
||||
`resize`, `signal`, `reconnect`, and `close` acknowledge completion with JSON
|
||||
`null`. Objects on those control operations are rejected by the generated
|
||||
schema, SDK type map, runtime dispatch shape, and runtime validator.
|
||||
`ConnectionStatusResult` may carry bounded `ProviderValidationIssue`
|
||||
diagnostics; when a later status poll reports `closed` or `error`, those
|
||||
diagnostics are forwarded with the terminal-session exit event rather than
|
||||
being available only during the initial `open`.
|
||||
The application validates configuration and runs `probe` before opening a
|
||||
session, routes an explicit terminal interrupt through `signal`, and gives a
|
||||
retryable runtime failure one host-owned `reconnect` attempt before closing the
|
||||
session. Resize, close, status polling, and reconnect remain bound to the same
|
||||
host-owned session identity.
|
||||
|
||||
Importer draft records are also exact public shapes, not arbitrary JSON bags.
|
||||
Host, identity, key, snippet, and group drafts each declare required fields,
|
||||
allowed enums, byte/array limits, and closed object properties. Host drafts may
|
||||
either name a built-in host with a hostname or a plugin protocol plus an opaque
|
||||
`pluginConnection` object whose provider ID must match the `plugin:<id>`
|
||||
protocol during host-owned semantic normalization. Executable startup commands,
|
||||
hidden built-in plaintext credentials, and unknown host properties are not part
|
||||
of the importer contract; plugin-owned credentials must flow through identity
|
||||
or key drafts and then through the existing host-owned encrypted persistence
|
||||
path. Safe preview output is redacted and bounded before UI display.
|
||||
Importer Providers use their own operation-keyed handler map, so `detect` must
|
||||
return a detection result and `parse` must return completion counters. Streamed
|
||||
records use the public `ImporterLimits` byte and count bounds. A plugin
|
||||
connection draft may reference an identity or key draft from the same import by
|
||||
its source ID; Netcatty maps that reference to the new host-owned credential ID
|
||||
before encrypted persistence and rejects unresolved or ambiguous references.
|
||||
|
||||
PR 8 adds sync Providers as another operation-keyed map under permission
|
||||
`provider.sync`. Plugins implement only encrypted object storage: `connect`,
|
||||
`disconnect`, `getAccount`, `getCapabilities`, `readObject`, `writeObject`, and
|
||||
`deleteObject`. Capability reporting covers revisions, conditional writes,
|
||||
atomic replacement, and size limits (`SyncLimits`). Large objects leave the
|
||||
JSON control plane on the existing stream seam; plugins never receive the cloud
|
||||
master key or plaintext vault. Netcatty continues to own encryption, CRDT merge,
|
||||
migrations, protection snapshots, conflict handling, and read-merge-write-verify.
|
||||
WebDAV is adapted through the shared encrypted-object storage interface so the
|
||||
same path can exercise configuration, secret handles, upload/download,
|
||||
verification, and recovery.
|
||||
|
||||
`PluginSecretStore.get()` never returns plaintext. It returns a host-issued
|
||||
`SecretRef`. Its random ID stays opaque; its non-secret `key` binds later lease
|
||||
authorization to the same manifest resource used by `get()`/`set()`. `set()`
|
||||
immediately transfers a value already known to the plugin into host storage
|
||||
before returning the same kind of reference. Network,
|
||||
authentication, and companion brokers can consume a one-use lease for the
|
||||
reference while the main process revalidates plugin ownership and operation
|
||||
scope. PR 7 also supplies a host-issued `CredentialRef` for Netcatty-owned
|
||||
Vault credentials through the same SDK method; its injected resolver does not
|
||||
materialize plaintext until lease consumption. Neither reference kind is a
|
||||
bearer capability, and neither may bypass permission, ownership, runtime, and
|
||||
operation checks.
|
||||
Host-rendered password settings likewise expose only references to plugin code.
|
||||
|
||||
The isolated host and phase-3 capability brokers provide these implementations.
|
||||
No renderer decision provider means requests fail closed; a manifest declaration
|
||||
never grants authority by itself.
|
||||
|
||||
`PluginFilesystemClient.writeFile()` currently requires `{ overwrite: true }`
|
||||
and an existing regular file. This preserves one stable SDK method while the
|
||||
cross-platform host denies unsafe arbitrary-path creation until it can bind a
|
||||
new child to an opened parent directory without a path race.
|
||||
`readDirectory()` likewise keeps its stable SDK/RPC method but fails closed
|
||||
unless the main process supplies a native adapter whose inode checks and entry
|
||||
enumeration are bound to the same directory handle.
|
||||
|
||||
Permission names already use the phase-3 enforcement boundaries: clipboard
|
||||
read/write, terminal metadata/output/input and input/output interception, Vault
|
||||
metadata/write/credentials, SFTP read/write, filesystem read/write, network
|
||||
origins, companion execution, and each Provider registration class are separate
|
||||
grants. A broad permission such as `terminal.read` or `filesystem` is not part
|
||||
of the contract. Setting controls likewise include the complete planned native
|
||||
set, including radio, slider, font, file/directory, sortable list, and structured
|
||||
table controls. Secret settings cannot opt into sync; list and table controls
|
||||
must declare a host-validated `valueSchema`. The accepted schema subset has
|
||||
bounded depth/nodes, explicit types and closed object properties; executable or
|
||||
backtracking features such as `$ref`, `pattern`, formats and conditionals are
|
||||
rejected. Defaults are checked against the control's value type, declared
|
||||
options, numeric range, step, and structured schema; duplicate option values
|
||||
and unsafe text patterns fail package validation. File and
|
||||
directory paths are device-local values and cannot opt into cloud sync.
|
||||
Semantic validation also requires every contribution class to declare its
|
||||
capability: commands, menus, views,
|
||||
settings, companion executables, and each Provider kind cannot appear without
|
||||
the matching required or optional permission. Companion-specific permission
|
||||
lists reuse the same canonical permission catalog and must be a subset of the
|
||||
manifest declarations. Provider capability IDs use the same lowercase,
|
||||
namespaced feature-ID grammar as runtime negotiation.
|
||||
Provider `configurationSchema` values are declarative JSON data interpreted by
|
||||
the host's restricted schema validator; providers never receive a way to inject
|
||||
configuration UI code into Netcatty.
|
||||
|
||||
Terminal Provider declarations are also tied to their least-privilege data
|
||||
capabilities. Completion requires `terminal.complete`; text-derived visual
|
||||
providers require terminal output plus decoration access; backgrounds require
|
||||
decoration access only. Raw interception is represented by two distinct kinds,
|
||||
`terminal.interceptor.input` and `terminal.interceptor.output`, and each requires
|
||||
its matching high-risk permission. One generic interceptor kind cannot be used
|
||||
to acquire both directions implicitly.
|
||||
|
||||
Plugin entrypoints should return or register every acquired resource:
|
||||
|
||||
```ts
|
||||
import { definePlugin } from "@netcatty/plugin-sdk";
|
||||
|
||||
export default definePlugin({
|
||||
activate(context) {
|
||||
context.subscriptions.add(registerSomething());
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Activation code must treat cancellation and deadlines as normal outcomes.
|
||||
Host-side cancellation can stop waiting for a plugin but cannot forcibly unwind
|
||||
arbitrary JavaScript without terminating the isolated runtime.
|
||||
|
||||
## CLI
|
||||
|
||||
`@netcatty/plugin-cli` supplies five commands:
|
||||
|
||||
- `init` creates a minimal TypeScript plugin;
|
||||
- `validate` checks a source directory or packaged archive;
|
||||
- `compatibility` checks Netcatty/API ranges and negotiates required and
|
||||
optional features;
|
||||
- `build` validates the manifest and runs the plugin's npm build script without
|
||||
a shell;
|
||||
- `pack` emits a deterministic `.ncpkg` archive.
|
||||
|
||||
The packer sorts UTF-8 package paths, stores fixed ZIP timestamps and file
|
||||
modes, and writes entries without platform-dependent compression output. The
|
||||
same files and manifest therefore produce the same archive bytes.
|
||||
The validated source-manifest byte length and SHA-256 are bound to the scanned
|
||||
manifest entry before writing; the archive writer then rechecks every scanned
|
||||
file while streaming it. A manifest changed after validation cannot be packaged
|
||||
under the previously validated object model. Source hashing enforces its byte
|
||||
budget during the read, and archive writing stops before emitting bytes beyond
|
||||
the scanned size, so a concurrently growing file cannot cause unbounded I/O.
|
||||
Build, archive-validation, extraction, and directory-validation results also
|
||||
carry the same versioned `contentSha256`. It hashes sorted logical entries
|
||||
(path, byte length, declared-companion classification, and file SHA-256), so the host
|
||||
can compare an extracted tree with a valid archive without assuming that all
|
||||
publishers used the same ZIP encoder or compression method.
|
||||
|
||||
Package validation rejects:
|
||||
|
||||
- path traversal, absolute paths, backslashes, and case-colliding names;
|
||||
- non-UTF-8 names and local/central ZIP header disagreements;
|
||||
- symbolic links and non-regular files;
|
||||
- executable files not declared as companion executables;
|
||||
- companion binaries whose SHA-256 does not match the manifest;
|
||||
- duplicate entries, encrypted entries, and unsupported compression methods;
|
||||
- missing entrypoints, views, package icons, and companion variants;
|
||||
- a source manifest whose packaged bytes differ from the validated snapshot;
|
||||
- excessive path, file, archive, or expanded-package sizes.
|
||||
|
||||
These checks are repeated when reading `.ncpkg` files. Installation in phase 2
|
||||
must not trust a package merely because the publisher previously ran the CLI.
|
||||
|
||||
## Compatibility rules for later phases
|
||||
|
||||
The following rules are fixed for this internal pre-release contract:
|
||||
|
||||
1. JSON Schema is the wire authority. TypeScript types alone never justify
|
||||
accepting an unvalidated message.
|
||||
2. Unknown manifest properties are rejected within API 0.1. This prevents a
|
||||
misspelled security declaration from silently becoming ineffective.
|
||||
3. Runtime control envelopes are JSON values. Binary stream data crosses a
|
||||
MessagePort only through the declared `ArrayBuffer` envelope property and
|
||||
the matching structured-clone transfer list; native objects,
|
||||
functions, Electron handles, DOM nodes, and cyclic values remain forbidden.
|
||||
4. Permission declarations do not grant access. They only make a future user
|
||||
grant possible.
|
||||
5. Required and optional permission sets cannot overlap.
|
||||
6. Secret settings cannot contain defaults in the manifest.
|
||||
7. Companion executables are content-addressed and explicitly declared.
|
||||
8. Cancellation identifiers and deadlines are part of the RPC contract so a
|
||||
slow plugin cannot retain an unbounded host request.
|
||||
9. Stream sequence numbers and receive windows are part of the public protocol;
|
||||
producers must stop when the receiver's advertised capacity is exhausted.
|
||||
10. Manifest schema validation is followed by semantic package validation.
|
||||
This enforces NFC paths and content-dependent rules that JSON Schema cannot
|
||||
represent by itself.
|
||||
11. Secret reads return opaque `SecretRef` values. Plaintext is never a normal
|
||||
SDK storage result, and possession of a reference never replaces identity,
|
||||
permission, ownership, or operation checks at the privileged boundary.
|
||||
12. Contribution IDs use the exact owning plugin ID as their namespace.
|
||||
13. Engine ranges and required features are checked before activation.
|
||||
14. Companion stdio uses bounded Content-Length-framed UTF-8 JSON; newline JSON
|
||||
and unbounded reads are not compatible transports.
|
||||
|
||||
## Phase-consumer audit and evolution rules
|
||||
|
||||
The cross-phase contract was checked against every planned consumer:
|
||||
|
||||
| Phase | Contract used without importing application internals |
|
||||
| --- | --- |
|
||||
| PR 2 runtime | manifest header/full validation, `plugin.initialize`, JSON-RPC, progress, cancellation, framing, streams |
|
||||
| PR 3 security | principal-bound grants, canonical resources, permission requests/decisions, `SecretRef`/`CredentialRef`/`SecretLeaseRef`, mediated SDK capabilities, stable failures and cancellation |
|
||||
| PR 4 contributions | namespaced settings, commands, menus, views and strict semantic references |
|
||||
| PR 5 terminal providers | namespaced provider IDs, provider request/result envelopes and bounded streams |
|
||||
| PR 6 data pipeline | direct MessagePort transfer envelopes, sequence, per-chunk credit, and bounded receive-window fields |
|
||||
| PR 7 connection/auth/import | provider kinds and configuration schemas, platform-specific companion variants, framing, stable failures and progress |
|
||||
| PR 8 sync | implemented: namespaced `sync` Providers, `SyncLimits`, operation-keyed connect/read/write/delete/capabilities, inline or streamed encrypted objects |
|
||||
| PR 9 rollout | schema/API selection, compatibility reporting and the final API 1.0 freeze |
|
||||
|
||||
The contract tests construct representative manifests for the contribution UI,
|
||||
ordinary terminal Provider, privileged input/output interceptor, and combined
|
||||
connection/authentication/sync/importer phases. These fixtures are validated by
|
||||
the same strict schema and semantic validator used by the CLI, so a later edit
|
||||
cannot silently make a planned phase inexpressible.
|
||||
|
||||
Core meanings are never changed in place after merge: contribution ownership,
|
||||
RPC method names, error-code mappings, framing, stream encodings, and feature
|
||||
negotiation require a new contract revision for incompatible changes. New
|
||||
setting controls, permission names, provider capabilities, or optional fields
|
||||
may be added only with an API/schema revision; a plugin that uses them declares
|
||||
the matching API range and required feature. This keeps strict validation while
|
||||
giving old hosts a deterministic fail-closed path through the manifest header.
|
||||
|
||||
## Repository commands
|
||||
|
||||
```bash
|
||||
npm run generate:plugin-contract
|
||||
npm run check:plugin-contract
|
||||
npm run test:plugin-contract
|
||||
npm run build:plugin-packages
|
||||
```
|
||||
|
||||
The complete application checks remain mandatory because workspace and root
|
||||
dependency changes affect installation and release builds.
|
||||
Reference in New Issue
Block a user