229805a070
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
203 lines
5.5 KiB
JavaScript
203 lines
5.5 KiB
JavaScript
import path from 'node:path';
|
|
import { developerToolsFromPolicy } from './capabilities.mjs';
|
|
import { buildSessionMemoryEntries } from './user-memory-profile.mjs';
|
|
import { buildSandboxSessionConstraints } from './user-publish.mjs';
|
|
|
|
function extensionName(config) {
|
|
return config?.name ?? null;
|
|
}
|
|
|
|
export function allowedExtensionNames(extensionOverrides) {
|
|
if (!extensionOverrides) return null;
|
|
return new Set(extensionOverrides.map((item) => item.name).filter(Boolean));
|
|
}
|
|
|
|
export function extensionToolsKey(config) {
|
|
const tools = config?.available_tools ?? config?.availableTools ?? [];
|
|
return [...tools].sort().join('\0');
|
|
}
|
|
|
|
export function extensionConfigsMatch(sessionExt, desiredConfig) {
|
|
if (extensionName(sessionExt) !== extensionName(desiredConfig)) return false;
|
|
return extensionToolsKey(sessionExt) === extensionToolsKey(desiredConfig);
|
|
}
|
|
|
|
export function extensionsNeedingRefresh(currentExtensions, desiredExtensions) {
|
|
const current = currentExtensions ?? [];
|
|
const desired = desiredExtensions ?? [];
|
|
const toRemove = [];
|
|
const toAdd = [];
|
|
|
|
for (const ext of current) {
|
|
const name = extensionName(ext);
|
|
if (!name) continue;
|
|
const wanted = desired.find((item) => extensionName(item) === name);
|
|
if (!wanted) {
|
|
toRemove.push(name);
|
|
continue;
|
|
}
|
|
if (!extensionConfigsMatch(ext, wanted)) {
|
|
toRemove.push(name);
|
|
toAdd.push(wanted);
|
|
}
|
|
}
|
|
|
|
for (const config of desired) {
|
|
const name = extensionName(config);
|
|
if (!name) continue;
|
|
const exists = current.some((ext) => extensionName(ext) === name);
|
|
if (!exists) {
|
|
toAdd.push(config);
|
|
}
|
|
}
|
|
|
|
return { toRemove, toAdd };
|
|
}
|
|
|
|
function samePath(left, right) {
|
|
if (!left || !right) return false;
|
|
return path.resolve(left) === path.resolve(right);
|
|
}
|
|
|
|
async function readJson(upstream) {
|
|
const text = await upstream.text();
|
|
if (!upstream.ok) {
|
|
throw new Error(text || `upstream ${upstream.status}`);
|
|
}
|
|
return text ? JSON.parse(text) : null;
|
|
}
|
|
|
|
function isInvalidDirectoryPathError(err) {
|
|
return err instanceof Error && /Invalid directory path/i.test(err.message);
|
|
}
|
|
|
|
/**
|
|
* Re-apply H5 session policy after resume so stale extension sets cannot bypass capability limits.
|
|
*/
|
|
export async function reconcileAgentSession(
|
|
apiFetch,
|
|
sessionId,
|
|
{
|
|
workingDir,
|
|
sessionPolicy,
|
|
sandboxConstraints = null,
|
|
userContext = null,
|
|
tolerateInvalidWorkingDir = false,
|
|
},
|
|
) {
|
|
if (sessionPolicy?.unrestricted) return;
|
|
|
|
const session = await readJson(await apiFetch(`/sessions/${sessionId}`));
|
|
if (workingDir && !samePath(session?.working_dir, workingDir)) {
|
|
try {
|
|
await readJson(
|
|
await apiFetch('/agent/update_working_dir', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ session_id: sessionId, working_dir: workingDir }),
|
|
}),
|
|
);
|
|
} catch (err) {
|
|
if (!(tolerateInvalidWorkingDir && isInvalidDirectoryPathError(err))) {
|
|
throw err;
|
|
}
|
|
console.warn(
|
|
`[session-reconcile] skip invalid working_dir during resume for session ${sessionId}: ${
|
|
err.message
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (sessionPolicy?.gooseMode) {
|
|
await readJson(
|
|
await apiFetch('/agent/update_session', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
session_id: sessionId,
|
|
goose_mode: sessionPolicy.gooseMode,
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
|
|
const desired = sessionPolicy?.extensionOverrides ?? [];
|
|
const allowed = allowedExtensionNames(desired);
|
|
const currentPayload = await readJson(await apiFetch(`/sessions/${sessionId}/extensions`));
|
|
const current = currentPayload?.extensions ?? [];
|
|
|
|
for (const ext of current) {
|
|
const name = extensionName(ext);
|
|
if (!name || allowed.has(name)) continue;
|
|
await readJson(
|
|
await apiFetch('/agent/remove_extension', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ session_id: sessionId, name }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
const refreshedPayload = await readJson(await apiFetch(`/sessions/${sessionId}/extensions`));
|
|
const refreshed = refreshedPayload?.extensions ?? [];
|
|
const { toRemove, toAdd } = extensionsNeedingRefresh(refreshed, desired);
|
|
|
|
for (const name of toRemove) {
|
|
await readJson(
|
|
await apiFetch('/agent/remove_extension', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ session_id: sessionId, name }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
for (const config of toAdd) {
|
|
await readJson(
|
|
await apiFetch('/agent/add_extension', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ session_id: sessionId, config }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
await readJson(
|
|
await apiFetch('/agent/restart', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ session_id: sessionId }),
|
|
}),
|
|
);
|
|
|
|
const sandboxText = sandboxConstraints?.trim()
|
|
? buildSandboxSessionConstraints({
|
|
baseConstraints: sandboxConstraints,
|
|
developerTools: developerToolsFromPolicy(sessionPolicy),
|
|
})
|
|
: null;
|
|
|
|
const memoryEntries = buildSessionMemoryEntries({
|
|
workingDir,
|
|
sessionPolicy,
|
|
sandboxConstraints: sandboxText,
|
|
userContext,
|
|
});
|
|
|
|
if (memoryEntries.length > 0) {
|
|
for (const entry of memoryEntries) {
|
|
if (!entry.content?.trim()) continue;
|
|
await apiFetch('/agent/harness_remember', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
sessionId,
|
|
content: entry.content,
|
|
title: entry.title,
|
|
}),
|
|
});
|
|
}
|
|
await apiFetch('/agent/harness_bootstrap', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
sessionId,
|
|
force: true,
|
|
}),
|
|
});
|
|
}
|
|
}
|