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
JavaScript
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