mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
73a9eed95b
* feat(audit): carry canonical admitted execution context * fix(agents): preserve admitted context across retries * fix(worker): fence legacy launch dialect * test(gateway): track approval temp dirs * fix(plugin-sdk): preserve harness attempt compatibility * fix: close delegated run authority at owner boundaries * fix: internalize delegated authority validators * refactor: split delegated authority proof surfaces * refactor: centralize command admission identity * test: claim runtime tool authority * fix(gateway): keep lifecycle cleanup within static budgets * fix(agents): revalidate harness policy authority * fix(agents): fence awaited approval capability results * test(copilot): supply required harness capability fixtures * fix(agent): preserve scoped embedded run admission * fix(agent): preserve keyless and worker authority * test(agent): bind incomplete-turn authority * docs: preserve execution authority invariants * chore(plugin-sdk): regenerate API baseline * fix(gateway): notify pending claim closure * fix(gateway): revalidate delegated tool authority * fix(plugin-sdk): keep source guard internal * fix: close delegated authority races * fix: revalidate delegated side effects * fix: close harness authority projection gaps * fix: align authority integration types * fix: isolate settled harness finalization * fix: fence recovery identity finalization * fix: preserve committed session worktrees * fix: preserve worker placement agent identity * fix: fence active harness tool work * fix(plugins): restore embedded run admission owner * chore(plugin-sdk): compose integrated surface budgets * fix(copilot): keep finalization attempt type internal * fix(plugins): complete admission owner type imports * test(harness): use settled finalization attempt shape * fix(security): retain exact side-run and approval authority * fix(security): preserve protected authority through terminal sweep * fix(agents): follow moved recovery store owner * fix(ci): align integrated authority owners with gates * fix(plugins): distinguish embedded agent adapter export * chore(plugin-sdk): regenerate API baseline after rolling integration * refactor(gateway): keep session authority within owner budgets * fix(gateway): keep session helpers private * docs(plugin-sdk): name the V2 parameter subpath * chore(integration): reconcile worker and SDK surfaces * docs(plugin-sdk): require the V2 host API floor * chore(plugin-sdk): regenerate after proxy-auth integration
152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
import type { SessionConfig } from "@github/copilot-sdk";
|
|
import {
|
|
callGatewayTool,
|
|
embeddedAgentLog,
|
|
runAgentHarnessGatewayQuestion,
|
|
type AgentHarnessQuestionGatewayCall,
|
|
type AgentHarnessUserInputQuestion,
|
|
type EmbeddedRunAttemptParamsV2,
|
|
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
|
|
type CopilotUserInputHandler = NonNullable<SessionConfig["onUserInputRequest"]>;
|
|
type CopilotUserInputRequest = Parameters<CopilotUserInputHandler>[0];
|
|
type CopilotUserInputResponse = Awaited<ReturnType<CopilotUserInputHandler>>;
|
|
|
|
export type CopilotUserInputBridge = {
|
|
onUserInputRequest: CopilotUserInputHandler;
|
|
cancelPending: () => void;
|
|
};
|
|
|
|
const COPILOT_USER_INPUT_QUESTION_ID = "answer";
|
|
const DEFAULT_USER_INPUT_TIMEOUT_MS = 15 * 60_000;
|
|
|
|
export function createCopilotUserInputBridge(params: {
|
|
paramsForRun: EmbeddedRunAttemptParamsV2;
|
|
signal?: AbortSignal;
|
|
gatewayCall?: AgentHarnessQuestionGatewayCall;
|
|
}): CopilotUserInputBridge {
|
|
let pending: AbortController | undefined;
|
|
const gatewayCall = params.gatewayCall ?? callGatewayTool;
|
|
|
|
return {
|
|
async onUserInputRequest(request) {
|
|
pending?.abort(new Error("Copilot user input request replaced"));
|
|
const abort = new AbortController();
|
|
pending = abort;
|
|
const abortFromRun = () => abort.abort(params.signal?.reason);
|
|
params.signal?.addEventListener("abort", abortFromRun, { once: true });
|
|
if (params.signal?.aborted) {
|
|
abortFromRun();
|
|
}
|
|
try {
|
|
const question = toQuestion(request);
|
|
const result = await runAgentHarnessGatewayQuestion({
|
|
questions: [question],
|
|
sessionKey: params.paramsForRun.sessionKey ?? params.paramsForRun.sessionId,
|
|
agentId: params.paramsForRun.agentId,
|
|
runId: params.paramsForRun.runId,
|
|
timeoutMs: params.paramsForRun.timeoutMs ?? DEFAULT_USER_INPUT_TIMEOUT_MS,
|
|
gatewayCall,
|
|
delivery: params.paramsForRun,
|
|
promptOptions: {
|
|
intro: "Copilot needs input:",
|
|
formatText: formatCopilotDisplayText,
|
|
},
|
|
signal: abort.signal,
|
|
});
|
|
if (result.status !== "answered") {
|
|
return emptyCopilotUserInputResponse();
|
|
}
|
|
const selected = result.answers.answers[COPILOT_USER_INPUT_QUESTION_ID]?.[0] ?? "";
|
|
return {
|
|
answer: selected,
|
|
wasFreeform: !isChoiceAnswer(question, selected),
|
|
};
|
|
} catch (error) {
|
|
embeddedAgentLog.warn("failed to bridge copilot user input through gateway", { error });
|
|
return emptyCopilotUserInputResponse();
|
|
} finally {
|
|
params.signal?.removeEventListener("abort", abortFromRun);
|
|
if (pending === abort) {
|
|
pending = undefined;
|
|
}
|
|
}
|
|
},
|
|
cancelPending() {
|
|
pending?.abort(new Error("Copilot user input request cancelled"));
|
|
},
|
|
};
|
|
}
|
|
|
|
function toQuestion(request: CopilotUserInputRequest): AgentHarnessUserInputQuestion {
|
|
return {
|
|
id: COPILOT_USER_INPUT_QUESTION_ID,
|
|
header: "Copilot",
|
|
question: request.question,
|
|
isOther: request.allowFreeform !== false,
|
|
isSecret: false,
|
|
options:
|
|
request.choices && request.choices.length > 0
|
|
? request.choices.map((choice: string) => ({ label: choice }))
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function emptyCopilotUserInputResponse(): CopilotUserInputResponse {
|
|
return { answer: "", wasFreeform: true };
|
|
}
|
|
|
|
function isChoiceAnswer(question: AgentHarnessUserInputQuestion, answer: string): boolean {
|
|
return Boolean(
|
|
answer &&
|
|
question.options?.some((option) => option.label.toLowerCase() === answer.toLowerCase()),
|
|
);
|
|
}
|
|
|
|
function formatCopilotDisplayText(value: string): string {
|
|
const safe = sanitizeCopilotDisplayText(value).trim();
|
|
return escapeCopilotChatText(safe || "<unknown>");
|
|
}
|
|
|
|
function sanitizeCopilotDisplayText(value: string): string {
|
|
let safe = "";
|
|
for (const character of value) {
|
|
const codePoint = character.codePointAt(0);
|
|
safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
|
|
}
|
|
return safe;
|
|
}
|
|
|
|
function escapeCopilotChatText(value: string): string {
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll("@", "\uff20")
|
|
.replaceAll("`", "\uff40")
|
|
.replaceAll("[", "\uff3b")
|
|
.replaceAll("]", "\uff3d")
|
|
.replaceAll("(", "\uff08")
|
|
.replaceAll(")", "\uff09")
|
|
.replaceAll("*", "\u2217")
|
|
.replaceAll("_", "\uff3f")
|
|
.replaceAll("~", "\uff5e")
|
|
.replaceAll("|", "\uff5c");
|
|
}
|
|
|
|
function isUnsafeDisplayCodePoint(codePoint: number): boolean {
|
|
return (
|
|
codePoint <= 0x001f ||
|
|
(codePoint >= 0x007f && codePoint <= 0x009f) ||
|
|
codePoint === 0x00ad ||
|
|
codePoint === 0x061c ||
|
|
codePoint === 0x180e ||
|
|
(codePoint >= 0x200b && codePoint <= 0x200f) ||
|
|
(codePoint >= 0x202a && codePoint <= 0x202e) ||
|
|
(codePoint >= 0x2060 && codePoint <= 0x206f) ||
|
|
codePoint === 0xfeff ||
|
|
(codePoint >= 0xfff9 && codePoint <= 0xfffb) ||
|
|
(codePoint >= 0xe0000 && codePoint <= 0xe007f)
|
|
);
|
|
}
|