cloak-api / tools /sync_platform.mjs
n1th1sh's picture
Upload 3 files
a3f8710 verified
Raw History Blame Contribute Delete
8.17 kB
// Copies the platform facts the copilot depends on out of a cloakv2 checkout.
//
// node tools/sync_platform.mjs <path-to-cloakv2> (from hf-space/: node tools/sync_platform.mjs ..)
//
// Reads the checkout, never writes to it. Writes copilot/platform/snapshot.json and
// copilot/platform/defaults/<type>.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 <path-to-cloakv2>');
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 `<name> ... =`, 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}`);