Files
openclaw/extensions/browser/chrome-extension/modules/copilot-relay-custody.js
T
Peter Steinberger ec8f6e5e03 feat(browser): add secure per-tab copilot panel (#109817)
* 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>
2026-07-18 01:00:23 +01:00

90 lines
2.6 KiB
JavaScript

/** Relay/run boundary owner for the tab-bound copilot. */
export function createCopilotRelayCustodyController({
appendGatewayRevocation,
broadcastStatus,
currentGatewayScope,
drainAborts,
getGatewayStatus,
invalidateGatewayEpoch,
markGatewayAbortError,
registry,
revokeActiveBindings,
runLifecycle,
}) {
let ready = false;
let label = "Connecting to browser relay";
let statusRevision = 0;
let reconciledStatusRevision = 0;
let pendingRevocation = Promise.resolve();
function isOperational() {
return ready && reconciledStatusRevision === statusRevision;
}
function currentPanelStatus() {
const gatewayStatus = getGatewayStatus();
return gatewayStatus.state === "ready" && !isOperational()
? { state: "connecting", label }
: gatewayStatus;
}
async function onStatus(status) {
const nextReady = status.ready === true;
const readinessChanged = ready !== nextReady;
ready = nextReady;
label = status.label || "Browser relay reconnecting";
if (!readinessChanged) {
broadcastStatus();
return;
}
// Relay availability is part of the run epoch. Reconcile debugger/run
// custody before a reconnected tool route can admit panel work again.
invalidateGatewayEpoch();
const revision = ++statusRevision;
if (nextReady) {
broadcastStatus();
await pendingRevocation;
await runLifecycle(async () => {
const gatewayScope = currentGatewayScope();
if (revision === statusRevision && ready && gatewayScope) {
await drainAborts(gatewayScope);
}
});
if (revision !== statusRevision || !ready) {
return;
}
reconciledStatusRevision = revision;
const gatewayScope = currentGatewayScope();
if (
gatewayScope &&
registry.pendingAborts(gatewayScope).length > 0 &&
getGatewayStatus().state === "ready"
) {
markGatewayAbortError();
broadcastStatus();
return;
}
broadcastStatus({ ensureSetup: true, hydrateHistory: true });
return;
}
reconciledStatusRevision = 0;
broadcastStatus();
const gatewayScope = currentGatewayScope();
if (!gatewayScope) {
return;
}
const revocation = revokeActiveBindings(gatewayScope);
appendGatewayRevocation(revocation);
const cleanup = runLifecycle(async () => {
await revocation;
if (gatewayScope === currentGatewayScope() && !ready) {
await drainAborts(gatewayScope);
}
});
pendingRevocation = cleanup.catch(() => undefined);
await pendingRevocation;
}
return { currentPanelStatus, isOperational, onStatus };
}