mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-18 04:01:03 +00:00
refactor: isolate exec approval followup handoff
This commit is contained in:
@@ -3,16 +3,30 @@ import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type { ExecElevatedDefaults } from "./bash-tools.exec-types.js";
|
||||
|
||||
const EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_PREFIX = "exec-approval-followup:";
|
||||
const EXEC_APPROVAL_FOLLOWUP_ELEVATED_TOKEN_MARKER = ":elevated:";
|
||||
const EXEC_APPROVAL_FOLLOWUP_ELEVATED_TTL_MS = 5 * 60 * 1000;
|
||||
const EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_NONCE_MARKER = ":nonce:";
|
||||
const EXEC_APPROVAL_FOLLOWUP_RUNTIME_HANDOFF_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
type ExecApprovalFollowupElevatedEntry = {
|
||||
export type ExecApprovalFollowupRuntimeHandoff = {
|
||||
kind: "exec-approval-followup";
|
||||
approvalId: string;
|
||||
sessionKey: string;
|
||||
idempotencyKey: string;
|
||||
bashElevated: ExecElevatedDefaults;
|
||||
};
|
||||
|
||||
export type ExecApprovalFollowupRuntimeHandoffRegistration = {
|
||||
handoffId: string;
|
||||
idempotencyKey: string;
|
||||
};
|
||||
|
||||
type ExecApprovalFollowupRuntimeHandoffEntry = ExecApprovalFollowupRuntimeHandoff & {
|
||||
expiresAtMs: number;
|
||||
};
|
||||
|
||||
const execApprovalFollowupElevatedDefaults = new Map<string, ExecApprovalFollowupElevatedEntry>();
|
||||
const execApprovalFollowupRuntimeHandoffs = new Map<
|
||||
string,
|
||||
ExecApprovalFollowupRuntimeHandoffEntry
|
||||
>();
|
||||
|
||||
function cloneExecElevatedDefaults(value: ExecElevatedDefaults): ExecElevatedDefaults {
|
||||
return {
|
||||
@@ -28,96 +42,109 @@ function cloneExecElevatedDefaults(value: ExecElevatedDefaults): ExecElevatedDef
|
||||
};
|
||||
}
|
||||
|
||||
function pruneExpiredExecApprovalFollowupElevatedDefaults(nowMs: number): void {
|
||||
for (const [token, entry] of execApprovalFollowupElevatedDefaults) {
|
||||
function cloneExecApprovalFollowupRuntimeHandoff(
|
||||
value: ExecApprovalFollowupRuntimeHandoff,
|
||||
): ExecApprovalFollowupRuntimeHandoff {
|
||||
return {
|
||||
kind: value.kind,
|
||||
approvalId: value.approvalId,
|
||||
sessionKey: value.sessionKey,
|
||||
idempotencyKey: value.idempotencyKey,
|
||||
bashElevated: cloneExecElevatedDefaults(value.bashElevated),
|
||||
};
|
||||
}
|
||||
|
||||
function pruneExpiredExecApprovalFollowupRuntimeHandoffs(nowMs: number): void {
|
||||
for (const [handoffId, entry] of execApprovalFollowupRuntimeHandoffs) {
|
||||
if (entry.expiresAtMs <= nowMs) {
|
||||
execApprovalFollowupElevatedDefaults.delete(token);
|
||||
execApprovalFollowupRuntimeHandoffs.delete(handoffId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildExecApprovalFollowupIdempotencyKey(params: {
|
||||
approvalId: string;
|
||||
execApprovalFollowupToken?: string;
|
||||
nonce?: string;
|
||||
}): string {
|
||||
const base = `${EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_PREFIX}${params.approvalId}`;
|
||||
return params.execApprovalFollowupToken
|
||||
? `${base}${EXEC_APPROVAL_FOLLOWUP_ELEVATED_TOKEN_MARKER}${params.execApprovalFollowupToken}`
|
||||
: base;
|
||||
const nonce = normalizeOptionalString(params.nonce);
|
||||
return nonce ? `${base}${EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_NONCE_MARKER}${nonce}` : base;
|
||||
}
|
||||
|
||||
function parseExecApprovalFollowupToken(idempotencyKey: string): string | undefined {
|
||||
if (!idempotencyKey.startsWith(EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_PREFIX)) {
|
||||
export function parseExecApprovalFollowupApprovalId(idempotencyKey: string): string | undefined {
|
||||
const normalized = normalizeOptionalString(idempotencyKey);
|
||||
if (!normalized?.startsWith(EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_PREFIX)) {
|
||||
return undefined;
|
||||
}
|
||||
const tokenMarker = idempotencyKey.lastIndexOf(EXEC_APPROVAL_FOLLOWUP_ELEVATED_TOKEN_MARKER);
|
||||
if (tokenMarker < EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_PREFIX.length) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeOptionalString(
|
||||
idempotencyKey.slice(tokenMarker + EXEC_APPROVAL_FOLLOWUP_ELEVATED_TOKEN_MARKER.length),
|
||||
);
|
||||
const body = normalized.slice(EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_PREFIX.length);
|
||||
const nonceMarker = body.lastIndexOf(EXEC_APPROVAL_FOLLOWUP_IDEMPOTENCY_NONCE_MARKER);
|
||||
return normalizeOptionalString(nonceMarker >= 0 ? body.slice(0, nonceMarker) : body);
|
||||
}
|
||||
|
||||
export function registerExecApprovalFollowupElevatedDefaults(params: {
|
||||
export function registerExecApprovalFollowupRuntimeHandoff(params: {
|
||||
approvalId: string;
|
||||
sessionKey: string;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
nowMs?: number;
|
||||
}): string | undefined {
|
||||
}): ExecApprovalFollowupRuntimeHandoffRegistration | undefined {
|
||||
const approvalId = normalizeOptionalString(params.approvalId);
|
||||
const sessionKey = normalizeOptionalString(params.sessionKey);
|
||||
if (!params.bashElevated || !sessionKey) {
|
||||
if (!approvalId || !sessionKey || !params.bashElevated) {
|
||||
return undefined;
|
||||
}
|
||||
const nowMs = params.nowMs ?? Date.now();
|
||||
pruneExpiredExecApprovalFollowupElevatedDefaults(nowMs);
|
||||
const token = randomUUID();
|
||||
execApprovalFollowupElevatedDefaults.set(token, {
|
||||
sessionKey,
|
||||
bashElevated: cloneExecElevatedDefaults(params.bashElevated),
|
||||
expiresAtMs: nowMs + EXEC_APPROVAL_FOLLOWUP_ELEVATED_TTL_MS,
|
||||
pruneExpiredExecApprovalFollowupRuntimeHandoffs(nowMs);
|
||||
const handoffId = randomUUID();
|
||||
const idempotencyKey = buildExecApprovalFollowupIdempotencyKey({
|
||||
approvalId,
|
||||
nonce: randomUUID(),
|
||||
});
|
||||
return token;
|
||||
execApprovalFollowupRuntimeHandoffs.set(handoffId, {
|
||||
kind: "exec-approval-followup",
|
||||
approvalId,
|
||||
sessionKey,
|
||||
idempotencyKey,
|
||||
bashElevated: cloneExecElevatedDefaults(params.bashElevated),
|
||||
expiresAtMs: nowMs + EXEC_APPROVAL_FOLLOWUP_RUNTIME_HANDOFF_TTL_MS,
|
||||
});
|
||||
return { handoffId, idempotencyKey };
|
||||
}
|
||||
|
||||
export function consumeExecApprovalFollowupElevatedDefaults(params: {
|
||||
token?: string;
|
||||
export function consumeExecApprovalFollowupRuntimeHandoff(params: {
|
||||
handoffId?: string;
|
||||
approvalId?: string;
|
||||
idempotencyKey?: string;
|
||||
sessionKey?: string;
|
||||
nowMs?: number;
|
||||
}): ExecElevatedDefaults | undefined {
|
||||
const token = normalizeOptionalString(params.token);
|
||||
if (!token) {
|
||||
}): ExecApprovalFollowupRuntimeHandoff | undefined {
|
||||
const handoffId = normalizeOptionalString(params.handoffId);
|
||||
const approvalId = normalizeOptionalString(params.approvalId);
|
||||
const idempotencyKey = normalizeOptionalString(params.idempotencyKey);
|
||||
if (!handoffId || !approvalId || !idempotencyKey) {
|
||||
return undefined;
|
||||
}
|
||||
const nowMs = params.nowMs ?? Date.now();
|
||||
pruneExpiredExecApprovalFollowupElevatedDefaults(nowMs);
|
||||
const entry = execApprovalFollowupElevatedDefaults.get(token);
|
||||
pruneExpiredExecApprovalFollowupRuntimeHandoffs(nowMs);
|
||||
const entry = execApprovalFollowupRuntimeHandoffs.get(handoffId);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
if (entry.expiresAtMs <= nowMs) {
|
||||
execApprovalFollowupElevatedDefaults.delete(token);
|
||||
execApprovalFollowupRuntimeHandoffs.delete(handoffId);
|
||||
return undefined;
|
||||
}
|
||||
const sessionKey = normalizeOptionalString(params.sessionKey);
|
||||
if (entry.sessionKey !== sessionKey) {
|
||||
if (
|
||||
entry.approvalId !== approvalId ||
|
||||
entry.idempotencyKey !== idempotencyKey ||
|
||||
entry.sessionKey !== sessionKey
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
execApprovalFollowupElevatedDefaults.delete(token);
|
||||
return cloneExecElevatedDefaults(entry.bashElevated);
|
||||
execApprovalFollowupRuntimeHandoffs.delete(handoffId);
|
||||
return cloneExecApprovalFollowupRuntimeHandoff(entry);
|
||||
}
|
||||
|
||||
export function consumeExecApprovalFollowupElevatedDefaultsFromIdempotencyKey(params: {
|
||||
idempotencyKey: string;
|
||||
sessionKey?: string;
|
||||
nowMs?: number;
|
||||
}): ExecElevatedDefaults | undefined {
|
||||
return consumeExecApprovalFollowupElevatedDefaults({
|
||||
token: parseExecApprovalFollowupToken(params.idempotencyKey),
|
||||
sessionKey: params.sessionKey,
|
||||
nowMs: params.nowMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetExecApprovalFollowupElevatedDefaultsForTests(): void {
|
||||
execApprovalFollowupElevatedDefaults.clear();
|
||||
export function resetExecApprovalFollowupRuntimeHandoffsForTests(): void {
|
||||
execApprovalFollowupRuntimeHandoffs.clear();
|
||||
}
|
||||
|
||||
@@ -283,13 +283,14 @@ describe("exec approval followup", () => {
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("carries the elevated followup token through idempotency without exposing elevated defaults", async () => {
|
||||
it("carries the runtime handoff separately from idempotency without exposing elevated defaults", async () => {
|
||||
await sendExecApprovalFollowup({
|
||||
approvalId: "req-elevated-75832",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
turnSourceChannel: "telegram",
|
||||
resultText: "Exec finished (gateway id=req-elevated-75832, code 0)\nok",
|
||||
execApprovalFollowupToken: "token-75832",
|
||||
internalRuntimeHandoffId: "handoff-75832",
|
||||
idempotencyKey: "exec-approval-followup:req-elevated-75832:nonce:nonce-75832",
|
||||
});
|
||||
|
||||
expect(callGatewayTool).toHaveBeenCalledWith(
|
||||
@@ -298,7 +299,8 @@ describe("exec approval followup", () => {
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
channel: "telegram",
|
||||
idempotencyKey: "exec-approval-followup:req-elevated-75832:elevated:token-75832",
|
||||
idempotencyKey: "exec-approval-followup:req-elevated-75832:nonce:nonce-75832",
|
||||
internalRuntimeHandoffId: "handoff-75832",
|
||||
}),
|
||||
{ expectFinal: true },
|
||||
);
|
||||
|
||||
@@ -24,7 +24,8 @@ type ExecApprovalFollowupParams = {
|
||||
turnSourceThreadId?: string | number;
|
||||
resultText: string;
|
||||
direct?: boolean;
|
||||
execApprovalFollowupToken?: string;
|
||||
internalRuntimeHandoffId?: string;
|
||||
idempotencyKey?: string;
|
||||
};
|
||||
|
||||
function buildExecDeniedFollowupPrompt(resultText: string): string {
|
||||
@@ -151,7 +152,8 @@ function buildAgentFollowupArgs(params: {
|
||||
turnSourceTo?: string;
|
||||
turnSourceAccountId?: string;
|
||||
turnSourceThreadId?: string | number;
|
||||
execApprovalFollowupToken?: string;
|
||||
internalRuntimeHandoffId?: string;
|
||||
idempotencyKey?: string;
|
||||
}) {
|
||||
const { deliveryTarget, sessionOnlyOriginChannel } = params;
|
||||
// When the followup run has no deliverable route and no gateway-internal channel,
|
||||
@@ -179,10 +181,14 @@ function buildAgentFollowupArgs(params: {
|
||||
: sessionOnlyOriginChannel
|
||||
? params.turnSourceThreadId
|
||||
: undefined,
|
||||
idempotencyKey: buildExecApprovalFollowupIdempotencyKey({
|
||||
approvalId: params.approvalId,
|
||||
execApprovalFollowupToken: params.execApprovalFollowupToken,
|
||||
}),
|
||||
idempotencyKey:
|
||||
params.idempotencyKey ??
|
||||
buildExecApprovalFollowupIdempotencyKey({
|
||||
approvalId: params.approvalId,
|
||||
}),
|
||||
...(params.internalRuntimeHandoffId
|
||||
? { internalRuntimeHandoffId: params.internalRuntimeHandoffId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,7 +262,8 @@ export async function sendExecApprovalFollowup(
|
||||
turnSourceTo: params.turnSourceTo,
|
||||
turnSourceAccountId: params.turnSourceAccountId,
|
||||
turnSourceThreadId: params.turnSourceThreadId,
|
||||
execApprovalFollowupToken: params.execApprovalFollowupToken,
|
||||
internalRuntimeHandoffId: params.internalRuntimeHandoffId,
|
||||
idempotencyKey: params.idempotencyKey,
|
||||
}),
|
||||
{ expectFinal: true },
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
consumeExecApprovalFollowupElevatedDefaults,
|
||||
resetExecApprovalFollowupElevatedDefaultsForTests,
|
||||
consumeExecApprovalFollowupRuntimeHandoff,
|
||||
resetExecApprovalFollowupRuntimeHandoffsForTests,
|
||||
} from "./bash-tools.exec-approval-followup-state.js";
|
||||
import {
|
||||
buildExecApprovalPendingToolResult,
|
||||
@@ -63,7 +63,7 @@ describe("sendExecApprovalFollowupResult", () => {
|
||||
allowlist: [],
|
||||
file: { version: 1, agents: {} },
|
||||
});
|
||||
resetExecApprovalFollowupElevatedDefaultsForTests();
|
||||
resetExecApprovalFollowupRuntimeHandoffsForTests();
|
||||
});
|
||||
|
||||
it("logs repeated followup dispatch failures once per approval id and error message", async () => {
|
||||
@@ -132,22 +132,65 @@ describe("sendExecApprovalFollowupResult", () => {
|
||||
);
|
||||
|
||||
const call = sendExecApprovalFollowup.mock.calls[0]?.[0] as
|
||||
| { execApprovalFollowupToken?: string; bashElevated?: unknown }
|
||||
| {
|
||||
internalRuntimeHandoffId?: string;
|
||||
idempotencyKey?: string;
|
||||
execApprovalFollowupToken?: string;
|
||||
bashElevated?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
expect(call?.execApprovalFollowupToken).toEqual(expect.any(String));
|
||||
expect(call?.internalRuntimeHandoffId).toEqual(expect.any(String));
|
||||
expect(call?.idempotencyKey).toMatch(/^exec-approval-followup:approval-elevated-75832:nonce:/);
|
||||
expect(call?.idempotencyKey).not.toContain(call?.internalRuntimeHandoffId ?? "");
|
||||
expect(call).not.toHaveProperty("bashElevated");
|
||||
expect(call).not.toHaveProperty("execApprovalFollowupToken");
|
||||
expect(
|
||||
consumeExecApprovalFollowupElevatedDefaults({
|
||||
token: call?.execApprovalFollowupToken ?? "",
|
||||
consumeExecApprovalFollowupRuntimeHandoff({
|
||||
handoffId: call?.internalRuntimeHandoffId ?? "",
|
||||
approvalId: "approval-elevated-75832",
|
||||
idempotencyKey: call?.idempotencyKey ?? "",
|
||||
sessionKey: "agent:main:telegram:direct:wrong",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
consumeExecApprovalFollowupElevatedDefaults({
|
||||
token: call?.execApprovalFollowupToken ?? "",
|
||||
consumeExecApprovalFollowupRuntimeHandoff({
|
||||
handoffId: call?.internalRuntimeHandoffId ?? "",
|
||||
approvalId: "approval-elevated-75832",
|
||||
idempotencyKey: call?.idempotencyKey ?? "",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
}),
|
||||
).toEqual(bashElevated);
|
||||
).toEqual({
|
||||
kind: "exec-approval-followup",
|
||||
approvalId: "approval-elevated-75832",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
idempotencyKey: call?.idempotencyKey,
|
||||
bashElevated,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps non-elevated agent followups on the deterministic idempotency path", async () => {
|
||||
sendExecApprovalFollowup.mockResolvedValue(true);
|
||||
|
||||
await sendExecApprovalFollowupResult(
|
||||
{
|
||||
approvalId: "approval-normal-75832",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
turnSourceChannel: "telegram",
|
||||
},
|
||||
"Exec finished",
|
||||
{ sendExecApprovalFollowup, logWarn },
|
||||
);
|
||||
|
||||
const call = sendExecApprovalFollowup.mock.calls[0]?.[0] as
|
||||
| {
|
||||
internalRuntimeHandoffId?: string;
|
||||
idempotencyKey?: string;
|
||||
bashElevated?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
expect(call).not.toHaveProperty("internalRuntimeHandoffId");
|
||||
expect(call).not.toHaveProperty("idempotencyKey");
|
||||
expect(call).not.toHaveProperty("bashElevated");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type ExecSecurity,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { registerExecApprovalFollowupElevatedDefaults } from "./bash-tools.exec-approval-followup-state.js";
|
||||
import { registerExecApprovalFollowupRuntimeHandoff } from "./bash-tools.exec-approval-followup-state.js";
|
||||
import { sendExecApprovalFollowup } from "./bash-tools.exec-approval-followup.js";
|
||||
import {
|
||||
type ExecApprovalRegistration,
|
||||
@@ -411,10 +411,11 @@ export async function sendExecApprovalFollowupResult(
|
||||
): Promise<void> {
|
||||
const send = deps.sendExecApprovalFollowup ?? sendExecApprovalFollowup;
|
||||
const warn = deps.logWarn ?? logWarn;
|
||||
const execApprovalFollowupToken =
|
||||
const runtimeHandoff =
|
||||
target.direct === true || !target.sessionKey
|
||||
? undefined
|
||||
: registerExecApprovalFollowupElevatedDefaults({
|
||||
: registerExecApprovalFollowupRuntimeHandoff({
|
||||
approvalId: target.approvalId,
|
||||
sessionKey: target.sessionKey,
|
||||
bashElevated: target.bashElevated,
|
||||
});
|
||||
@@ -427,7 +428,12 @@ export async function sendExecApprovalFollowupResult(
|
||||
turnSourceThreadId: target.turnSourceThreadId,
|
||||
resultText,
|
||||
direct: target.direct,
|
||||
execApprovalFollowupToken,
|
||||
...(runtimeHandoff
|
||||
? {
|
||||
internalRuntimeHandoffId: runtimeHandoff.handoffId,
|
||||
idempotencyKey: runtimeHandoff.idempotencyKey,
|
||||
}
|
||||
: {}),
|
||||
}).catch((error) => {
|
||||
const message = formatErrorMessage(error);
|
||||
const key = `${target.approvalId}:${message}`;
|
||||
|
||||
@@ -169,6 +169,7 @@ export const AgentParamsSchema = Type.Object(
|
||||
Type.Union([Type.Literal("default"), Type.Literal("heartbeat"), Type.Literal("cron")]),
|
||||
),
|
||||
acpTurnSource: Type.Optional(Type.Literal("manual_spawn")),
|
||||
internalRuntimeHandoffId: Type.Optional(NonEmptyString),
|
||||
internalEvents: Type.Optional(Type.Array(AgentInternalEventSchema)),
|
||||
inputProvenance: Type.Optional(InputProvenanceSchema),
|
||||
voiceWakeTrigger: Type.Optional(Type.String()),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
registerExecApprovalFollowupElevatedDefaults,
|
||||
resetExecApprovalFollowupElevatedDefaultsForTests,
|
||||
registerExecApprovalFollowupRuntimeHandoff,
|
||||
resetExecApprovalFollowupRuntimeHandoffsForTests,
|
||||
} from "../../agents/bash-tools.exec-approval-followup-state.js";
|
||||
import { BARE_SESSION_RESET_PROMPT } from "../../auto-reply/reply/session-reset-prompt.js";
|
||||
import {
|
||||
@@ -360,6 +360,22 @@ function readLastAgentCommandCall(): AgentCommandCall | undefined {
|
||||
return mocks.agentCommand.mock.calls.at(-1)?.[0] as AgentCommandCall | undefined;
|
||||
}
|
||||
|
||||
function backendGatewayClient(): AgentHandlerArgs["client"] {
|
||||
return {
|
||||
connect: {
|
||||
minProtocol: 1,
|
||||
maxProtocol: 1,
|
||||
client: {
|
||||
id: "gateway-client",
|
||||
version: "test",
|
||||
platform: "test",
|
||||
mode: "backend",
|
||||
},
|
||||
scopes: ["operator.write"],
|
||||
},
|
||||
} as AgentHandlerArgs["client"];
|
||||
}
|
||||
|
||||
async function waitForAgentCommandCall<
|
||||
T extends AgentCommandCall = AgentCommandCall,
|
||||
>(): Promise<T> {
|
||||
@@ -458,7 +474,7 @@ describe("gateway agent handler", () => {
|
||||
mocks.resolveSendPolicy.mockReset().mockReturnValue("allow");
|
||||
dateOnlyFakeClockActive = false;
|
||||
vi.useRealTimers();
|
||||
resetExecApprovalFollowupElevatedDefaultsForTests();
|
||||
resetExecApprovalFollowupRuntimeHandoffsForTests();
|
||||
});
|
||||
|
||||
it("preserves ACP metadata from the current stored session entry", async () => {
|
||||
@@ -1592,16 +1608,20 @@ describe("gateway agent handler", () => {
|
||||
expect(callArgs.runContext?.messageChannel).toBe("webchat");
|
||||
});
|
||||
|
||||
it("forwards elevated defaults only for valid exec approval followup tokens", async () => {
|
||||
it("forwards elevated defaults only for valid exec approval runtime handoffs", async () => {
|
||||
const bashElevated = {
|
||||
enabled: true,
|
||||
allowed: true,
|
||||
defaultLevel: "on" as const,
|
||||
};
|
||||
const token = registerExecApprovalFollowupElevatedDefaults({
|
||||
const registration = registerExecApprovalFollowupRuntimeHandoff({
|
||||
approvalId: "req-elevated-75832",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
bashElevated,
|
||||
});
|
||||
if (!registration) {
|
||||
throw new Error("expected runtime handoff id");
|
||||
}
|
||||
mockMainSessionEntry({
|
||||
sessionId: "existing-session-id",
|
||||
lastChannel: "telegram",
|
||||
@@ -1617,16 +1637,59 @@ describe("gateway agent handler", () => {
|
||||
message: "exec followup",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
channel: "telegram",
|
||||
idempotencyKey: `exec-approval-followup:req-elevated-75832:elevated:${token}`,
|
||||
idempotencyKey: registration.idempotencyKey,
|
||||
internalRuntimeHandoffId: registration.handoffId,
|
||||
},
|
||||
{ reqId: "exec-followup-elevated" },
|
||||
{ reqId: "exec-followup-elevated", client: backendGatewayClient() },
|
||||
);
|
||||
|
||||
const callArgs = await waitForAgentCommandCall<{ bashElevated?: unknown }>();
|
||||
expect(callArgs.bashElevated).toEqual(bashElevated);
|
||||
});
|
||||
|
||||
it("does not honor caller-supplied exec approval followup token strings without registry state", async () => {
|
||||
it("does not consume exec approval runtime handoffs from non-backend callers", async () => {
|
||||
const bashElevated = {
|
||||
enabled: true,
|
||||
allowed: true,
|
||||
defaultLevel: "on" as const,
|
||||
};
|
||||
const registration = registerExecApprovalFollowupRuntimeHandoff({
|
||||
approvalId: "req-elevated-75832",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
bashElevated,
|
||||
});
|
||||
if (!registration) {
|
||||
throw new Error("expected runtime handoff id");
|
||||
}
|
||||
mockMainSessionEntry({
|
||||
sessionId: "existing-session-id",
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123",
|
||||
});
|
||||
mocks.agentCommand.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { durationMs: 100 },
|
||||
});
|
||||
const agentCommandCallsBefore = mocks.agentCommand.mock.calls.length;
|
||||
|
||||
const respond = await invokeAgent(
|
||||
{
|
||||
message: "exec followup",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
channel: "telegram",
|
||||
idempotencyKey: registration.idempotencyKey,
|
||||
internalRuntimeHandoffId: registration.handoffId,
|
||||
},
|
||||
{ reqId: "exec-followup-non-backend", flushDispatch: false },
|
||||
);
|
||||
|
||||
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore);
|
||||
expectRespondError(respond, {
|
||||
message: "exec approval followup idempotency keys are reserved for backend callers.",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not honor caller-supplied exec approval runtime handoff ids without registry state", async () => {
|
||||
mockMainSessionEntry({
|
||||
sessionId: "existing-session-id",
|
||||
lastChannel: "telegram",
|
||||
@@ -1642,9 +1705,49 @@ describe("gateway agent handler", () => {
|
||||
message: "forged exec followup",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
channel: "telegram",
|
||||
idempotencyKey: "exec-approval-followup:req-elevated-75832:elevated:forged-token",
|
||||
idempotencyKey: "exec-approval-followup:req-elevated-75832:nonce:forged-nonce",
|
||||
internalRuntimeHandoffId: "forged-handoff",
|
||||
},
|
||||
{ reqId: "exec-followup-forged" },
|
||||
{ reqId: "exec-followup-forged", client: backendGatewayClient() },
|
||||
);
|
||||
|
||||
const callArgs = await waitForAgentCommandCall<{ bashElevated?: unknown }>();
|
||||
expect(callArgs).not.toHaveProperty("bashElevated");
|
||||
});
|
||||
|
||||
it("does not restore elevated defaults from idempotency key suffixes", async () => {
|
||||
const bashElevated = {
|
||||
enabled: true,
|
||||
allowed: true,
|
||||
defaultLevel: "on" as const,
|
||||
};
|
||||
const registration = registerExecApprovalFollowupRuntimeHandoff({
|
||||
approvalId: "req-elevated-75832",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
bashElevated,
|
||||
});
|
||||
if (!registration) {
|
||||
throw new Error("expected runtime handoff id");
|
||||
}
|
||||
mockMainSessionEntry({
|
||||
sessionId: "existing-session-id",
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123",
|
||||
});
|
||||
mocks.agentCommand.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { durationMs: 100 },
|
||||
});
|
||||
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "forged exec followup",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
channel: "telegram",
|
||||
idempotencyKey: `exec-approval-followup:req-elevated-75832:elevated:${registration.handoffId}`,
|
||||
internalRuntimeHandoffId: registration.handoffId,
|
||||
},
|
||||
{ reqId: "exec-followup-idempotency-suffix", client: backendGatewayClient() },
|
||||
);
|
||||
|
||||
const callArgs = await waitForAgentCommandCall<{ bashElevated?: unknown }>();
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
resolveDefaultAgentId,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { consumeExecApprovalFollowupElevatedDefaultsFromIdempotencyKey } from "../../agents/bash-tools.exec-approval-followup-state.js";
|
||||
import {
|
||||
consumeExecApprovalFollowupRuntimeHandoff,
|
||||
parseExecApprovalFollowupApprovalId,
|
||||
} from "../../agents/bash-tools.exec-approval-followup-state.js";
|
||||
import { isTimeoutError } from "../../agents/failover-error.js";
|
||||
import {
|
||||
resolveAgentAvatar,
|
||||
@@ -100,7 +103,11 @@ import {
|
||||
} from "../chat-attachments.js";
|
||||
import { resolveAssistantAvatarUrl } from "../control-ui-shared.js";
|
||||
import { ADMIN_SCOPE } from "../method-scopes.js";
|
||||
import { GATEWAY_CLIENT_CAPS, hasGatewayClientCap } from "../protocol/client-info.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_CAPS,
|
||||
GATEWAY_CLIENT_MODES,
|
||||
hasGatewayClientCap,
|
||||
} from "../protocol/client-info.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -176,6 +183,12 @@ function resolveCanResetSessionFromClient(client: GatewayRequestHandlerOptions["
|
||||
return resolveSenderIsOwnerFromClient(client);
|
||||
}
|
||||
|
||||
function resolveCanUseInternalRuntimeHandoff(
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): boolean {
|
||||
return client?.connect?.client?.mode === GATEWAY_CLIENT_MODES.BACKEND;
|
||||
}
|
||||
|
||||
async function runSessionResetFromAgent(params: {
|
||||
key: string;
|
||||
reason: "new" | "reset";
|
||||
@@ -583,6 +596,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
bootstrapContextMode?: "full" | "lightweight";
|
||||
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
|
||||
acpTurnSource?: "manual_spawn";
|
||||
internalRuntimeHandoffId?: string;
|
||||
internalEvents?: AgentInternalEvent[];
|
||||
idempotencyKey: string;
|
||||
timeout?: number;
|
||||
@@ -596,6 +610,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
const senderIsOwner = resolveSenderIsOwnerFromClient(client);
|
||||
const allowModelOverride = resolveAllowModelOverrideFromClient(client);
|
||||
const canResetSession = resolveCanResetSessionFromClient(client);
|
||||
const canUseInternalRuntimeHandoff = resolveCanUseInternalRuntimeHandoff(client);
|
||||
const requestedModelOverride = Boolean(request.provider || request.model);
|
||||
const isRawModelRun = request.modelRun === true || request.promptMode === "none";
|
||||
if (requestedModelOverride && !allowModelOverride) {
|
||||
@@ -613,6 +628,18 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
const modelOverride = allowModelOverride ? request.model : undefined;
|
||||
const cfg = context.getRuntimeConfig();
|
||||
const idem = request.idempotencyKey;
|
||||
const execApprovalFollowupApprovalId = parseExecApprovalFollowupApprovalId(idem);
|
||||
if (execApprovalFollowupApprovalId && !canUseInternalRuntimeHandoff) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"exec approval followup idempotency keys are reserved for backend callers.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const normalizedSpawned = normalizeSpawnedRunMetadata({
|
||||
groupId: request.groupId,
|
||||
groupChannel: request.groupChannel,
|
||||
@@ -1392,22 +1419,31 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
(!resolvedSessionKey || resolveAgentIdFromSessionKey(resolvedSessionKey) === agentId)
|
||||
? agentId
|
||||
: undefined;
|
||||
let execApprovalFollowupElevatedDefaults =
|
||||
consumeExecApprovalFollowupElevatedDefaultsFromIdempotencyKey({
|
||||
idempotencyKey: idem,
|
||||
sessionKey: resolvedSessionKey,
|
||||
});
|
||||
let execApprovalFollowupRuntimeHandoff =
|
||||
canUseInternalRuntimeHandoff && execApprovalFollowupApprovalId
|
||||
? consumeExecApprovalFollowupRuntimeHandoff({
|
||||
handoffId: request.internalRuntimeHandoffId,
|
||||
approvalId: execApprovalFollowupApprovalId,
|
||||
idempotencyKey: idem,
|
||||
sessionKey: resolvedSessionKey,
|
||||
})
|
||||
: undefined;
|
||||
if (
|
||||
!execApprovalFollowupElevatedDefaults &&
|
||||
!execApprovalFollowupRuntimeHandoff &&
|
||||
canUseInternalRuntimeHandoff &&
|
||||
execApprovalFollowupApprovalId &&
|
||||
requestedSessionKeyRaw &&
|
||||
requestedSessionKeyRaw !== resolvedSessionKey
|
||||
) {
|
||||
execApprovalFollowupElevatedDefaults =
|
||||
consumeExecApprovalFollowupElevatedDefaultsFromIdempotencyKey({
|
||||
idempotencyKey: idem,
|
||||
sessionKey: requestedSessionKeyRaw,
|
||||
});
|
||||
execApprovalFollowupRuntimeHandoff = consumeExecApprovalFollowupRuntimeHandoff({
|
||||
handoffId: request.internalRuntimeHandoffId,
|
||||
approvalId: execApprovalFollowupApprovalId,
|
||||
idempotencyKey: idem,
|
||||
sessionKey: requestedSessionKeyRaw,
|
||||
});
|
||||
}
|
||||
const execApprovalFollowupElevatedDefaults =
|
||||
execApprovalFollowupRuntimeHandoff?.bashElevated;
|
||||
|
||||
dispatchAgentRunFromGateway({
|
||||
ingressOpts: {
|
||||
|
||||
Reference in New Issue
Block a user