UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

145 lines 5.61 kB
import { existsSync, readFileSync } from 'fs'; import { dirname, isAbsolute, relative, resolve } from 'path'; import { load as loadYaml } from 'js-yaml'; export const REPO_ACCESS_MANIFEST_PATHS = [ '.aiwg/ops/security/repo-access.manifest.yaml', '.aiwg/security/repo-access.manifest.yaml', ]; export const REPO_ACCESS_ACTIONS = [ 'read', 'write', 'commit', 'push', 'issue-comment', 'service-action', 'destructive', ]; const ACTION_SET = new Set(REPO_ACCESS_ACTIONS); function isObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function findProjectRootFromManifest(manifestPath) { const normalized = manifestPath.replace(/\\/g, '/'); if (normalized.endsWith('/.aiwg/ops/security/repo-access.manifest.yaml')) { return resolve(dirname(manifestPath), '..', '..', '..'); } if (normalized.endsWith('/.aiwg/security/repo-access.manifest.yaml')) { return resolve(dirname(manifestPath), '..', '..'); } return resolve(dirname(manifestPath)); } export function findRepoAccessManifest(startDir = process.cwd()) { let current = resolve(startDir); while (true) { for (const manifestRel of REPO_ACCESS_MANIFEST_PATHS) { const candidate = resolve(current, manifestRel); if (existsSync(candidate)) return candidate; } const parent = dirname(current); if (parent === current) return null; current = parent; } } function normalizeAction(action) { if (typeof action !== 'string' || !ACTION_SET.has(action)) { throw new Error(`Invalid repo access action: ${String(action)}`); } return action; } function normalizeRepoEntry(entry, index, projectRoot) { if (!isObject(entry)) throw new Error(`repos[${index}] must be an object`); const name = entry.name; const repoPath = entry.path; const rawActions = entry.actions ?? entry.permissions; if (typeof name !== 'string' || !name.trim()) { throw new Error(`repos[${index}].name is required`); } if (typeof repoPath !== 'string' || !repoPath.trim()) { throw new Error(`repos[${index}].path is required`); } if (!Array.isArray(rawActions) || rawActions.length === 0) { throw new Error(`repos[${index}].actions must be a non-empty array`); } const resolvedPath = isAbsolute(repoPath) ? resolve(repoPath) : resolve(projectRoot, repoPath); return { name: name.trim(), path: resolvedPath, actions: rawActions.map(normalizeAction), notes: typeof entry.notes === 'string' ? entry.notes : undefined, }; } export function loadRepoAccessManifest(startDir = process.cwd()) { const manifestPath = findRepoAccessManifest(startDir); if (!manifestPath) { throw new Error(`Repo access manifest not found. Expected ${REPO_ACCESS_MANIFEST_PATHS.join(' or ')}`); } const projectRoot = findProjectRootFromManifest(manifestPath); const raw = loadYaml(readFileSync(manifestPath, 'utf8')); if (!isObject(raw)) throw new Error('Repo access manifest must be a YAML object'); const repos = raw.repos; if (!Array.isArray(repos)) throw new Error('repo access manifest requires repos: []'); const defaultPolicy = raw.defaultPolicy ?? raw.default_policy ?? 'deny'; if (defaultPolicy !== 'deny') { throw new Error('repo access manifest default policy must be deny'); } return { version: typeof raw.version === 'string' ? raw.version : '1', path: manifestPath, projectRoot, defaultPolicy: 'deny', repos: repos.map((entry, index) => normalizeRepoEntry(entry, index, projectRoot)), }; } function isSameOrInside(candidate, root) { const rel = relative(root, candidate); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); } export function findRepoEntry(manifest, requestedPath, cwd = process.cwd()) { const resolved = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(cwd, requestedPath); const matches = manifest.repos .filter((entry) => isSameOrInside(resolved, entry.path)) .sort((a, b) => b.path.length - a.path.length); return matches[0] ?? null; } export function checkRepoAccess(manifest, requestedPath, action, cwd = process.cwd()) { const normalizedAction = normalizeAction(action); const resolved = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(cwd, requestedPath); const matchedRepo = findRepoEntry(manifest, resolved, cwd); if (!matchedRepo) { return { allowed: false, action: normalizedAction, requestedPath: resolved, matchedRepo: null, reason: 'unlisted repo/path defaults to denied', }; } if (!matchedRepo.actions.includes(normalizedAction)) { return { allowed: false, action: normalizedAction, requestedPath: resolved, matchedRepo, reason: `repo '${matchedRepo.name}' does not allow ${normalizedAction}`, }; } return { allowed: true, action: normalizedAction, requestedPath: resolved, matchedRepo, reason: `repo '${matchedRepo.name}' allows ${normalizedAction}`, }; } export function formatRepoAccessEntry(entry) { const notes = entry.notes ? ` — ${entry.notes}` : ''; return `${entry.name}: ${entry.path} [${entry.actions.join(', ')}]${notes}`; } //# sourceMappingURL=repo-access.js.map