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:
Harold Hunt
2026-03-09 23:04:35 -04:00
committed by GitHub
parent 9432a8bb3f
commit de49a8b72c
78 changed files with 4058 additions and 524 deletions

View File

@@ -19,14 +19,6 @@ export function createExecApprovalHandlers(
manager: ExecApprovalManager,
opts?: { forwarder?: ExecApprovalForwarder },
): GatewayRequestHandlers {
const hasApprovalClients = (context: { hasExecApprovalClients?: () => boolean }) => {
if (typeof context.hasExecApprovalClients === "function") {
return context.hasExecApprovalClients();
}
// Fail closed when no operator-scope probe is available.
return false;
};
return {
"exec.approval.request": async ({ params, respond, context, client }) => {
if (!validateExecApprovalRequestParams(params)) {
@@ -178,10 +170,11 @@ export function createExecApprovalHandlers(
},
{ dropIfSlow: true },
);
let forwardedToTargets = false;
const hasExecApprovalClients = context.hasExecApprovalClients?.() ?? false;
let forwarded = false;
if (opts?.forwarder) {
try {
forwardedToTargets = await opts.forwarder.handleRequested({
forwarded = await opts.forwarder.handleRequested({
id: record.id,
request: record.request,
createdAtMs: record.createdAtMs,
@@ -192,8 +185,19 @@ export function createExecApprovalHandlers(
}
}
if (!hasApprovalClients(context) && !forwardedToTargets) {
manager.expire(record.id, "auto-expire:no-approver-clients");
if (!hasExecApprovalClients && !forwarded) {
manager.expire(record.id, "no-approval-route");
respond(
true,
{
id: record.id,
decision: null,
createdAtMs: record.createdAtMs,
expiresAtMs: record.expiresAtMs,
},
undefined,
);
return;
}
// Only send immediate "accepted" response when twoPhase is requested.
@@ -275,21 +279,48 @@ export function createExecApprovalHandlers(
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid decision"));
return;
}
const snapshot = manager.getSnapshot(p.id);
const resolvedId = manager.lookupPendingId(p.id);
if (resolvedId.kind === "none") {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id"),
);
return;
}
if (resolvedId.kind === "ambiguous") {
const candidates = resolvedId.ids.slice(0, 3).join(", ");
const remainder = resolvedId.ids.length > 3 ? ` (+${resolvedId.ids.length - 3} more)` : "";
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`ambiguous approval id prefix; matches: ${candidates}${remainder}. Use the full id.`,
),
);
return;
}
const approvalId = resolvedId.id;
const snapshot = manager.getSnapshot(approvalId);
const resolvedBy = client?.connect?.client?.displayName ?? client?.connect?.client?.id;
const ok = manager.resolve(p.id, decision, resolvedBy ?? null);
const ok = manager.resolve(approvalId, decision, resolvedBy ?? null);
if (!ok) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown approval id"));
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id"),
);
return;
}
context.broadcast(
"exec.approval.resolved",
{ id: p.id, decision, resolvedBy, ts: Date.now(), request: snapshot?.request },
{ id: approvalId, decision, resolvedBy, ts: Date.now(), request: snapshot?.request },
{ dropIfSlow: true },
);
void opts?.forwarder
?.handleResolved({
id: p.id,
id: approvalId,
decision,
resolvedBy,
ts: Date.now(),

View File

@@ -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,
);
});
});