feat: add exec approvals editor in control ui and mac app
This commit is contained in:
@@ -56,6 +56,11 @@ import {
|
||||
CronStatusParamsSchema,
|
||||
type CronUpdateParams,
|
||||
CronUpdateParamsSchema,
|
||||
type ExecApprovalsGetParams,
|
||||
ExecApprovalsGetParamsSchema,
|
||||
type ExecApprovalsSetParams,
|
||||
ExecApprovalsSetParamsSchema,
|
||||
type ExecApprovalsSnapshot,
|
||||
ErrorCodes,
|
||||
type ErrorShape,
|
||||
ErrorShapeSchema,
|
||||
@@ -230,6 +235,12 @@ export const validateCronUpdateParams = ajv.compile<CronUpdateParams>(CronUpdate
|
||||
export const validateCronRemoveParams = ajv.compile<CronRemoveParams>(CronRemoveParamsSchema);
|
||||
export const validateCronRunParams = ajv.compile<CronRunParams>(CronRunParamsSchema);
|
||||
export const validateCronRunsParams = ajv.compile<CronRunsParams>(CronRunsParamsSchema);
|
||||
export const validateExecApprovalsGetParams = ajv.compile<ExecApprovalsGetParams>(
|
||||
ExecApprovalsGetParamsSchema,
|
||||
);
|
||||
export const validateExecApprovalsSetParams = ajv.compile<ExecApprovalsSetParams>(
|
||||
ExecApprovalsSetParamsSchema,
|
||||
);
|
||||
export const validateLogsTailParams = ajv.compile<LogsTailParams>(LogsTailParamsSchema);
|
||||
export const validateChatHistoryParams = ajv.compile(ChatHistoryParamsSchema);
|
||||
export const validateChatSendParams = ajv.compile(ChatSendParamsSchema);
|
||||
@@ -388,6 +399,9 @@ export type {
|
||||
CronRunParams,
|
||||
CronRunsParams,
|
||||
CronRunLogEntry,
|
||||
ExecApprovalsGetParams,
|
||||
ExecApprovalsSetParams,
|
||||
ExecApprovalsSnapshot,
|
||||
LogsTailParams,
|
||||
LogsTailResult,
|
||||
PollParams,
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./schema/channels.js";
|
||||
export * from "./schema/config.js";
|
||||
export * from "./schema/cron.js";
|
||||
export * from "./schema/error-codes.js";
|
||||
export * from "./schema/exec-approvals.js";
|
||||
export * from "./schema/frames.js";
|
||||
export * from "./schema/logs-chat.js";
|
||||
export * from "./schema/nodes.js";
|
||||
|
||||
72
src/gateway/protocol/schema/exec-approvals.ts
Normal file
72
src/gateway/protocol/schema/exec-approvals.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
export const ExecApprovalsAllowlistEntrySchema = Type.Object(
|
||||
{
|
||||
pattern: Type.String(),
|
||||
lastUsedAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastUsedCommand: Type.Optional(Type.String()),
|
||||
lastResolvedPath: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ExecApprovalsDefaultsSchema = Type.Object(
|
||||
{
|
||||
security: Type.Optional(Type.String()),
|
||||
ask: Type.Optional(Type.String()),
|
||||
askFallback: Type.Optional(Type.String()),
|
||||
autoAllowSkills: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ExecApprovalsAgentSchema = Type.Object(
|
||||
{
|
||||
security: Type.Optional(Type.String()),
|
||||
ask: Type.Optional(Type.String()),
|
||||
askFallback: Type.Optional(Type.String()),
|
||||
autoAllowSkills: Type.Optional(Type.Boolean()),
|
||||
allowlist: Type.Optional(Type.Array(ExecApprovalsAllowlistEntrySchema)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ExecApprovalsFileSchema = Type.Object(
|
||||
{
|
||||
version: Type.Literal(1),
|
||||
socket: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
path: Type.Optional(Type.String()),
|
||||
token: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
defaults: Type.Optional(ExecApprovalsDefaultsSchema),
|
||||
agents: Type.Optional(Type.Record(Type.String(), ExecApprovalsAgentSchema)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ExecApprovalsSnapshotSchema = Type.Object(
|
||||
{
|
||||
path: NonEmptyString,
|
||||
exists: Type.Boolean(),
|
||||
hash: NonEmptyString,
|
||||
file: ExecApprovalsFileSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ExecApprovalsGetParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
export const ExecApprovalsSetParamsSchema = Type.Object(
|
||||
{
|
||||
file: ExecApprovalsFileSchema,
|
||||
baseHash: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
@@ -47,6 +47,11 @@ import {
|
||||
CronStatusParamsSchema,
|
||||
CronUpdateParamsSchema,
|
||||
} from "./cron.js";
|
||||
import {
|
||||
ExecApprovalsGetParamsSchema,
|
||||
ExecApprovalsSetParamsSchema,
|
||||
ExecApprovalsSnapshotSchema,
|
||||
} from "./exec-approvals.js";
|
||||
import {
|
||||
ConnectParamsSchema,
|
||||
ErrorShapeSchema,
|
||||
@@ -170,6 +175,9 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
CronRunLogEntry: CronRunLogEntrySchema,
|
||||
LogsTailParams: LogsTailParamsSchema,
|
||||
LogsTailResult: LogsTailResultSchema,
|
||||
ExecApprovalsGetParams: ExecApprovalsGetParamsSchema,
|
||||
ExecApprovalsSetParams: ExecApprovalsSetParamsSchema,
|
||||
ExecApprovalsSnapshot: ExecApprovalsSnapshotSchema,
|
||||
ChatHistoryParams: ChatHistoryParamsSchema,
|
||||
ChatSendParams: ChatSendParamsSchema,
|
||||
ChatAbortParams: ChatAbortParamsSchema,
|
||||
|
||||
@@ -45,6 +45,11 @@ import type {
|
||||
CronStatusParamsSchema,
|
||||
CronUpdateParamsSchema,
|
||||
} from "./cron.js";
|
||||
import type {
|
||||
ExecApprovalsGetParamsSchema,
|
||||
ExecApprovalsSetParamsSchema,
|
||||
ExecApprovalsSnapshotSchema,
|
||||
} from "./exec-approvals.js";
|
||||
import type {
|
||||
ConnectParamsSchema,
|
||||
ErrorShapeSchema,
|
||||
@@ -163,6 +168,9 @@ export type CronRunsParams = Static<typeof CronRunsParamsSchema>;
|
||||
export type CronRunLogEntry = Static<typeof CronRunLogEntrySchema>;
|
||||
export type LogsTailParams = Static<typeof LogsTailParamsSchema>;
|
||||
export type LogsTailResult = Static<typeof LogsTailResultSchema>;
|
||||
export type ExecApprovalsGetParams = Static<typeof ExecApprovalsGetParamsSchema>;
|
||||
export type ExecApprovalsSetParams = Static<typeof ExecApprovalsSetParamsSchema>;
|
||||
export type ExecApprovalsSnapshot = Static<typeof ExecApprovalsSnapshotSchema>;
|
||||
export type ChatAbortParams = Static<typeof ChatAbortParamsSchema>;
|
||||
export type ChatInjectParams = Static<typeof ChatInjectParamsSchema>;
|
||||
export type ChatEvent = Static<typeof ChatEventSchema>;
|
||||
|
||||
@@ -12,6 +12,8 @@ const BASE_METHODS = [
|
||||
"config.apply",
|
||||
"config.patch",
|
||||
"config.schema",
|
||||
"exec.approvals.get",
|
||||
"exec.approvals.set",
|
||||
"wizard.start",
|
||||
"wizard.next",
|
||||
"wizard.cancel",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { chatHandlers } from "./server-methods/chat.js";
|
||||
import { configHandlers } from "./server-methods/config.js";
|
||||
import { connectHandlers } from "./server-methods/connect.js";
|
||||
import { cronHandlers } from "./server-methods/cron.js";
|
||||
import { execApprovalsHandlers } from "./server-methods/exec-approvals.js";
|
||||
import { healthHandlers } from "./server-methods/health.js";
|
||||
import { logsHandlers } from "./server-methods/logs.js";
|
||||
import { modelsHandlers } from "./server-methods/models.js";
|
||||
@@ -30,6 +31,7 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
|
||||
...channelsHandlers,
|
||||
...chatHandlers,
|
||||
...cronHandlers,
|
||||
...execApprovalsHandlers,
|
||||
...webHandlers,
|
||||
...modelsHandlers,
|
||||
...configHandlers,
|
||||
|
||||
157
src/gateway/server-methods/exec-approvals.ts
Normal file
157
src/gateway/server-methods/exec-approvals.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
ensureExecApprovals,
|
||||
normalizeExecApprovals,
|
||||
readExecApprovalsSnapshot,
|
||||
resolveExecApprovalsSocketPath,
|
||||
saveExecApprovals,
|
||||
type ExecApprovalsFile,
|
||||
type ExecApprovalsSnapshot,
|
||||
} from "../../infra/exec-approvals.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateExecApprovalsGetParams,
|
||||
validateExecApprovalsSetParams,
|
||||
} from "../protocol/index.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
|
||||
function resolveBaseHash(params: unknown): string | null {
|
||||
const raw = (params as { baseHash?: unknown })?.baseHash;
|
||||
if (typeof raw !== "string") return null;
|
||||
const trimmed = raw.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function requireApprovalsBaseHash(
|
||||
params: unknown,
|
||||
snapshot: ExecApprovalsSnapshot,
|
||||
respond: RespondFn,
|
||||
): boolean {
|
||||
if (!snapshot.exists) return true;
|
||||
if (!snapshot.hash) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"exec approvals base hash unavailable; re-run exec.approvals.get and retry",
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const baseHash = resolveBaseHash(params);
|
||||
if (!baseHash) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"exec approvals base hash required; re-run exec.approvals.get and retry",
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (baseHash !== snapshot.hash) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"exec approvals changed since last load; re-run exec.approvals.get and retry",
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function redactExecApprovals(file: ExecApprovalsFile): ExecApprovalsFile {
|
||||
const socketPath = file.socket?.path?.trim();
|
||||
return {
|
||||
...file,
|
||||
socket: socketPath ? { path: socketPath } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const execApprovalsHandlers: GatewayRequestHandlers = {
|
||||
"exec.approvals.get": ({ params, respond }) => {
|
||||
if (!validateExecApprovalsGetParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid exec.approvals.get params: ${formatValidationErrors(validateExecApprovalsGetParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ensureExecApprovals();
|
||||
const snapshot = readExecApprovalsSnapshot();
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
path: snapshot.path,
|
||||
exists: snapshot.exists,
|
||||
hash: snapshot.hash,
|
||||
file: redactExecApprovals(snapshot.file),
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
"exec.approvals.set": ({ params, respond }) => {
|
||||
if (!validateExecApprovalsSetParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid exec.approvals.set params: ${formatValidationErrors(validateExecApprovalsSetParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ensureExecApprovals();
|
||||
const snapshot = readExecApprovalsSnapshot();
|
||||
if (!requireApprovalsBaseHash(params, snapshot, respond)) {
|
||||
return;
|
||||
}
|
||||
const incoming = (params as { file?: unknown }).file;
|
||||
if (!incoming || typeof incoming !== "object") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "exec approvals file is required"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeExecApprovals(incoming as ExecApprovalsFile);
|
||||
const currentSocketPath = snapshot.file.socket?.path?.trim();
|
||||
const currentToken = snapshot.file.socket?.token?.trim();
|
||||
const socketPath =
|
||||
normalized.socket?.path?.trim() ??
|
||||
currentSocketPath ??
|
||||
resolveExecApprovalsSocketPath();
|
||||
const token = normalized.socket?.token?.trim() ?? currentToken ?? "";
|
||||
const next: ExecApprovalsFile = {
|
||||
...normalized,
|
||||
socket: {
|
||||
path: socketPath,
|
||||
token,
|
||||
},
|
||||
};
|
||||
saveExecApprovals(next);
|
||||
const nextSnapshot = readExecApprovalsSnapshot();
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
path: nextSnapshot.path,
|
||||
exists: nextSnapshot.exists,
|
||||
hash: nextSnapshot.hash,
|
||||
file: redactExecApprovals(nextSnapshot.file),
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user