/** * @fileoverview Content-based skill install path (private registry). * @module @skillsmith/core/services/skill-installation.content * @see SMI-5905 Wave 1: installFromContent() + resolveFreshAccessToken() extraction * @see docs/internal/implementation/private-registry-skill-install.md * * `install()` (skill-installation.service.ts) only knows how to fetch a * skill from GitHub. This module adds a second entry point, * `installFromContent()`, for skills whose content has already been resolved * elsewhere (a private-registry `content` JSONB column, read via a * per-team-member Supabase session or the `private-registry-get` Edge * Function — Waves 2/3 wire those transports; this module only needs the * already-fetched `{skillId, version, content}` triple). * * SCOPE TRIM (Sol review #5 — read before extending this function): * `installFromContent()` deliberately reuses only two things from the * `install()` policy chain: * 1. `writeInstallFiles()`'s disk-write + rollback-on-partial-write handling * (skill-installation.io.ts), plus this module's own manifest-driven * already-installed/force gate (mirrors `install()`'s ALREADY_INSTALLED * check). * 2. A security scan at the `community` trust tier, via the same * `classifyBundledFile()`/`isRejectableScan()` policy `install()` uses * for optional bundled files (skill-installation.policy.ts) — applied * here to every content-map entry (including "SKILL.md" itself, which * falls through to the conservative 'structured' default class since it * has no BUNDLED_SCAN_FILES entry). * * It deliberately does NOT route through the rest of `install()`'s chain: * - `skipScan` is NOT an accepted option here. Registry-content installs * always scan — there is no opt-out, unlike `install()`'s tier-gated * skipScan. * - Dependency-intelligence extraction/persistence (extractDepIntel, * persistDependencies, checkDepsAgainstQuarantine), risk-history * recording, AI-defence feedback, and co-install-session recording are * all OUT of v1 scope — a known, intentional gap, not a silent drop. * Wave 3's MCP `install` action and Wave 4's CLI command do not get * these signals for private-registry installs yet. * - Frontmatter/manifest validation on `SKILL.md` itself DOES still run * (`validateSkillMd()`), same as any other install. * * PATH VALIDATION (Sol review #2, critical, confirmed exploitable): every key * in `content` is attacker-controlled — any team member with publish access * chooses these filenames — and flows into `writeInstallFiles()`'s * `path.join(installPath, subSkill.filename)` (skill-installation.io.ts:167) * with NO containment check of its own. `path.join('/a/b', '../../etc/passwd')` * escapes `installPath` after normalization, so every key is validated here, * BEFORE any disk write — see `validateContentKeys()`. */ import type { Database } from '../db/database-interface.js'; import type { ClientId } from '../install/paths.js'; import type { ProgressCallback, InstallFromContentOptions, InstallResult } from './skill-installation.types.js'; import type { ManifestManager } from './skill-manifest.js'; /** * Validate every key in a content map before any disk write. * * Rejects (as a single combined error, first offense wins): * - path-traversal (`..` segment), absolute paths (posix or win32/UNC), * NUL bytes, backslashes, empty/bare-"."/".git"-adjacent keys; * - any key that, after `path.resolve()` against `installPath`, resolves * outside `installPath` (a lexical backstop behind the checks above, * matching `writeInstallFiles()`'s own lexical escape check); * - two keys that `path.normalize()` to the same on-disk path CASE * -INSENSITIVELY (SMI-6529 M4/M9) — a collision on any filesystem, but * specifically catches a case-insensitive alias (e.g. "SKILL.md" and * "skill.md" — literally the same inode on default APFS) that a * case-sensitive string comparison would miss, in addition to the * exact-string collision it already caught (e.g. "SKILL.md" and * "./SKILL.md"). */ export declare function validateContentKeys(content: Record, installPath: string): { valid: true; } | { valid: false; error: string; }; /** Internal params bag: the caller's InstallFromContentOptions plus the service state * installFromContent() needs (mirrors performUninstall()'s flattened-params convention * in skill-installation.helpers.ts). */ export interface InstallFromContentParams extends InstallFromContentOptions { db: Database; skillsDir: string; manifest: ManifestManager; client: ClientId; onProgress: ProgressCallback; /** * SMI-5982 code-review fix #1: base dir for resolving a relative companion-agent * target (Antigravity only) — see writeInstallFiles()'s own doc comment. * PR-review follow-up: optional (not required) — an omitted value must flow * through to `resolveCompanionAgentPath()`'s own required-`baseDir` guard as * `undefined`, not be silently defaulted anywhere in this chain. */ companionBaseDir?: string; } /** * Install an already-resolved private-registry skill's content to disk. * * See this file's header doc comment for the full scope trim vs. `install()`. */ export declare function installFromContent(params: InstallFromContentParams): Promise; //# sourceMappingURL=skill-installation.content.d.ts.map