mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
fix: finish 2.9.0 plugin moves and get every check green
This commit is contained in:
1 parent
8c1e02af89
commit
6489b62ece
335 files changed
+29878
-17749
No files matched your search
@@ -56,6 +56,7 @@ export function registerAiRoutes(
|
||||
notify: ctx.notify,
|
||||
rbac: ctx.rbac,
|
||||
ssh: ctx.ssh,
|
||||
audit: ctx.audit,
|
||||
};
|
||||
|
||||
const logError = (message: string, error: unknown) =>
|
||||
@@ -728,8 +729,9 @@ export function registerAiRoutes(
|
||||
).aiAccess ?? { allowReadOnlyCommands: false };
|
||||
const hosts = await ctx.hosts.list();
|
||||
const config = await providerConfig(provider);
|
||||
const tools = availableTools((service) =>
|
||||
serviceAvailable(ctx.services, service),
|
||||
const tools = availableTools(
|
||||
(service) => serviceAvailable(ctx.services, service),
|
||||
{ allowReadOnlyCommands: access.allowReadOnlyCommands },
|
||||
);
|
||||
|
||||
// no-transform keeps core's compression from buffering the stream,
|
||||
|
||||
@@ -64,8 +64,14 @@ export const FORBIDDEN_DOMAINS = [
|
||||
*/
|
||||
export function availableTools(
|
||||
isAvailable: (service: string) => boolean,
|
||||
options: { allowReadOnlyCommands?: boolean } = {},
|
||||
): AiTool[] {
|
||||
return AI_TOOLS.filter((tool) => !tool.service || isAvailable(tool.service));
|
||||
return AI_TOOLS.filter(
|
||||
(tool) =>
|
||||
(!tool.service || isAvailable(tool.service)) &&
|
||||
(!tool.requiresReadOnlyCommands ||
|
||||
options.allowReadOnlyCommands === true),
|
||||
);
|
||||
}
|
||||
|
||||
/** Tool definitions in the shape the provider adapters expect. */
|
||||
|
||||
@@ -247,7 +247,7 @@ export async function applyProposal(
|
||||
* Runs one approved command over this plugin's SSH pool, mirroring how an
|
||||
* automation run_command step executes.
|
||||
*/
|
||||
async function runCommandOnHost(
|
||||
export async function runCommandOnHost(
|
||||
deps: ToolDeps,
|
||||
hostId: number,
|
||||
command: string,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { PluginHostSummary } from "@termix/plugin-sdk/backend";
|
||||
import { num, objectSchema, type AiTool } from "./types.js";
|
||||
import { num, objectSchema, str, type AiTool } from "./types.js";
|
||||
import { readService, SERVICE } from "../services.js";
|
||||
import { isReadOnlyCommand } from "./command-allowlist.js";
|
||||
import { runCommandOnHost } from "./executor.js";
|
||||
|
||||
/** Output handed back to the model is capped; a log can be huge. */
|
||||
const MAX_COMMAND_OUTPUT = 8000;
|
||||
|
||||
/**
|
||||
* Read tools project explicit fields rather than spreading rows. Redaction runs
|
||||
@@ -30,6 +35,47 @@ function toHostSummary(host: PluginHostSummary) {
|
||||
}
|
||||
|
||||
export const readTools: AiTool[] = [
|
||||
{
|
||||
name: "run_readonly_command",
|
||||
description:
|
||||
"Run a read-only diagnostic command on a host and get its output, for example df -h, uptime, free -m or systemctl status nginx. Only commands on a fixed read-only allowlist run; anything else must be proposed with propose_run_command.",
|
||||
category: "read",
|
||||
requiresReadOnlyCommands: true,
|
||||
parameters: objectSchema(
|
||||
{
|
||||
hostId: num("The host id, as returned by list_hosts"),
|
||||
command: str("The command to run"),
|
||||
},
|
||||
["hostId", "command"],
|
||||
),
|
||||
handler: async (args, context) => {
|
||||
if (!context.allowReadOnlyCommands) {
|
||||
return { error: "The user has not allowed read-only commands" };
|
||||
}
|
||||
const hostId = Number(args.hostId);
|
||||
const command = typeof args.command === "string" ? args.command : "";
|
||||
const check = isReadOnlyCommand(command);
|
||||
if (!check.allowed) {
|
||||
return { error: check.reason ?? "Command is not read-only" };
|
||||
}
|
||||
const access = await context.deps.hosts.checkAccess(hostId, "connect");
|
||||
if (!access.hasAccess) return { error: "Host not found" };
|
||||
|
||||
const result = await runCommandOnHost(context.deps, hostId, command);
|
||||
await context.deps.audit
|
||||
.record({
|
||||
action: "ai_readonly_command",
|
||||
resourceType: "host",
|
||||
resourceId: String(hostId),
|
||||
details: JSON.stringify({ command }),
|
||||
success: !result.error,
|
||||
errorMessage: result.error,
|
||||
})
|
||||
.catch(() => undefined);
|
||||
if (result.error) return { error: result.error };
|
||||
return { output: (result.output ?? "").slice(0, MAX_COMMAND_OUTPUT) };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_hosts",
|
||||
description:
|
||||
|
||||
@@ -5,7 +5,7 @@ export type ToolCategory = "read" | "propose";
|
||||
/** What tools and the proposal executor reach core and other plugins through. */
|
||||
export type ToolDeps = Pick<
|
||||
PluginContext,
|
||||
"hosts" | "services" | "notify" | "rbac" | "ssh"
|
||||
"hosts" | "services" | "notify" | "rbac" | "ssh" | "audit"
|
||||
>;
|
||||
|
||||
export interface ToolContext {
|
||||
@@ -26,6 +26,8 @@ export interface AiTool {
|
||||
* not offered to the model at all.
|
||||
*/
|
||||
service?: string;
|
||||
/** Offered only to a user who turned on read-only commands. */
|
||||
requiresReadOnlyCommands?: boolean;
|
||||
/** JSON Schema for the arguments, sent to the provider verbatim. */
|
||||
parameters: Record<string, unknown>;
|
||||
/**
|
||||
|
||||
@@ -184,18 +184,11 @@ export function activate(app: TermixApp): void {
|
||||
applyStatus();
|
||||
refresh();
|
||||
// The admin switch and the user's own choice are settings, saved from
|
||||
// the settings screen, so re-read when plugin state or focus changes too.
|
||||
let lastFocusRefresh = Date.now();
|
||||
const onFocus = () => {
|
||||
if (Date.now() - lastFocusRefresh < 30_000) return;
|
||||
lastFocusRefresh = Date.now();
|
||||
refresh();
|
||||
};
|
||||
// the settings screen.
|
||||
app.onSettingsChanged(() => refresh());
|
||||
const events = [AI_STATUS_CHANGED_EVENT, "termix:plugins-changed"];
|
||||
for (const event of events) window.addEventListener(event, refresh);
|
||||
window.addEventListener("focus", onFocus);
|
||||
app.onDispose(() => {
|
||||
for (const event of events) window.removeEventListener(event, refresh);
|
||||
window.removeEventListener("focus", onFocus);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user