fix: unify exec approval ids
This commit is contained in:
@@ -33,11 +33,15 @@ type PendingEntry = {
|
||||
export class ExecApprovalManager {
|
||||
private pending = new Map<string, PendingEntry>();
|
||||
|
||||
create(request: ExecApprovalRequestPayload, timeoutMs: number): ExecApprovalRecord {
|
||||
create(
|
||||
request: ExecApprovalRequestPayload,
|
||||
timeoutMs: number,
|
||||
id?: string | null,
|
||||
): ExecApprovalRecord {
|
||||
const now = Date.now();
|
||||
const id = randomUUID();
|
||||
const resolvedId = id && id.trim().length > 0 ? id.trim() : randomUUID();
|
||||
const record: ExecApprovalRecord = {
|
||||
id,
|
||||
id: resolvedId,
|
||||
request,
|
||||
createdAtMs: now,
|
||||
expiresAtMs: now + timeoutMs,
|
||||
|
||||
@@ -89,6 +89,7 @@ export const ExecApprovalsNodeSetParamsSchema = Type.Object(
|
||||
|
||||
export const ExecApprovalRequestParamsSchema = Type.Object(
|
||||
{
|
||||
id: Type.Optional(NonEmptyString),
|
||||
command: NonEmptyString,
|
||||
cwd: Type.Optional(Type.String()),
|
||||
host: Type.Optional(Type.String()),
|
||||
|
||||
@@ -60,4 +60,120 @@ describe("exec approval handlers", () => {
|
||||
);
|
||||
expect(broadcasts.some((entry) => entry.event === "exec.approval.resolved")).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts explicit approval ids", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const broadcasts: Array<{ event: string; payload: unknown }> = [];
|
||||
|
||||
const respond = vi.fn();
|
||||
const context = {
|
||||
broadcast: (event: string, payload: unknown) => {
|
||||
broadcasts.push({ event, payload });
|
||||
},
|
||||
};
|
||||
|
||||
const requestPromise = handlers["exec.approval.request"]({
|
||||
params: {
|
||||
id: "approval-123",
|
||||
command: "echo ok",
|
||||
cwd: "/tmp",
|
||||
host: "gateway",
|
||||
timeoutMs: 2000,
|
||||
},
|
||||
respond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.request"]
|
||||
>[0]["context"],
|
||||
client: null,
|
||||
req: { id: "req-1", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: noop,
|
||||
});
|
||||
|
||||
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
|
||||
const id = (requested?.payload as { id?: string })?.id ?? "";
|
||||
expect(id).toBe("approval-123");
|
||||
|
||||
const resolveRespond = vi.fn();
|
||||
await handlers["exec.approval.resolve"]({
|
||||
params: { id, decision: "allow-once" },
|
||||
respond: resolveRespond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.resolve"]
|
||||
>[0]["context"],
|
||||
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
|
||||
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
|
||||
isWebchatConnect: noop,
|
||||
});
|
||||
|
||||
await requestPromise;
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ id: "approval-123", decision: "allow-once" }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects duplicate approval ids", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const respondA = vi.fn();
|
||||
const respondB = vi.fn();
|
||||
const broadcasts: Array<{ event: string; payload: unknown }> = [];
|
||||
const context = {
|
||||
broadcast: (event: string, payload: unknown) => {
|
||||
broadcasts.push({ event, payload });
|
||||
},
|
||||
};
|
||||
|
||||
const requestPromise = handlers["exec.approval.request"]({
|
||||
params: {
|
||||
id: "dup-1",
|
||||
command: "echo ok",
|
||||
},
|
||||
respond: respondA,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.request"]
|
||||
>[0]["context"],
|
||||
client: null,
|
||||
req: { id: "req-1", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: noop,
|
||||
});
|
||||
|
||||
await handlers["exec.approval.request"]({
|
||||
params: {
|
||||
id: "dup-1",
|
||||
command: "echo again",
|
||||
},
|
||||
respond: respondB,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.request"]
|
||||
>[0]["context"],
|
||||
client: null,
|
||||
req: { id: "req-2", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: noop,
|
||||
});
|
||||
|
||||
expect(respondB).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "approval id already pending" }),
|
||||
);
|
||||
|
||||
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
|
||||
const id = (requested?.payload as { id?: string })?.id ?? "";
|
||||
const resolveRespond = vi.fn();
|
||||
await handlers["exec.approval.resolve"]({
|
||||
params: { id, decision: "deny" },
|
||||
respond: resolveRespond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.resolve"]
|
||||
>[0]["context"],
|
||||
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
|
||||
req: { id: "req-3", type: "req", method: "exec.approval.resolve" },
|
||||
isWebchatConnect: noop,
|
||||
});
|
||||
|
||||
await requestPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export function createExecApprovalHandlers(manager: ExecApprovalManager): Gatewa
|
||||
return;
|
||||
}
|
||||
const p = params as {
|
||||
id?: string;
|
||||
command: string;
|
||||
cwd?: string;
|
||||
host?: string;
|
||||
@@ -37,6 +38,15 @@ export function createExecApprovalHandlers(manager: ExecApprovalManager): Gatewa
|
||||
timeoutMs?: number;
|
||||
};
|
||||
const timeoutMs = typeof p.timeoutMs === "number" ? p.timeoutMs : 120_000;
|
||||
const explicitId = typeof p.id === "string" && p.id.trim().length > 0 ? p.id.trim() : null;
|
||||
if (explicitId && manager.getSnapshot(explicitId)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "approval id already pending"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const request = {
|
||||
command: p.command,
|
||||
cwd: p.cwd ?? null,
|
||||
@@ -47,7 +57,7 @@ export function createExecApprovalHandlers(manager: ExecApprovalManager): Gatewa
|
||||
resolvedPath: p.resolvedPath ?? null,
|
||||
sessionKey: p.sessionKey ?? null,
|
||||
};
|
||||
const record = manager.create(request, timeoutMs);
|
||||
const record = manager.create(request, timeoutMs, explicitId);
|
||||
context.broadcast(
|
||||
"exec.approval.requested",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user