mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 16:08:12 +00:00
Telegram: exec approvals for OpenCode/Codex (#37233)
Merged via squash.
Prepared head SHA: f243379094
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
This commit is contained in:
@@ -531,6 +531,19 @@ describe("exec approval handlers", () => {
|
||||
expect(broadcasts.some((entry) => entry.event === "exec.approval.resolved")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not reuse a resolved exact id as a prefix for another pending approval", () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const resolvedRecord = manager.create({ command: "echo old", host: "gateway" }, 2_000, "abc");
|
||||
void manager.register(resolvedRecord, 2_000);
|
||||
expect(manager.resolve("abc", "allow-once")).toBe(true);
|
||||
|
||||
const pendingRecord = manager.create({ command: "echo new", host: "gateway" }, 2_000, "abcdef");
|
||||
void manager.register(pendingRecord, 2_000);
|
||||
|
||||
expect(manager.lookupPendingId("abc")).toEqual({ kind: "none" });
|
||||
expect(manager.lookupPendingId("abcdef")).toEqual({ kind: "exact", id: "abcdef" });
|
||||
});
|
||||
|
||||
it("stores versioned system.run binding and sorted env keys on approval request", async () => {
|
||||
const { handlers, broadcasts, respond, context } = createExecApprovalFixture();
|
||||
await requestExecApproval({
|
||||
@@ -666,6 +679,134 @@ describe("exec approval handlers", () => {
|
||||
expect(resolveRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
});
|
||||
|
||||
it("accepts unique short approval id prefixes", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const respond = vi.fn();
|
||||
const context = {
|
||||
broadcast: (_event: string, _payload: unknown) => {},
|
||||
};
|
||||
|
||||
const record = manager.create({ command: "echo ok" }, 60_000, "approval-12345678-aaaa");
|
||||
void manager.register(record, 60_000);
|
||||
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id: "approval-1234",
|
||||
respond,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
expect(manager.getSnapshot(record.id)?.decision).toBe("allow-once");
|
||||
});
|
||||
|
||||
it("rejects ambiguous short approval id prefixes", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const respond = vi.fn();
|
||||
const context = {
|
||||
broadcast: (_event: string, _payload: unknown) => {},
|
||||
};
|
||||
|
||||
void manager.register(
|
||||
manager.create({ command: "echo one" }, 60_000, "approval-abcd-1111"),
|
||||
60_000,
|
||||
);
|
||||
void manager.register(
|
||||
manager.create({ command: "echo two" }, 60_000, "approval-abcd-2222"),
|
||||
60_000,
|
||||
);
|
||||
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id: "approval-abcd",
|
||||
respond,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("ambiguous approval id prefix"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns deterministic unknown/expired message for missing approval ids", async () => {
|
||||
const { handlers, respond, context } = createExecApprovalFixture();
|
||||
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id: "missing-approval-id",
|
||||
respond,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
message: "unknown or expired approval id",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves only the targeted approval id when multiple requests are pending", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const context = {
|
||||
broadcast: (_event: string, _payload: unknown) => {},
|
||||
hasExecApprovalClients: () => true,
|
||||
};
|
||||
const respondOne = vi.fn();
|
||||
const respondTwo = vi.fn();
|
||||
|
||||
const requestOne = requestExecApproval({
|
||||
handlers,
|
||||
respond: respondOne,
|
||||
context,
|
||||
params: { id: "approval-one", host: "gateway", timeoutMs: 60_000 },
|
||||
});
|
||||
const requestTwo = requestExecApproval({
|
||||
handlers,
|
||||
respond: respondTwo,
|
||||
context,
|
||||
params: { id: "approval-two", host: "gateway", timeoutMs: 60_000 },
|
||||
});
|
||||
|
||||
await drainApprovalRequestTicks();
|
||||
|
||||
const resolveRespond = vi.fn();
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id: "approval-one",
|
||||
respond: resolveRespond,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(resolveRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
expect(manager.getSnapshot("approval-one")?.decision).toBe("allow-once");
|
||||
expect(manager.getSnapshot("approval-two")?.decision).toBeUndefined();
|
||||
expect(manager.getSnapshot("approval-two")?.resolvedAtMs).toBeUndefined();
|
||||
|
||||
expect(manager.expire("approval-two", "test-expire")).toBe(true);
|
||||
await requestOne;
|
||||
await requestTwo;
|
||||
|
||||
expect(respondOne).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ id: "approval-one", decision: "allow-once" }),
|
||||
undefined,
|
||||
);
|
||||
expect(respondTwo).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ id: "approval-two", decision: null }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards turn-source metadata to exec approval forwarding", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
@@ -703,32 +844,59 @@ describe("exec approval handlers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("expires immediately when no approver clients and no forwarding targets", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { manager, handlers, forwarder, respond, context } =
|
||||
createForwardingExecApprovalFixture();
|
||||
const expireSpy = vi.spyOn(manager, "expire");
|
||||
it("fast-fails approvals when no approver clients and no forwarding targets", async () => {
|
||||
const { manager, handlers, forwarder, respond, context } =
|
||||
createForwardingExecApprovalFixture();
|
||||
const expireSpy = vi.spyOn(manager, "expire");
|
||||
|
||||
const requestPromise = requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context,
|
||||
params: { timeoutMs: 60_000 },
|
||||
});
|
||||
await drainApprovalRequestTicks();
|
||||
expect(forwarder.handleRequested).toHaveBeenCalledTimes(1);
|
||||
expect(expireSpy).toHaveBeenCalledTimes(1);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await requestPromise;
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ decision: null }),
|
||||
undefined,
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
await requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context,
|
||||
params: { timeoutMs: 60_000, id: "approval-no-approver", host: "gateway" },
|
||||
});
|
||||
|
||||
expect(forwarder.handleRequested).toHaveBeenCalledTimes(1);
|
||||
expect(expireSpy).toHaveBeenCalledWith("approval-no-approver", "no-approval-route");
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ id: "approval-no-approver", decision: null }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps approvals pending when no approver clients but forwarding accepted the request", async () => {
|
||||
const { manager, handlers, forwarder, respond, context } =
|
||||
createForwardingExecApprovalFixture();
|
||||
const expireSpy = vi.spyOn(manager, "expire");
|
||||
const resolveRespond = vi.fn();
|
||||
forwarder.handleRequested.mockResolvedValueOnce(true);
|
||||
|
||||
const requestPromise = requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context,
|
||||
params: { timeoutMs: 60_000, id: "approval-forwarded", host: "gateway" },
|
||||
});
|
||||
await drainApprovalRequestTicks();
|
||||
|
||||
expect(forwarder.handleRequested).toHaveBeenCalledTimes(1);
|
||||
expect(expireSpy).not.toHaveBeenCalled();
|
||||
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id: "approval-forwarded",
|
||||
respond: resolveRespond,
|
||||
context,
|
||||
});
|
||||
await requestPromise;
|
||||
|
||||
expect(resolveRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ id: "approval-forwarded", decision: "allow-once" }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user