// Copies the platform facts the copilot depends on out of a cloakv2 checkout. // // node tools/sync_platform.mjs (from hf-space/: node tools/sync_platform.mjs ..) // // Reads the checkout, never writes to it. Writes copilot/platform/snapshot.json and // copilot/platform/defaults/.json in this project. Each source file's sha256 is // recorded, so after a later sync `git diff` shows exactly which platform file moved. // // The TypeScript is not compiled: every constant we need is an object or array literal, // so we cut the literal out by bracket matching, strip `as T` casts and comments, and // evaluate it. import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const OUT = join(HERE, '..', 'copilot', 'platform'); const repo = process.argv[2]; if (!repo || !existsSync(join(repo, 'server', 'src'))) { console.error('usage: node tools/sync_platform.mjs '); process.exit(2); } const SOURCES = { presets: 'server/src/modules/org/presets.ts', blockLists:'server/src/modules/org/blockListPresets.ts', pii: 'server/src/detection/pii.ts', patterns: 'server/src/detection/patterns.json', resolve: 'server/src/modules/pii/resolvePolicy.ts', ide: 'server/src/modules/ide/policy.ts', ideCat: 'server/src/modules/ide/catalog.ts', aiSites: 'dashboard/lib/platformLogos.ts', rulesUi: 'dashboard/features/policies/DetectionRulesPanel.tsx', endpoint: 'dashboard/features/agent/PolicyForm.tsx', ideForm: 'dashboard/features/agent/IdePolicySection.tsx', email: 'dashboard/features/email/PolicyForm.tsx', emailT: 'dashboard/features/email/types.ts', slack: 'dashboard/features/channels/slack/PolicyForm.tsx', teams: 'dashboard/features/channels/microsoft/teams/PolicyForm.tsx', chat: 'dashboard/features/channels/google/chat/PolicyForm.tsx', drive: 'dashboard/features/channels/google/drive/PolicyForm.tsx', m365files: 'dashboard/features/channels/microsoft/files/PolicyForm.tsx', s3: 'dashboard/features/channels/aws/s3/PolicyForm.tsx', gcs: 'dashboard/features/channels/google/gcs/PolicyForm.tsx', azure: 'dashboard/features/channels/azure/blob/PolicyForm.tsx', github: 'dashboard/features/channels/github/PolicyForm.tsx', }; const text = {}; const hashes = {}; for (const [k, rel] of Object.entries(SOURCES)) { const buf = readFileSync(join(repo, rel)); text[k] = buf.toString('utf8'); hashes[rel] = createHash('sha256').update(buf).digest('hex'); } // The literal that follows ` ... =`, from its opening bracket to the matching close. function literal(src, name) { const at = src.search(new RegExp(`\\b(const|let)\\s+${name}\\b[^=]*=`)); if (at < 0) throw new Error(`${name} not found`); let i = src.indexOf('=', at) + 1; while (!'[{'.includes(src[i])) i++; const start = i; let depth = 0, quote = null; for (; i < src.length; i++) { const c = src[i]; if (quote) { if (c === '\\') { i++; continue; } if (c === quote) quote = null; continue; } if (c === '/' && src[i + 1] === '/') { i = src.indexOf('\n', i); continue; } if (c === "'" || c === '"' || c === '`') quote = c; else if (c === '[' || c === '{') depth++; else if (c === ']' || c === '}') { depth--; if (depth === 0) return src.slice(start, i + 1); } } throw new Error(`${name}: unbalanced literal`); } function evaluate(lit, scope = {}) { const js = lit .replace(/(^|[\s,{[(])\/\/[^\n]*/g, '$1') // line comments, not the // inside URLs .replace(/\s+as\s+(const|[A-Za-z_][\w]*(\[\])?|'[^']*'(\s*\|\s*'[^']*')*)/g, ''); // casts return new Function(...Object.keys(scope), `return (${js});`)(...Object.values(scope)); } const value = (k, name, scope) => evaluate(literal(text[k], name), scope); // `new Set([...])` constants: read the array inside. function setLiteral(k, name) { const m = text[k].match(new RegExp(`const\\s+${name}\\s*=\\s*new Set\\(\\s*(\\[[\\s\\S]*?\\])\\s*\\)`)); if (!m) throw new Error(`${name} not found`); return evaluate(m[1]); } const aiSites = value('aiSites', 'AI_PLATFORMS'); const allHosts = aiSites.map(s => s.host); const emptyExt = value('endpoint', 'EMPTY_EXT'); const emptyAgent = value('endpoint', 'EMPTY_AGENT'); const emptyIdeForm = value('ideForm', 'EMPTY_IDE'); const ideDefaults = value('ide', 'IDE_DEFAULTS'); const excludes = value('github', 'DEFAULT_EXCLUDES'); const channelEmpty = { email: value('email', 'EMPTY_EMAIL'), slack: value('slack', 'EMPTY_SLACK'), teams: value('teams', 'EMPTY_TEAMS'), chat: value('chat', 'EMPTY_CHAT'), drive: value('drive', 'EMPTY_DRIVE'), m365files: value('m365files', 'EMPTY_M365'), s3: value('s3', 'EMPTY_S3'), gcs: value('gcs', 'EMPTY_GCS'), azure: value('azure', 'EMPTY_AZURE'), github: value('github', 'EMPTY_GH', { DEFAULT_EXCLUDES: excludes }), }; // Bodies exactly as each form's handleSubmit builds them on create, before anything is // edited. The endpoint form spreads the whole form state, so ideControl is sent both at // the top level (ignored by the server) and inside agentControl (stored). const defaults = { endpoint: { name: '', description: '', groupId: 'default', action: 'warn', active: true, testMode: false, enabledEntities: [], allowFileUpload: true, allowCodePaste: true, customSites: [], extensionControl: emptyExt, agentControl: { ...emptyAgent, allowedUsbDeviceIds: [], blockedApplications: [], blockedDomains: [], ideControl: emptyIdeForm }, ideControl: emptyIdeForm, activeWebCategories: [], exceptions: [], enabled: true, enabledSites: allHosts, channels: {}, }, }; for (const [ch, empty] of Object.entries(channelEmpty)) { defaults[ch] = { name: '', description: '', groupId: 'default', active: true, enabled: true, enabledEntities: [], agentControl: {}, extensionControl: {}, channels: { [ch]: { ...empty, enabled: true } }, }; } const ideCatalog = { providers: value('ideCat', 'AI_PROVIDERS').map(p => ({ id: p.id, name: p.name, hosts: p.hosts })), tools: value('ideCat', 'AI_TOOLS').map(t => ({ key: t.key, name: t.name, kind: t.kind, processes: t.processes, provider: t.provider || null })), aiConfigFiles: value('ideCat', 'AI_CONFIG_FILES'), }; const stamp = { syncedAt: new Date().toISOString(), sources: hashes }; mkdirSync(join(OUT, 'defaults'), { recursive: true }); for (const [type, body] of Object.entries(defaults)) { writeFileSync(join(OUT, 'defaults', `${type}.json`), JSON.stringify({ _source: stamp, body }, null, 2) + '\n'); } const snapshot = { _source: stamp, aiSites, genaiPasteSites: value('endpoint', 'GENAI_PRESET_SITES'), genaiPasteApps: value('endpoint', 'GENAI_PRESET_APPS'), detectionRules: value('presets', 'DETECTION_RULE_PRESETS'), blockLists: value('blockLists', 'BLOCK_LIST_PRESETS'), entityGroupsUi: value('rulesUi', 'ENTITY_GROUPS'), fileTypes: value('rulesUi', 'FILE_TYPES'), patternLabels: [...new Set(JSON.parse(text.patterns).patterns.map(p => p.label))].sort(), highRisk: setLiteral('pii', 'HIGH_RISK'), mediumRisk: setLiteral('pii', 'MEDIUM_RISK'), actionRank: value('resolve', 'ACTION_RANK'), emailActions: value('emailT', 'EMAIL_ACTIONS').map(a => a.value), ide: { defaults: ideDefaults, promptActions: value('ide', 'PROMPT_ACTIONS'), accessActions: value('ide', 'ACCESS_ACTIONS'), sensorActions: value('ide', 'SENSOR_ACTIONS'), riskLevels: value('ide', 'RISK_LEVELS'), ...ideCatalog, }, }; writeFileSync(join(OUT, 'snapshot.json'), JSON.stringify(snapshot, null, 2) + '\n'); console.log(`types ${Object.keys(defaults).length}, rules ${snapshot.detectionRules.length}, ` + `block lists ${snapshot.blockLists.length}, AI sites ${aiSites.length}, ` + `pattern labels ${snapshot.patternLabels.length}, IDE tools ${ideCatalog.tools.length}, ` + `IDE providers ${ideCatalog.providers.length}`);