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
195 lines • 6.3 kB
JavaScript
/**
* Path-emission allowlist for AGENTS.md link-index entries.
*
* Per ADR-1 §2 (security mitigation R2): the generator MUST only emit `Path:` values
* that match a path produced by the AIWG-owned PROVIDER_PATHS map plus the canonical
* `~/.agents/skills/` cross-provider user-scope target. Files deployed by project-local
* manifests at non-AIWG paths are not indexed. This closes the link-redirect attack
* surface where a malicious project-local artifact could cite a shadow file outside
* AIWG's path-map domain.
*/
import { homedir } from 'node:os';
import * as path from 'node:path';
/**
* The AIWG-owned path prefixes that may appear in an AGENTS.md link entry.
*
* Mirrors the per-provider directory list from `src/cli/handlers/use.ts` PROVIDER_PATHS,
* generalized to prefixes (so per-artifact subpaths under each prefix are accepted).
*
* Sources kept in sync:
* - `src/cli/handlers/use.ts:161-232` PROVIDER_PATHS
* - `src/smiths/platform-paths.ts` (TypeScript path resolvers)
* - `src/config/aiwg-config.ts:552`
*
* If a new platform path is added there, add it here.
*/
const AIWG_PATH_PREFIXES_RELATIVE = [
// Claude Code
'.claude/agents/',
'.claude/skills/',
'.claude/commands/',
'.claude/rules/',
'.claude/hooks/',
// Factory AI
'.factory/droids/',
'.factory/skills/',
'.factory/commands/',
'.factory/rules/',
// OpenAI Codex (project scope; user scope handled below via homedir)
'.codex/agents/',
'.codex/skills/',
'.codex/commands/',
'.codex/rules/',
// OpenCode
'.opencode/agent/',
'.opencode/skill/',
'.opencode/rule/',
// Copilot
'.github/agents/',
'.github/skills/',
'.github/copilot-rules/',
'.github/prompts/',
'.github/instructions/',
// Cursor
'.cursor/agents/',
'.cursor/skills/',
'.cursor/commands/',
'.cursor/rules/',
// Warp
'.warp/agents/',
'.warp/skills/',
'.warp/commands/',
'.warp/rules/',
// Windsurf
'.windsurf/agents/',
'.windsurf/skills/',
'.windsurf/workflows/',
'.windsurf/rules/',
// Cross-agent canonical project-scope path (#766 + ADR-1)
'.agents/agents/',
'.agents/skills/',
'.agents/rules/',
'.agents/behaviors/',
];
/**
* User-scope path prefixes (resolved with homedir() at check time).
*
* Per ADR-4 §2 user-global path map.
*/
const AIWG_PATH_PREFIXES_USER = [
// Cross-agent canonical user-scope path (ADR-4)
'.agents/skills/',
// Per-provider user-scope paths
'.claude/agents/',
'.claude/skills/',
'.claude/commands/',
'.claude/rules/',
'.claude/hooks/',
'.codex/skills/',
'.codex/prompts/',
'.codex/agents/',
'.codex/rules/',
'.cursor/agents/',
'.cursor/skills/',
'.cursor/commands/',
'.cursor/rules/',
'.warp/agents/',
'.warp/skills/',
'.warp/commands/',
'.warp/rules/',
'.windsurf/agents/',
'.windsurf/skills/',
'.windsurf/workflows/',
'.windsurf/rules/',
'.opencode/agent/',
'.opencode/skill/',
'.opencode/rule/',
'.openclaw/agents/',
'.openclaw/skills/',
'.openclaw/commands/',
'.openclaw/rules/',
'.openclaw/behaviors/',
'.openclaw/hooks/',
'.factory/droids/',
'.factory/skills/',
'.factory/commands/',
'.factory/rules/',
'.config/github-copilot/agents/',
'.config/github-copilot/prompts/',
'.config/github-copilot/instructions/',
'.hermes/skills/',
];
/**
* Check whether a path is allowed in an AGENTS.md link-index entry.
*
* Accepts both relative project-scope paths and absolute user-scope paths
* (rooted in homedir()). Rejects all other paths.
*
* Path normalization:
* - Backslashes are converted to forward slashes for cross-platform compatibility.
* - Leading `./` is stripped.
* - `..` segments anywhere in the path cause rejection (prevents escaping the project root).
*/
export function checkPathAllowed(inputPath) {
if (typeof inputPath !== 'string' || inputPath.length === 0) {
return { ok: false, rejectedFor: 'empty or non-string path', isUserScope: false };
}
// Reject any path with a parent-dir traversal segment.
const segments = inputPath.split(/[/\\]/);
if (segments.includes('..')) {
return { ok: false, rejectedFor: 'parent-dir traversal (..) not allowed', isUserScope: false };
}
// Normalize to forward slashes; strip leading ./
let normalized = inputPath.replace(/\\/g, '/');
if (normalized.startsWith('./')) {
normalized = normalized.slice(2);
}
// User-scope: starts with ~/ (or absolute home directory)
const home = homedir();
const homeNormalized = home.replace(/\\/g, '/');
let isUserScope = false;
let userScopeRelative = '';
if (normalized.startsWith('~/')) {
isUserScope = true;
userScopeRelative = normalized.slice(2);
}
else if (normalized.startsWith(homeNormalized + '/')) {
isUserScope = true;
userScopeRelative = normalized.slice(homeNormalized.length + 1);
}
else if (path.isAbsolute(inputPath)) {
return {
ok: false,
rejectedFor: 'absolute path outside home directory',
isUserScope: false,
};
}
if (isUserScope) {
for (const prefix of AIWG_PATH_PREFIXES_USER) {
if (userScopeRelative === prefix.replace(/\/$/, '') || userScopeRelative.startsWith(prefix)) {
return { ok: true, rejectedFor: '', isUserScope: true };
}
}
return {
ok: false,
rejectedFor: 'user-scope path not in AIWG-owned prefix list',
isUserScope: true,
};
}
// Project-scope: must start with one of AIWG_PATH_PREFIXES_RELATIVE.
for (const prefix of AIWG_PATH_PREFIXES_RELATIVE) {
if (normalized === prefix.replace(/\/$/, '') || normalized.startsWith(prefix)) {
return { ok: true, rejectedFor: '', isUserScope: false };
}
}
return {
ok: false,
rejectedFor: 'project-scope path not in AIWG-owned prefix list',
isUserScope: false,
};
}
export const ALLOWLIST_INTERNALS = {
AIWG_PATH_PREFIXES_RELATIVE,
AIWG_PATH_PREFIXES_USER,
};
//# sourceMappingURL=allowlist.js.map