feat: convert serial console into the serial plugin

This commit is contained in:
LukeGus committed 2026-09-23 23:06:37 -05:00
1 parent 708eedb861
commit 791d8656a1
121 files changed
+2216 -1425

No files matched your search

+17
View File
@@ -1111,6 +1111,21 @@ export interface PluginAudit {
}) => Promise<void>;
}
/**
* A generic capability check, for a privileged action that has no dedicated
* ctx method to wrap it: a plugin bundling its own native dependency to touch
* hardware (a serial port, a USB device) is the first caller. Core cannot
* mediate that access the way it mediates ctx.ssh or ctx.db, so the
* capability here is a declared, reviewable, audited statement of intent
* rather than a technical gate, matching credentials:read and process:spawn.
*/
export interface PluginCapabilities {
/** Whether this plugin currently holds `capability`. Not audited: for deciding whether to offer something, not for gating an action. */
has: (capability: string) => Promise<boolean>;
/** Throws PluginCapabilityError if this plugin does not hold `capability`. Audited like every guarded ctx method. */
require: (capability: string) => Promise<void>;
}
export interface PluginContext {
readonly pluginId: string;
readonly manifest: PluginManifest;
@@ -1127,6 +1142,8 @@ export interface PluginContext {
readonly rbac: PluginRbac;
readonly settings: PluginSettings;
readonly disposables: PluginDisposables;
/** A generic capability check, for privileged code no ctx method wraps. */
readonly capabilities: PluginCapabilities;
/** Hosts the actor can see, and host-sharing operations. Needs hosts:read / hosts:write. */
readonly hosts: PluginHosts;
/** SSH through core's connect pipeline. Needs ssh:connect and credentials:use. */
+1
View File
@@ -45,6 +45,7 @@ export const CAPABILITY_CATALOG: readonly CapabilityInfo[] = [
entry("users:write", "high"),
entry("auth:provide", "high"),
entry("system:tls", "high"),
entry("device:serial", "high"),
entry("hosts:write", "medium"),
entry("credentials:use", "medium"),
+6
View File
@@ -232,6 +232,12 @@ export interface TabOptions {
singleton?: boolean;
/** A live session: asks before closing and can be refreshed. */
session?: boolean;
/**
* Whether a session tab offers "Share session". Default true. A plugin
* whose ref does not implement session sharing (a local device, not a
* remote connection another user could join) sets this false.
*/
shareable?: boolean;
/** Restorable without a host. */
hostless?: boolean;
/** Decides whether a saved tab for this host may be restored. */
+26
View File
@@ -298,6 +298,15 @@ export interface PluginManifest {
/** Locales directory, relative to the plugin root. */
locales?: string;
platforms?: string[];
/**
* Bare npm package names carrying a native (.node) binding. The CLI build
* never bundles them: they stay a real dependency in the plugin's own
* package.json and are resolved from node_modules at runtime instead,
* because esbuild bundling a native addon breaks the relative path it uses
* to locate its compiled binary. See ARCHITECTURE.md under "native
* dependencies".
*/
nativeDependencies?: string[];
}
export const DEFAULT_BACKEND_ENTRY = "dist/backend.js";
@@ -331,6 +340,7 @@ const ALLOWED_TOP_LEVEL = new Set([
"frontend",
"locales",
"platforms",
"nativeDependencies",
]);
const ALLOWED_CONTRIBUTES = new Set([
@@ -441,6 +451,7 @@ export function validateManifest(manifest: unknown): string[] {
validateCategory(m.category, errors);
validateCapabilities(m.capabilities, errors);
validatePlatforms(m.platforms, errors);
validateNativeDependencies(m.nativeDependencies, errors);
validateDependencyMap(m.dependencies, "dependencies", errors);
validateDependencyMap(m.optionalDependencies, "optionalDependencies", errors);
validateProvides(m.provides, errors);
@@ -535,6 +546,21 @@ function validatePlatforms(platforms: unknown, errors: string[]): void {
});
}
function validateNativeDependencies(value: unknown, errors: string[]): void {
if (value === undefined) return;
if (!Array.isArray(value) || value.length === 0) {
errors.push(
'Field "nativeDependencies" must be a non-empty array when present',
);
return;
}
value.forEach((entry, index) => {
if (typeof entry !== "string" || entry.length === 0) {
errors.push(`nativeDependencies[${index}] must be a non-empty string`);
}
});
}
function validateDependencyMap(
value: unknown,
field: string,
+14
View File
@@ -406,6 +406,13 @@ export function createFakeContext(
}) as PluginMiddleware,
},
// Nothing here is capability-checked, per the module doc; createMockCtx
// gates this against options.capabilities.
capabilities: {
has: async () => true,
require: async () => {},
},
settings: {
get: async (key) =>
settings.get(settingsKey("admin", undefined, key)) as never,
@@ -936,6 +943,13 @@ export function createMockCtx(
return ctx.desktop.openIsolatedWindow(request);
},
},
capabilities: {
has: async (capability) => granted.has(capability),
require: async (capability) => {
require(capability);
},
},
};
return { ...base, ctx: gatedCtx, checked };