feat: ship enterprise control and desktop governance

This commit is contained in:
AI Bot
2026-05-11 14:59:26 +08:00
parent 0757d07521
commit a311280238
285 changed files with 48574 additions and 2428 deletions

View File

@@ -1,26 +1,130 @@
import { createHash } from "node:crypto";
import os from "node:os";
import { basename, resolve } from "node:path";
import { basename, dirname, resolve } from "node:path";
import { readFileSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { open, readFile, readdir } from "node:fs/promises";
import { DatabaseSync } from "node:sqlite";
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
const MAX_ROLLOUT_TAIL_BYTES = 768 * 1024;
const MAX_RECENT_ASSISTANT_MESSAGES = 6;
const ASSISTANT_DUPLICATE_TURN_WINDOW_MS = 2_000;
const LEAKED_TITLE_PREFIXES = [
"你当前接手的项目根目录是",
"你现在接手的项目根目录是",
"你现在以目标线程身份直接回复用户",
"你正在向主 Agent 同步当前项目状态",
"只回复对用户真正有用的内容",
"只输出 JSON",
];
const LEAKED_TITLE_CONTAINS = [
"不要发送内部字段",
"不要自称主 Agent",
"不要解释系统如何分发",
"不要输出 JSON",
"项目名称:",
"线程名称:",
"文件夹:",
"同步原因:",
"当前消息:",
"用户当前消息:",
];
function toIsoFromUnixSeconds(value) {
if (!Number.isFinite(value) || value <= 0) return null;
return new Date(value * 1000).toISOString();
}
function sanitizeDisplayName(raw, fallback) {
function normalizeDisplayName(raw) {
const source = typeof raw === "string" ? raw : "";
const firstLine = source
.replace(/\u0000/g, "")
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
if (!firstLine) return fallback;
const compact = firstLine.replace(/\s+/g, " ").trim();
if (!compact) return fallback;
return compact.length > 48 ? `${compact.slice(0, 45)}...` : compact;
if (!firstLine) return "";
return firstLine.replace(/\s+/g, " ").trim();
}
function trimWorkspacePrefix(value) {
const normalized = normalizeDisplayName(value).replaceAll("\\", "/");
if (!normalized) {
return "";
}
return normalized
.replace(/^\/Users\/[^/]+\/code\//i, "")
.replace(/^\/home\/[^/]+\/code\//i, "")
.replace(/^[A-Za-z]:\/Users\/[^/]+\/code\//i, "");
}
function stripTrailingDisplayNameNoise(value) {
return value.replace(/['"}\]]{2,}$/g, "").trimEnd();
}
function looksLikeLeakedDisplayName(value) {
const normalized = normalizeDisplayName(value);
if (!normalized) {
return false;
}
return (
LEAKED_TITLE_PREFIXES.some((marker) => normalized.startsWith(marker)) ||
LEAKED_TITLE_CONTAINS.some((marker) => normalized.includes(marker))
);
}
function extractWorkspaceProjectName(value) {
const normalized = normalizeDisplayName(value).replaceAll("\\", "/");
if (!normalized) {
return "";
}
const patterns = [
/\/Users\/[^/]+\/code\/([^/\s"'`,。;!?]+)/i,
/\/home\/[^/]+\/code\/([^/\s"'`,。;!?]+)/i,
/[A-Za-z]:\/Users\/[^/]+\/code\/([^/\s"'`,。;!?]+)/i,
];
for (const pattern of patterns) {
const match = normalized.match(pattern);
if (match?.[1]) {
return match[1].split("/")[0]?.trim() ?? "";
}
}
return "";
}
function pickDisplayNameFallback(candidates) {
for (const candidate of candidates) {
const extracted = extractWorkspaceProjectName(candidate);
if (extracted && !looksLikeLeakedDisplayName(extracted)) {
return extracted;
}
const normalized = stripTrailingDisplayNameNoise(trimWorkspacePrefix(candidate));
if (normalized && !looksLikeLeakedDisplayName(normalized)) {
return normalized;
}
}
return "";
}
function sanitizeDisplayName(raw, fallback, options = {}) {
const compact = stripTrailingDisplayNameNoise(trimWorkspacePrefix(raw));
if (compact && !looksLikeLeakedDisplayName(raw) && !looksLikeLeakedDisplayName(compact)) {
return compact.length > 48 ? `${compact.slice(0, 45)}...` : compact;
}
const extractedProject = extractWorkspaceProjectName(raw);
if (extractedProject && !looksLikeLeakedDisplayName(extractedProject)) {
return extractedProject;
}
const safeFallback = pickDisplayNameFallback([
options.folderName,
options.folderPath,
fallback,
]);
if (safeFallback) {
return safeFallback;
}
return fallback;
}
function fallbackDisplayName(thread, folderName) {
@@ -119,7 +223,7 @@ function loadThreadsFromStateDb(stateDbPath) {
try {
return db
.prepare(
"SELECT id, cwd, updated_at, archived, title, sandbox_policy, agent_nickname, agent_role FROM threads WHERE archived = 0 ORDER BY updated_at DESC",
"SELECT id, cwd, updated_at, archived, title, sandbox_policy, agent_nickname, agent_role, rollout_path, source FROM threads WHERE archived = 0 ORDER BY updated_at DESC",
)
.all()
.map((row) => ({
@@ -131,6 +235,8 @@ function loadThreadsFromStateDb(stateDbPath) {
sandboxPolicy: typeof row.sandbox_policy === "string" ? row.sandbox_policy : "",
agentNickname: typeof row.agent_nickname === "string" ? row.agent_nickname : "",
agentRole: typeof row.agent_role === "string" ? row.agent_role : "",
rolloutPath: typeof row.rollout_path === "string" ? row.rollout_path : "",
source: typeof row.source === "string" ? row.source : "",
}));
} finally {
db.close();
@@ -154,16 +260,45 @@ function parseSessionMeta(line) {
title: "",
agentNickname: typeof parsed.payload.agent_nickname === "string" ? parsed.payload.agent_nickname : "",
agentRole: typeof parsed.payload.agent_role === "string" ? parsed.payload.agent_role : "",
rolloutPath: "",
source: "sessions",
};
} catch {
return null;
}
}
async function loadThreadsFromSessions(sessionsDir) {
function parseRolloutFilenameTimestampSeconds(fileName) {
const match = fileName.match(
/^rollout-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-/,
);
if (!match) {
return null;
}
const [, datePart, hour, minute, second] = match;
const parsed = new Date(`${datePart}T${hour}:${minute}:${second}`);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return Math.floor(parsed.getTime() / 1000);
}
function shouldReadSessionFile(fileName, cutoffSeconds) {
if (!Number.isFinite(cutoffSeconds)) {
return true;
}
const filenameSeconds = parseRolloutFilenameTimestampSeconds(fileName);
if (filenameSeconds === null) {
return true;
}
return filenameSeconds >= cutoffSeconds;
}
async function loadThreadsFromSessions(sessionsDir, options = {}) {
if (!sessionsDir) return [];
const pending = [resolve(sessionsDir)];
const threads = [];
const cutoffSeconds = Number(options.cutoffSeconds);
while (pending.length > 0) {
const dir = pending.pop();
if (!dir) continue;
@@ -180,11 +315,12 @@ async function loadThreadsFromSessions(sessionsDir) {
continue;
}
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
if (!shouldReadSessionFile(entry.name, cutoffSeconds)) continue;
try {
const raw = await readFile(fullPath, "utf8");
const firstLine = raw.split(/\r?\n/, 1)[0];
const parsed = parseSessionMeta(firstLine);
if (parsed) threads.push(parsed);
if (parsed) threads.push({ ...parsed, rolloutPath: fullPath });
} catch {
continue;
}
@@ -193,10 +329,165 @@ async function loadThreadsFromSessions(sessionsDir) {
return threads;
}
function normalizeEventTimestamp(value) {
if (typeof value !== "string" || !value.trim()) return null;
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return null;
return parsed.toISOString();
}
function buildAssistantMessageId(threadId, sentAt, body) {
const digest = createHash("sha1").update(body).digest("hex").slice(0, 12);
return `codex-thread:${threadId}:${sentAt}:${digest}`;
}
function assistantMessageTimeValue(value) {
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
function assistantPhasePriority(value) {
const phase = normalizeAssistantMessagePhase(value);
if (!phase) return 0;
if (phase === "final_answer" || phase === "final" || phase === "answer") return 3;
if (phase === "commentary" || phase === "process" || phase === "thinking") return 1;
return 2;
}
function normalizeAssistantMessagePhase(value) {
const phase = trimToDefined(value);
return phase || undefined;
}
async function readRolloutTail(rolloutPath) {
if (!rolloutPath) return "";
let handle;
try {
handle = await open(resolve(rolloutPath), "r");
const stats = await handle.stat();
const start = Math.max(0, stats.size - MAX_ROLLOUT_TAIL_BYTES);
const length = Math.max(0, stats.size - start);
if (length === 0) {
return "";
}
const buffer = Buffer.alloc(length);
await handle.read(buffer, 0, length, start);
let text = buffer.toString("utf8");
if (start > 0) {
const firstNewline = text.indexOf("\n");
text = firstNewline >= 0 ? text.slice(firstNewline + 1) : "";
}
return text;
} catch {
return "";
} finally {
await handle?.close().catch(() => {});
}
}
function parseRecentAssistantMessage(line, threadId) {
if (!line.trim()) return null;
try {
const parsed = JSON.parse(line);
let body = "";
let phase;
if (parsed?.type === "event_msg" && parsed?.payload?.type === "agent_message") {
body = typeof parsed.payload.message === "string" ? parsed.payload.message.trim() : "";
phase = normalizeAssistantMessagePhase(parsed.payload.phase);
} else if (
parsed?.type === "response_item" &&
parsed?.payload?.type === "message" &&
parsed?.payload?.role === "assistant"
) {
const content = Array.isArray(parsed.payload.content) ? parsed.payload.content : [];
body = content
.map((item) => (typeof item?.text === "string" ? item.text.trim() : ""))
.filter(Boolean)
.join("\n")
.trim();
phase = normalizeAssistantMessagePhase(parsed.payload.phase);
} else {
return null;
}
const sentAt = normalizeEventTimestamp(parsed.timestamp ?? parsed.payload.timestamp);
if (!body || !sentAt) {
return null;
}
const message = {
messageId: buildAssistantMessageId(threadId, sentAt, body),
body,
sentAt,
};
return phase ? { ...message, phase } : message;
} catch {
return null;
}
}
function mergeRecentAssistantMessage(existing, incoming) {
if (!existing) return incoming;
const existingPriority = assistantPhasePriority(existing.phase);
const incomingPriority = assistantPhasePriority(incoming.phase);
if (!incoming.phase || existingPriority >= incomingPriority) return existing;
return {
...existing,
phase: incoming.phase,
};
}
function isDuplicateAssistantTurn(existing, incoming) {
if (existing.body !== incoming.body) return false;
const existingTime = assistantMessageTimeValue(existing.sentAt);
const incomingTime = assistantMessageTimeValue(incoming.sentAt);
if (!existingTime || !incomingTime) return false;
return Math.abs(existingTime - incomingTime) <= ASSISTANT_DUPLICATE_TURN_WINDOW_MS;
}
function isGuiCodexThreadSource(value) {
const source = trimToDefined(value);
if (!source) return false;
if (source === "cli" || source === "exec" || source === "sessions") {
return false;
}
return true;
}
async function loadRecentAssistantMessages(rolloutPath, threadId) {
const tail = await readRolloutTail(rolloutPath);
if (!tail) return [];
const messagesById = new Map();
for (const line of tail.split(/\r?\n/)) {
const message = parseRecentAssistantMessage(line, threadId);
if (!message) continue;
const duplicateEntry = [...messagesById.entries()].find(([, existing]) =>
isDuplicateAssistantTurn(existing, message),
);
if (duplicateEntry) {
const [duplicateId, existing] = duplicateEntry;
messagesById.set(duplicateId, mergeRecentAssistantMessage(existing, message));
continue;
}
messagesById.set(message.messageId, mergeRecentAssistantMessage(messagesById.get(message.messageId), message));
}
return [...messagesById.values()]
.sort((left, right) => left.sentAt.localeCompare(right.sentAt))
.slice(-MAX_RECENT_ASSISTANT_MESSAGES);
}
function requireText(filePath) {
return readFileSync(resolve(filePath), "utf8");
}
function resolveDefaultSessionsDir(options = {}) {
if (options.sessionsDir) {
return options.sessionsDir;
}
if (options.stateDbPath) {
return resolve(dirname(resolve(options.stateDbPath)), "sessions");
}
return resolve(os.homedir(), ".codex/sessions");
}
export async function discoverCodexProjectCandidates(options = {}) {
const now = options.now instanceof Date ? options.now : new Date();
const lookbackHours = Number.isFinite(options.lookbackHours) ? Number(options.lookbackHours) : 24;
@@ -209,17 +500,17 @@ export async function discoverCodexProjectCandidates(options = {}) {
options.logsDbPath ?? resolve(os.homedir(), ".codex/logs_1.sqlite"),
);
let threads = loadThreadsFromStateDb(
const stateDbThreads = loadThreadsFromStateDb(
options.stateDbPath ?? resolve(os.homedir(), ".codex/state_5.sqlite"),
);
if (threads.length === 0) {
threads = await loadThreadsFromSessions(
options.sessionsDir ?? resolve(os.homedir(), ".codex/sessions"),
);
}
const sessionThreads = await loadThreadsFromSessions(resolveDefaultSessionsDir(options), {
cutoffSeconds,
});
const threads = [...stateDbThreads, ...sessionThreads];
const seenThreadIds = new Set();
const groupedCandidates = new Map();
let guiConnected = false;
for (const thread of threads) {
if (!thread?.id || seenThreadIds.has(thread.id)) continue;
if (isReadOnlySandboxPolicy(thread.sandboxPolicy)) {
@@ -231,6 +522,7 @@ export async function discoverCodexProjectCandidates(options = {}) {
}
seenThreadIds.add(thread.id);
guiConnected = guiConnected || isGuiCodexThreadSource(thread.source);
const hintedPath = workspaceHints.get(thread.id);
const folderPath = resolve(hintedPath || thread.cwd || "");
const folderName = basename(folderPath);
@@ -239,8 +531,16 @@ export async function discoverCodexProjectCandidates(options = {}) {
const sessionName = sessionNames.get(thread.id)?.threadName;
const displayName = sanitizeDisplayName(
sessionName,
sanitizeDisplayName(thread.title, fallbackDisplayName(thread, folderName)),
sanitizeDisplayName(thread.title, fallbackDisplayName(thread, folderName), {
folderName,
folderPath,
}),
{
folderName,
folderPath,
},
);
const recentAssistantMessages = await loadRecentAssistantMessages(thread.rolloutPath, thread.id);
const candidate = {
folderName,
@@ -251,6 +551,7 @@ export async function discoverCodexProjectCandidates(options = {}) {
codexThreadRef: thread.id,
lastActiveAt: toIsoFromUnixSeconds(latestActivitySeconds) ?? now.toISOString(),
suggestedImport: true,
...(recentAssistantMessages.length > 0 ? { recentAssistantMessages } : {}),
};
const folderKey = folderPath || folderName;
const bucket = groupedCandidates.get(folderKey) ?? [];
@@ -277,6 +578,7 @@ export async function discoverCodexProjectCandidates(options = {}) {
return {
projects,
projectCandidates: candidates,
guiConnected,
};
}