mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-26 01:51:11 +00:00
* feat(browser): add copilot security contracts * fix(gateway): expose verified client identity to handlers * feat(browser): add secure per-tab copilot panel Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * refactor(browser): separate copilot gateway hint custody * fix(browser): preserve legacy pairing parse shape * fix(browser): harden copilot lifecycle custody Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * fix(browser): enforce copilot lifecycle boundaries Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * style(browser): format copilot sources Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * fix(browser): preserve copilot consent revocation Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * refactor(browser): split copilot custody owners Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * test(browser): normalize websocket array buffers Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * chore(protocol): regenerate Swift gateway models Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * refactor(browser): model copilot runtime entrypoints Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * fix(browser): honor extension build boundaries Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * test(gateway): assert targeted chat delivery Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * test(gateway): cover targeted delivery calls Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * fix(browser): declare copilot build dependencies Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * fix(ci): clear browser copilot gate failures Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * test(ci): cover copilot lint exclusion Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * fix(browser): gate copilot on relay custody Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> * test(browser): bound copilot relay frames Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com> --------- Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com>
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
type BrowserTabToolBinding = {
|
|
kind: "tab";
|
|
tabId: number;
|
|
target: "host" | "node";
|
|
node?: string;
|
|
profile: string;
|
|
targetId: string;
|
|
};
|
|
|
|
type BindingResult = { ok: true; binding: BrowserTabToolBinding } | { ok: false; error: string };
|
|
|
|
function nonEmptyString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
}
|
|
|
|
/** Validate the plugin-owned run binding before any browser route is resolved. */
|
|
export function parseBrowserTabToolBinding(value: unknown): BindingResult {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return { ok: false, error: "browser tool binding must be an object" };
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
const target = record.target === "host" || record.target === "node" ? record.target : undefined;
|
|
const node = nonEmptyString(record.node);
|
|
const profile = nonEmptyString(record.profile);
|
|
const targetId = nonEmptyString(record.targetId);
|
|
if (record.kind !== "tab") {
|
|
return { ok: false, error: 'browser tool binding kind must be "tab"' };
|
|
}
|
|
if (!Number.isSafeInteger(record.tabId) || Number(record.tabId) < 0) {
|
|
return { ok: false, error: "browser tool binding tabId must be a non-negative integer" };
|
|
}
|
|
if (!target || !profile || !targetId || (target === "node" && !node)) {
|
|
return { ok: false, error: "browser tool binding requires target, profile, and targetId" };
|
|
}
|
|
if (target === "host" && node) {
|
|
return { ok: false, error: "browser host binding cannot include node" };
|
|
}
|
|
return {
|
|
ok: true,
|
|
binding: {
|
|
kind: "tab",
|
|
tabId: Number(record.tabId),
|
|
target,
|
|
...(node ? { node } : {}),
|
|
profile,
|
|
targetId,
|
|
},
|
|
};
|
|
}
|
|
|
|
const TAB_BOUND_ACTIONS = new Set([
|
|
"act",
|
|
"close",
|
|
"console",
|
|
"dialog",
|
|
"download",
|
|
"focus",
|
|
"navigate",
|
|
"pdf",
|
|
"screenshot",
|
|
"snapshot",
|
|
"tabs",
|
|
"upload",
|
|
"waitfordownload",
|
|
]);
|
|
|
|
function bindTargetId(record: Record<string, unknown>, targetId: string): Record<string, unknown> {
|
|
const requestedTargetId = nonEmptyString(record.targetId);
|
|
if (requestedTargetId && requestedTargetId !== targetId) {
|
|
throw new Error("browser action cannot override its run-bound tab target");
|
|
}
|
|
const actions = Array.isArray(record.actions)
|
|
? record.actions.map((action) =>
|
|
action && typeof action === "object" && !Array.isArray(action)
|
|
? bindTargetId(action as Record<string, unknown>, targetId)
|
|
: action,
|
|
)
|
|
: record.actions;
|
|
return { ...record, targetId, ...(actions ? { actions } : {}) };
|
|
}
|
|
|
|
/** Pin model-supplied browser arguments to the trusted tab route for this run. */
|
|
export function applyBrowserTabToolBinding(
|
|
input: Record<string, unknown>,
|
|
binding: BrowserTabToolBinding,
|
|
): Record<string, unknown> {
|
|
const action = nonEmptyString(input.action);
|
|
if (!action || !TAB_BOUND_ACTIONS.has(action)) {
|
|
throw new Error(`browser action ${JSON.stringify(action)} is unavailable in a tab-bound run`);
|
|
}
|
|
const requestedTarget = nonEmptyString(input.target);
|
|
const requestedNode = nonEmptyString(input.node);
|
|
const requestedProfile = nonEmptyString(input.profile);
|
|
if (requestedTarget && requestedTarget !== binding.target) {
|
|
throw new Error("browser action cannot override its run-bound target");
|
|
}
|
|
if (requestedNode && requestedNode !== binding.node) {
|
|
throw new Error("browser action cannot override its run-bound node");
|
|
}
|
|
if (requestedProfile && requestedProfile !== binding.profile) {
|
|
throw new Error("browser action cannot override its run-bound profile");
|
|
}
|
|
const bound = bindTargetId(input, binding.targetId);
|
|
const request =
|
|
bound.request && typeof bound.request === "object" && !Array.isArray(bound.request)
|
|
? bindTargetId(bound.request as Record<string, unknown>, binding.targetId)
|
|
: bound.request;
|
|
return {
|
|
...bound,
|
|
target: binding.target,
|
|
...(binding.node ? { node: binding.node } : {}),
|
|
profile: binding.profile,
|
|
...(request ? { request } : {}),
|
|
};
|
|
}
|