feat: add exec approvals allowlists

This commit is contained in:
Peter Steinberger
2026-01-18 01:33:52 +00:00
parent 3a0fd6be3c
commit 0674f1fa3c
21 changed files with 1019 additions and 101 deletions

View File

@@ -74,7 +74,10 @@ export function createClawdbotTools(options?: {
allowedControlPorts: options?.allowedControlPorts,
}),
createCanvasTool(),
createNodesTool(),
createNodesTool({
agentSessionKey: options?.agentSessionKey,
config: options?.config,
}),
createCronTool({
agentSessionKey: options?.agentSessionKey,
}),

View File

@@ -17,12 +17,14 @@ import {
writeScreenRecordToFile,
} from "../../cli/nodes-screen.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { imageMimeFromFormat } from "../../media/mime.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
import { sanitizeToolResultImages } from "../tool-images.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
import { resolveNodeId } from "./nodes-utils.js";
import { listNodes, resolveNodeIdFromList, resolveNodeId } from "./nodes-utils.js";
const NODES_TOOL_ACTIONS = [
"status",
@@ -86,7 +88,14 @@ const NodesToolSchema = Type.Object({
needsScreenRecording: Type.Optional(Type.Boolean()),
});
export function createNodesTool(): AnyAgentTool {
export function createNodesTool(options?: {
agentSessionKey?: string;
config?: ClawdbotConfig;
}): AnyAgentTool {
const agentId = resolveSessionAgentId({
sessionKey: options?.agentSessionKey,
config: options?.config,
});
return {
label: "Nodes",
name: "nodes",
@@ -375,7 +384,22 @@ export function createNodesTool(): AnyAgentTool {
}
case "run": {
const node = readStringParam(params, "node", { required: true });
const nodeId = await resolveNodeId(gatewayOpts, node);
const nodes = await listNodes(gatewayOpts);
if (nodes.length === 0) {
throw new Error(
"system.run requires a paired macOS companion app (no nodes available).",
);
}
const nodeId = resolveNodeIdFromList(nodes, node);
const nodeInfo = nodes.find((entry) => entry.nodeId === nodeId);
const supportsSystemRun = Array.isArray(nodeInfo?.commands)
? nodeInfo?.commands?.includes("system.run")
: false;
if (!supportsSystemRun) {
throw new Error(
"system.run requires the macOS companion app; the selected node does not support system.run.",
);
}
const commandRaw = params.command;
if (!commandRaw) {
throw new Error("command required (argv array, e.g. ['echo', 'Hello'])");
@@ -405,6 +429,7 @@ export function createNodesTool(): AnyAgentTool {
env,
timeoutMs: commandTimeoutMs,
needsScreenRecording,
agentId,
},
timeoutMs: invokeTimeoutMs,
idempotencyKey: crypto.randomUUID(),

View File

@@ -1,6 +1,6 @@
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
type NodeListNode = {
export type NodeListNode = {
nodeId: string;
displayName?: string;
platform?: string;
@@ -99,12 +99,15 @@ function pickDefaultNode(nodes: NodeListNode[]): NodeListNode | null {
return null;
}
export async function resolveNodeId(
opts: GatewayCallOptions,
export async function listNodes(opts: GatewayCallOptions): Promise<NodeListNode[]> {
return loadNodes(opts);
}
export function resolveNodeIdFromList(
nodes: NodeListNode[],
query?: string,
allowDefault = false,
) {
const nodes = await loadNodes(opts);
): string {
const q = String(query ?? "").trim();
if (!q) {
if (allowDefault) {
@@ -138,3 +141,12 @@ export async function resolveNodeId(
.join(", ")})`,
);
}
export async function resolveNodeId(
opts: GatewayCallOptions,
query?: string,
allowDefault = false,
) {
const nodes = await loadNodes(opts);
return resolveNodeIdFromList(nodes, query, allowDefault);
}