import { promises as fs } from "node:fs"; import path from "node:path"; import type { Db } from "@paperclipai/db"; import type { CompanyPortabilityAgentManifestEntry, CompanyPortabilityCollisionStrategy, CompanyPortabilityExport, CompanyPortabilityExportResult, CompanyPortabilityImport, CompanyPortabilityImportResult, CompanyPortabilityInclude, CompanyPortabilityManifest, CompanyPortabilityPreview, CompanyPortabilityPreviewAgentPlan, CompanyPortabilityPreviewResult, } from "@paperclipai/shared"; import { normalizeAgentUrlKey } from "@paperclipai/shared"; import { notFound, unprocessable } from "../errors.js"; import { accessService } from "./access.js"; import { agentService } from "./agents.js"; import { companyService } from "./companies.js"; const DEFAULT_INCLUDE: CompanyPortabilityInclude = { company: true, agents: true, }; const DEFAULT_COLLISION_STRATEGY: CompanyPortabilityCollisionStrategy = "rename"; const SENSITIVE_ENV_KEY_RE = /(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i; type ResolvedSource = { manifest: CompanyPortabilityManifest; files: Record; warnings: string[]; }; type MarkdownDoc = { frontmatter: Record; body: string; }; type CompanyPackageIncludeEntry = { path: string; }; type ImportPlanInternal = { preview: CompanyPortabilityPreviewResult; source: ResolvedSource; include: CompanyPortabilityInclude; collisionStrategy: CompanyPortabilityCollisionStrategy; selectedAgents: CompanyPortabilityAgentManifestEntry[]; }; type AgentLike = { id: string; name: string; adapterConfig: Record; }; const RUNTIME_DEFAULT_RULES: Array<{ path: string[]; value: unknown }> = [ { path: ["heartbeat", "cooldownSec"], value: 10 }, { path: ["heartbeat", "intervalSec"], value: 3600 }, { path: ["heartbeat", "wakeOnOnDemand"], value: true }, { path: ["heartbeat", "wakeOnAssignment"], value: true }, { path: ["heartbeat", "wakeOnAutomation"], value: true }, { path: ["heartbeat", "wakeOnDemand"], value: true }, { path: ["heartbeat", "maxConcurrentRuns"], value: 3 }, ]; const ADAPTER_DEFAULT_RULES_BY_TYPE: Record> = { codex_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], gemini_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], opencode_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], cursor: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], claude_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, { path: ["maxTurnsPerRun"], value: 300 }, ], openclaw_gateway: [ { path: ["timeoutSec"], value: 120 }, { path: ["waitTimeoutMs"], value: 120000 }, { path: ["sessionKeyStrategy"], value: "fixed" }, { path: ["sessionKey"], value: "paperclip" }, { path: ["role"], value: "operator" }, { path: ["scopes"], value: ["operator.admin"] }, ], }; function isPlainRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function asString(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function toSafeSlug(input: string, fallback: string) { return normalizeAgentUrlKey(input) ?? fallback; } function uniqueSlug(base: string, used: Set) { if (!used.has(base)) { used.add(base); return base; } let idx = 2; while (true) { const candidate = `${base}-${idx}`; if (!used.has(candidate)) { used.add(candidate); return candidate; } idx += 1; } } function uniqueNameBySlug(baseName: string, existingSlugs: Set) { const baseSlug = normalizeAgentUrlKey(baseName) ?? "agent"; if (!existingSlugs.has(baseSlug)) return baseName; let idx = 2; while (true) { const candidateName = `${baseName} ${idx}`; const candidateSlug = normalizeAgentUrlKey(candidateName) ?? `agent-${idx}`; if (!existingSlugs.has(candidateSlug)) return candidateName; idx += 1; } } function normalizeInclude(input?: Partial): CompanyPortabilityInclude { return { company: input?.company ?? DEFAULT_INCLUDE.company, agents: input?.agents ?? DEFAULT_INCLUDE.agents, }; } function normalizePortablePath(input: string) { const normalized = input.replace(/\\/g, "/").replace(/^\.\/+/, ""); const parts: string[] = []; for (const segment of normalized.split("/")) { if (!segment || segment === ".") continue; if (segment === "..") { if (parts.length > 0) parts.pop(); continue; } parts.push(segment); } return parts.join("/"); } function resolvePortablePath(fromPath: string, targetPath: string) { const baseDir = path.posix.dirname(fromPath.replace(/\\/g, "/")); return normalizePortablePath(path.posix.join(baseDir, targetPath.replace(/\\/g, "/"))); } function normalizeFileMap( files: Record, rootPath?: string | null, ): Record { const normalizedRoot = rootPath ? normalizePortablePath(rootPath) : null; const out: Record = {}; for (const [rawPath, content] of Object.entries(files)) { let nextPath = normalizePortablePath(rawPath); if (normalizedRoot && nextPath === normalizedRoot) { continue; } if (normalizedRoot && nextPath.startsWith(`${normalizedRoot}/`)) { nextPath = nextPath.slice(normalizedRoot.length + 1); } if (!nextPath) continue; out[nextPath] = content; } return out; } function ensureMarkdownPath(pathValue: string) { const normalized = pathValue.replace(/\\/g, "/"); if (!normalized.endsWith(".md")) { throw unprocessable(`Manifest file path must end in .md: ${pathValue}`); } return normalized; } function normalizePortableEnv( agentSlug: string, envValue: unknown, requiredSecrets: CompanyPortabilityManifest["requiredSecrets"], ) { if (typeof envValue !== "object" || envValue === null || Array.isArray(envValue)) return {}; const env = envValue as Record; const next: Record = {}; for (const [key, binding] of Object.entries(env)) { if (SENSITIVE_ENV_KEY_RE.test(key)) { requiredSecrets.push({ key, description: `Set ${key} for agent ${agentSlug}`, agentSlug, providerHint: null, }); continue; } next[key] = binding; } return next; } function normalizePortableConfig( value: unknown, agentSlug: string, requiredSecrets: CompanyPortabilityManifest["requiredSecrets"], ): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) return {}; const input = value as Record; const next: Record = {}; for (const [key, entry] of Object.entries(input)) { if (key === "cwd" || key === "instructionsFilePath") continue; if (key === "env") { next[key] = normalizePortableEnv(agentSlug, entry, requiredSecrets); continue; } next[key] = entry; } return next; } function jsonEqual(left: unknown, right: unknown): boolean { return JSON.stringify(left) === JSON.stringify(right); } function isPathDefault(pathSegments: string[], value: unknown, rules: Array<{ path: string[]; value: unknown }>) { return rules.some((rule) => jsonEqual(rule.path, pathSegments) && jsonEqual(rule.value, value)); } function pruneDefaultLikeValue( value: unknown, opts: { dropFalseBooleans: boolean; path?: string[]; defaultRules?: Array<{ path: string[]; value: unknown }>; }, ): unknown { const pathSegments = opts.path ?? []; if (opts.defaultRules && isPathDefault(pathSegments, value, opts.defaultRules)) { return undefined; } if (Array.isArray(value)) { return value.map((entry) => pruneDefaultLikeValue(entry, { ...opts, path: pathSegments })); } if (isPlainRecord(value)) { const out: Record = {}; for (const [key, entry] of Object.entries(value)) { const next = pruneDefaultLikeValue(entry, { ...opts, path: [...pathSegments, key], }); if (next === undefined) continue; out[key] = next; } return out; } if (value === undefined) return undefined; if (opts.dropFalseBooleans && value === false) return undefined; return value; } function renderYamlScalar(value: unknown): string { if (value === null) return "null"; if (typeof value === "boolean" || typeof value === "number") return String(value); if (typeof value === "string") return JSON.stringify(value); return JSON.stringify(value); } function isEmptyObject(value: unknown): boolean { return isPlainRecord(value) && Object.keys(value).length === 0; } function renderYamlBlock(value: unknown, indentLevel: number): string[] { const indent = " ".repeat(indentLevel); if (Array.isArray(value)) { if (value.length === 0) return [`${indent}[]`]; const lines: string[] = []; for (const entry of value) { const scalar = entry === null || typeof entry === "string" || typeof entry === "boolean" || typeof entry === "number" || Array.isArray(entry) && entry.length === 0 || isEmptyObject(entry); if (scalar) { lines.push(`${indent}- ${renderYamlScalar(entry)}`); continue; } lines.push(`${indent}-`); lines.push(...renderYamlBlock(entry, indentLevel + 1)); } return lines; } if (isPlainRecord(value)) { const entries = Object.entries(value); if (entries.length === 0) return [`${indent}{}`]; const lines: string[] = []; for (const [key, entry] of entries) { const scalar = entry === null || typeof entry === "string" || typeof entry === "boolean" || typeof entry === "number" || Array.isArray(entry) && entry.length === 0 || isEmptyObject(entry); if (scalar) { lines.push(`${indent}${key}: ${renderYamlScalar(entry)}`); continue; } lines.push(`${indent}${key}:`); lines.push(...renderYamlBlock(entry, indentLevel + 1)); } return lines; } return [`${indent}${renderYamlScalar(value)}`]; } function renderFrontmatter(frontmatter: Record) { const lines: string[] = ["---"]; for (const [key, value] of Object.entries(frontmatter)) { const scalar = value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" || Array.isArray(value) && value.length === 0 || isEmptyObject(value); if (scalar) { lines.push(`${key}: ${renderYamlScalar(value)}`); continue; } lines.push(`${key}:`); lines.push(...renderYamlBlock(value, 1)); } lines.push("---"); return `${lines.join("\n")}\n`; } function buildMarkdown(frontmatter: Record, body: string) { const cleanBody = body.replace(/\r\n/g, "\n").trim(); if (!cleanBody) { return `${renderFrontmatter(frontmatter)}\n`; } return `${renderFrontmatter(frontmatter)}\n${cleanBody}\n`; } function renderCompanyAgentsSection(agentSummaries: Array<{ slug: string; name: string }>) { const lines = ["# Agents", ""]; if (agentSummaries.length === 0) { lines.push("- _none_"); return lines.join("\n"); } for (const agent of agentSummaries) { lines.push(`- ${agent.slug} - ${agent.name}`); } return lines.join("\n"); } function parseYamlScalar(rawValue: string): unknown { const trimmed = rawValue.trim(); if (trimmed === "") return ""; if (trimmed === "null" || trimmed === "~") return null; if (trimmed === "true") return true; if (trimmed === "false") return false; if (trimmed === "[]") return []; if (trimmed === "{}") return {}; if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed); if ( trimmed.startsWith("\"") || trimmed.startsWith("[") || trimmed.startsWith("{") ) { try { return JSON.parse(trimmed); } catch { return trimmed; } } return trimmed; } function prepareYamlLines(raw: string) { return raw .split("\n") .map((line) => ({ indent: line.match(/^ */)?.[0].length ?? 0, content: line.trim(), })) .filter((line) => line.content.length > 0 && !line.content.startsWith("#")); } function parseYamlBlock( lines: Array<{ indent: number; content: string }>, startIndex: number, indentLevel: number, ): { value: unknown; nextIndex: number } { let index = startIndex; while (index < lines.length && lines[index]!.content.length === 0) { index += 1; } if (index >= lines.length || lines[index]!.indent < indentLevel) { return { value: {}, nextIndex: index }; } const isArray = lines[index]!.indent === indentLevel && lines[index]!.content.startsWith("-"); if (isArray) { const values: unknown[] = []; while (index < lines.length) { const line = lines[index]!; if (line.indent < indentLevel) break; if (line.indent !== indentLevel || !line.content.startsWith("-")) break; const remainder = line.content.slice(1).trim(); index += 1; if (!remainder) { const nested = parseYamlBlock(lines, index, indentLevel + 2); values.push(nested.value); index = nested.nextIndex; continue; } const inlineObjectSeparator = remainder.indexOf(":"); if ( inlineObjectSeparator > 0 && !remainder.startsWith("\"") && !remainder.startsWith("{") && !remainder.startsWith("[") ) { const key = remainder.slice(0, inlineObjectSeparator).trim(); const rawValue = remainder.slice(inlineObjectSeparator + 1).trim(); const nextObject: Record = { [key]: parseYamlScalar(rawValue), }; if (index < lines.length && lines[index]!.indent > indentLevel) { const nested = parseYamlBlock(lines, index, indentLevel + 2); if (isPlainRecord(nested.value)) { Object.assign(nextObject, nested.value); } index = nested.nextIndex; } values.push(nextObject); continue; } values.push(parseYamlScalar(remainder)); } return { value: values, nextIndex: index }; } const record: Record = {}; while (index < lines.length) { const line = lines[index]!; if (line.indent < indentLevel) break; if (line.indent !== indentLevel) { index += 1; continue; } const separatorIndex = line.content.indexOf(":"); if (separatorIndex <= 0) { index += 1; continue; } const key = line.content.slice(0, separatorIndex).trim(); const remainder = line.content.slice(separatorIndex + 1).trim(); index += 1; if (!remainder) { const nested = parseYamlBlock(lines, index, indentLevel + 2); record[key] = nested.value; index = nested.nextIndex; continue; } record[key] = parseYamlScalar(remainder); } return { value: record, nextIndex: index }; } function parseYamlFrontmatter(raw: string): Record { const prepared = prepareYamlLines(raw); if (prepared.length === 0) return {}; const parsed = parseYamlBlock(prepared, 0, prepared[0]!.indent); return isPlainRecord(parsed.value) ? parsed.value : {}; } function parseFrontmatterMarkdown(raw: string): MarkdownDoc { const normalized = raw.replace(/\r\n/g, "\n"); if (!normalized.startsWith("---\n")) { return { frontmatter: {}, body: normalized.trim() }; } const closing = normalized.indexOf("\n---\n", 4); if (closing < 0) { return { frontmatter: {}, body: normalized.trim() }; } const frontmatterRaw = normalized.slice(4, closing).trim(); const body = normalized.slice(closing + 5).trim(); return { frontmatter: parseYamlFrontmatter(frontmatterRaw), body, }; } async function fetchText(url: string) { const response = await fetch(url); if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.text(); } async function fetchOptionalText(url: string) { const response = await fetch(url); if (response.status === 404) return null; if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.text(); } function dedupeRequiredSecrets(values: CompanyPortabilityManifest["requiredSecrets"]) { const seen = new Set(); const out: CompanyPortabilityManifest["requiredSecrets"] = []; for (const value of values) { const key = `${value.agentSlug ?? ""}:${value.key.toUpperCase()}`; if (seen.has(key)) continue; seen.add(key); out.push(value); } return out; } function buildIncludes(paths: string[]): CompanyPackageIncludeEntry[] { return paths.map((value) => ({ path: value })); } function readCompanyApprovalDefault(frontmatter: Record) { const topLevel = frontmatter.requireBoardApprovalForNewAgents; if (typeof topLevel === "boolean") return topLevel; const defaults = frontmatter.defaults; if (isPlainRecord(defaults) && typeof defaults.requireBoardApprovalForNewAgents === "boolean") { return defaults.requireBoardApprovalForNewAgents; } return true; } function readIncludeEntries(frontmatter: Record): CompanyPackageIncludeEntry[] { const includes = frontmatter.includes; if (!Array.isArray(includes)) return []; return includes.flatMap((entry) => { if (typeof entry === "string") { return [{ path: entry }]; } if (isPlainRecord(entry)) { const pathValue = asString(entry.path); return pathValue ? [{ path: pathValue }] : []; } return []; }); } function readAgentSecretRequirements( frontmatter: Record, agentSlug: string, ): CompanyPortabilityManifest["requiredSecrets"] { const requirements = frontmatter.requirements; const secretsFromRequirements = isPlainRecord(requirements) && Array.isArray(requirements.secrets) ? requirements.secrets : []; const legacyRequiredSecrets = Array.isArray(frontmatter.requiredSecrets) ? frontmatter.requiredSecrets : []; const combined = [...secretsFromRequirements, ...legacyRequiredSecrets]; return combined.flatMap((entry) => { if (typeof entry === "string" && entry.trim()) { return [{ key: entry.trim(), description: `Set ${entry.trim()} for agent ${agentSlug}`, agentSlug, providerHint: null, }]; } if (isPlainRecord(entry)) { const key = asString(entry.key); if (!key) return []; return [{ key, description: asString(entry.description) ?? `Set ${key} for agent ${agentSlug}`, agentSlug, providerHint: asString(entry.providerHint), }]; } return []; }); } function buildManifestFromPackageFiles( files: Record, opts?: { sourceLabel?: { companyId: string; companyName: string } | null }, ): ResolvedSource { const normalizedFiles = normalizeFileMap(files); const companyPath = normalizedFiles["COMPANY.md"] ?? undefined; const resolvedCompanyPath = companyPath !== undefined ? "COMPANY.md" : Object.keys(normalizedFiles).find((entry) => entry.endsWith("/COMPANY.md") || entry === "COMPANY.md"); if (!resolvedCompanyPath) { throw unprocessable("Company package is missing COMPANY.md"); } const companyDoc = parseFrontmatterMarkdown(normalizedFiles[resolvedCompanyPath]!); const companyFrontmatter = companyDoc.frontmatter; const companyName = asString(companyFrontmatter.name) ?? opts?.sourceLabel?.companyName ?? "Imported Company"; const companySlug = asString(companyFrontmatter.slug) ?? normalizeAgentUrlKey(companyName) ?? "company"; const includeEntries = readIncludeEntries(companyFrontmatter); const referencedAgentPaths = includeEntries .map((entry) => resolvePortablePath(resolvedCompanyPath, entry.path)) .filter((entry) => entry.endsWith("/AGENTS.md") || entry === "AGENTS.md"); const discoveredAgentPaths = Object.keys(normalizedFiles).filter( (entry) => entry.endsWith("/AGENTS.md") || entry === "AGENTS.md", ); const agentPaths = Array.from(new Set([...referencedAgentPaths, ...discoveredAgentPaths])).sort(); const manifest: CompanyPortabilityManifest = { schemaVersion: 2, generatedAt: new Date().toISOString(), source: opts?.sourceLabel ?? null, includes: { company: true, agents: true, }, company: { path: resolvedCompanyPath, name: companyName, description: asString(companyFrontmatter.description), brandColor: asString(companyFrontmatter.brandColor), requireBoardApprovalForNewAgents: readCompanyApprovalDefault(companyFrontmatter), }, agents: [], requiredSecrets: [], }; const warnings: string[] = []; for (const agentPath of agentPaths) { const markdownRaw = normalizedFiles[agentPath]; if (typeof markdownRaw !== "string") { warnings.push(`Referenced agent file is missing from package: ${agentPath}`); continue; } const agentDoc = parseFrontmatterMarkdown(markdownRaw); const frontmatter = agentDoc.frontmatter; const fallbackSlug = normalizeAgentUrlKey(path.posix.basename(path.posix.dirname(agentPath))) ?? "agent"; const slug = asString(frontmatter.slug) ?? fallbackSlug; const adapter = isPlainRecord(frontmatter.adapter) ? frontmatter.adapter : null; const runtime = isPlainRecord(frontmatter.runtime) ? frontmatter.runtime : null; const permissions = isPlainRecord(frontmatter.permissions) ? frontmatter.permissions : {}; const metadata = isPlainRecord(frontmatter.metadata) ? frontmatter.metadata : null; const adapterConfig = isPlainRecord(adapter?.config) ? adapter.config : isPlainRecord(frontmatter.adapterConfig) ? frontmatter.adapterConfig : {}; const runtimeConfig = runtime ?? (isPlainRecord(frontmatter.runtimeConfig) ? frontmatter.runtimeConfig : {}); const title = asString(frontmatter.title); const capabilities = asString(frontmatter.capabilities); manifest.agents.push({ slug, name: asString(frontmatter.name) ?? title ?? slug, path: agentPath, role: asString(frontmatter.role) ?? "agent", title, icon: asString(frontmatter.icon), capabilities, reportsToSlug: asString(frontmatter.reportsTo), adapterType: asString(adapter?.type) ?? asString(frontmatter.adapterType) ?? "process", adapterConfig, runtimeConfig, permissions, budgetMonthlyCents: typeof frontmatter.budgetMonthlyCents === "number" && Number.isFinite(frontmatter.budgetMonthlyCents) ? Math.max(0, Math.floor(frontmatter.budgetMonthlyCents)) : 0, metadata, }); manifest.requiredSecrets.push(...readAgentSecretRequirements(frontmatter, slug)); if (frontmatter.kind !== "agent") { warnings.push(`Agent markdown ${agentPath} does not declare kind: agent in frontmatter.`); } } manifest.requiredSecrets = dedupeRequiredSecrets(manifest.requiredSecrets); return { manifest, files: normalizedFiles, warnings, }; } function isGitCommitRef(value: string) { return /^[0-9a-f]{40}$/i.test(value.trim()); } function parseGitHubSourceUrl(rawUrl: string) { const url = new URL(rawUrl); if (url.hostname !== "github.com") { throw unprocessable("GitHub source must use github.com URL"); } const parts = url.pathname.split("/").filter(Boolean); if (parts.length < 2) { throw unprocessable("Invalid GitHub URL"); } const owner = parts[0]!; const repo = parts[1]!.replace(/\.git$/i, ""); let ref = "main"; let basePath = ""; let companyPath = "COMPANY.md"; if (parts[2] === "tree") { ref = parts[3] ?? "main"; basePath = parts.slice(4).join("/"); } else if (parts[2] === "blob") { ref = parts[3] ?? "main"; const blobPath = parts.slice(4).join("/"); if (!blobPath) { throw unprocessable("Invalid GitHub blob URL"); } companyPath = blobPath; basePath = path.posix.dirname(blobPath); if (basePath === ".") basePath = ""; } return { owner, repo, ref, basePath, companyPath }; } function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string) { const normalizedFilePath = filePath.replace(/^\/+/, ""); return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${normalizedFilePath}`; } async function readAgentInstructions(agent: AgentLike): Promise<{ body: string; warning: string | null }> { const config = agent.adapterConfig as Record; const instructionsFilePath = asString(config.instructionsFilePath); if (instructionsFilePath) { const workspaceCwd = asString(process.env.PAPERCLIP_WORKSPACE_CWD); const candidates = new Set(); if (path.isAbsolute(instructionsFilePath)) { candidates.add(instructionsFilePath); } else { if (workspaceCwd) candidates.add(path.resolve(workspaceCwd, instructionsFilePath)); candidates.add(path.resolve(process.cwd(), instructionsFilePath)); } for (const candidate of candidates) { try { const stat = await fs.stat(candidate); if (!stat.isFile() || stat.size > 1024 * 1024) continue; const body = await Promise.race([ fs.readFile(candidate, "utf8"), new Promise((_, reject) => { setTimeout(() => reject(new Error("timed out reading instructions file")), 1500); }), ]); return { body, warning: null }; } catch { // try next candidate } } } const promptTemplate = asString(config.promptTemplate); if (promptTemplate) { const warning = instructionsFilePath ? `Agent ${agent.name} instructionsFilePath was not readable; fell back to promptTemplate.` : null; return { body: promptTemplate, warning, }; } return { body: "_No AGENTS instructions were resolved from current agent config._", warning: `Agent ${agent.name} has no resolvable instructionsFilePath/promptTemplate; exported placeholder AGENTS.md.`, }; } export function companyPortabilityService(db: Db) { const companies = companyService(db); const agents = agentService(db); const access = accessService(db); async function resolveSource(source: CompanyPortabilityPreview["source"]): Promise { if (source.type === "inline") { return buildManifestFromPackageFiles( normalizeFileMap(source.files, source.rootPath), ); } if (source.type === "url") { const normalizedUrl = source.url.trim(); const companyUrl = normalizedUrl.endsWith(".md") ? normalizedUrl : new URL("COMPANY.md", normalizedUrl.endsWith("/") ? normalizedUrl : `${normalizedUrl}/`).toString(); const companyMarkdown = await fetchText(companyUrl); const files: Record = { "COMPANY.md": companyMarkdown, }; const companyDoc = parseFrontmatterMarkdown(companyMarkdown); const includeEntries = readIncludeEntries(companyDoc.frontmatter); for (const includeEntry of includeEntries) { const includePath = normalizePortablePath(includeEntry.path); if (!includePath.endsWith(".md")) continue; const includeUrl = new URL(includeEntry.path, companyUrl).toString(); files[includePath] = await fetchText(includeUrl); } return buildManifestFromPackageFiles(files); } const parsed = parseGitHubSourceUrl(source.url); let ref = parsed.ref; const warnings: string[] = []; if (!isGitCommitRef(ref)) { warnings.push("GitHub source is not pinned to a commit SHA; imports may drift if the ref changes."); } const companyRelativePath = parsed.companyPath === "COMPANY.md" ? [parsed.basePath, "COMPANY.md"].filter(Boolean).join("/") : parsed.companyPath; let companyMarkdown: string | null = null; try { companyMarkdown = await fetchOptionalText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, companyRelativePath), ); } catch (err) { if (ref === "main") { ref = "master"; warnings.push("GitHub ref main not found; falling back to master."); companyMarkdown = await fetchOptionalText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, companyRelativePath), ); } else { throw err; } } if (!companyMarkdown) { throw unprocessable("GitHub company package is missing COMPANY.md"); } const companyPath = parsed.companyPath === "COMPANY.md" ? "COMPANY.md" : normalizePortablePath(path.posix.relative(parsed.basePath || ".", parsed.companyPath)); const files: Record = { [companyPath]: companyMarkdown, }; const companyDoc = parseFrontmatterMarkdown(companyMarkdown); const includeEntries = readIncludeEntries(companyDoc.frontmatter); for (const includeEntry of includeEntries) { const repoPath = [parsed.basePath, includeEntry.path].filter(Boolean).join("/"); if (!repoPath.endsWith(".md")) continue; files[normalizePortablePath(includeEntry.path)] = await fetchText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoPath), ); } const resolved = buildManifestFromPackageFiles(files); resolved.warnings.unshift(...warnings); return resolved; } async function exportBundle( companyId: string, input: CompanyPortabilityExport, ): Promise { const include = normalizeInclude(input.include); const company = await companies.getById(companyId); if (!company) throw notFound("Company not found"); const files: Record = {}; const warnings: string[] = []; const requiredSecrets: CompanyPortabilityManifest["requiredSecrets"] = []; const rootPath = normalizeAgentUrlKey(company.name) ?? "company-package"; const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : []; const agentRows = allAgentRows.filter((agent) => agent.status !== "terminated"); if (include.agents) { const skipped = allAgentRows.length - agentRows.length; if (skipped > 0) { warnings.push(`Skipped ${skipped} terminated agent${skipped === 1 ? "" : "s"} from export.`); } } const usedSlugs = new Set(); const idToSlug = new Map(); for (const agent of agentRows) { const baseSlug = toSafeSlug(agent.name, "agent"); const slug = uniqueSlug(baseSlug, usedSlugs); idToSlug.set(agent.id, slug); } { const companyPath = "COMPANY.md"; const companyAgentSummaries = agentRows.map((agent) => ({ slug: idToSlug.get(agent.id) ?? "agent", name: agent.name, })); const includes = include.agents ? buildIncludes( companyAgentSummaries.map((agent) => `agents/${agent.slug}/AGENTS.md`), ) : []; files[companyPath] = buildMarkdown( { schema: "company-packages/v0.1", kind: "company", slug: rootPath, name: company.name, description: company.description ?? null, brandColor: company.brandColor ?? null, defaults: { requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents, }, includes, }, renderCompanyAgentsSection(companyAgentSummaries), ); } if (include.agents) { for (const agent of agentRows) { const slug = idToSlug.get(agent.id)!; const instructions = await readAgentInstructions(agent); if (instructions.warning) warnings.push(instructions.warning); const agentPath = `agents/${slug}/AGENTS.md`; const secretStart = requiredSecrets.length; const adapterDefaultRules = ADAPTER_DEFAULT_RULES_BY_TYPE[agent.adapterType] ?? []; const portableAdapterConfig = pruneDefaultLikeValue( normalizePortableConfig(agent.adapterConfig, slug, requiredSecrets), { dropFalseBooleans: true, defaultRules: adapterDefaultRules, }, ) as Record; const portableRuntimeConfig = pruneDefaultLikeValue( normalizePortableConfig(agent.runtimeConfig, slug, requiredSecrets), { dropFalseBooleans: true, defaultRules: RUNTIME_DEFAULT_RULES, }, ) as Record; const portablePermissions = pruneDefaultLikeValue(agent.permissions ?? {}, { dropFalseBooleans: true }) as Record; const agentRequiredSecrets = dedupeRequiredSecrets( requiredSecrets .slice(secretStart) .filter((requirement) => requirement.agentSlug === slug), ); const reportsToSlug = agent.reportsTo ? (idToSlug.get(agent.reportsTo) ?? null) : null; files[agentPath] = buildMarkdown( { schema: "company-packages/v0.1", name: agent.name, slug, kind: "agent", role: agent.role, title: agent.title ?? null, icon: agent.icon ?? null, capabilities: agent.capabilities ?? null, reportsTo: reportsToSlug, adapter: { type: agent.adapterType, config: portableAdapterConfig, }, runtime: portableRuntimeConfig, permissions: portablePermissions, budgetMonthlyCents: agent.budgetMonthlyCents ?? 0, metadata: (agent.metadata as Record | null) ?? null, requirements: agentRequiredSecrets.length > 0 ? { secrets: agentRequiredSecrets.map((secret) => ({ key: secret.key, description: secret.description, providerHint: secret.providerHint, })), } : {}, }, instructions.body, ); } } const resolved = buildManifestFromPackageFiles(files, { sourceLabel: { companyId: company.id, companyName: company.name, }, }); resolved.manifest.includes = include; resolved.manifest.requiredSecrets = dedupeRequiredSecrets(requiredSecrets); resolved.warnings.unshift(...warnings); return { rootPath, manifest: resolved.manifest, files, warnings: resolved.warnings, }; } async function buildPreview(input: CompanyPortabilityPreview): Promise { const include = normalizeInclude(input.include); const source = await resolveSource(input.source); const manifest = source.manifest; const collisionStrategy = input.collisionStrategy ?? DEFAULT_COLLISION_STRATEGY; const warnings = [...source.warnings]; const errors: string[] = []; if (include.company && !manifest.company) { errors.push("Manifest does not include company metadata."); } const selectedSlugs = include.agents ? ( input.agents && input.agents !== "all" ? Array.from(new Set(input.agents)) : manifest.agents.map((agent) => agent.slug) ) : []; const selectedAgents = include.agents ? manifest.agents.filter((agent) => selectedSlugs.includes(agent.slug)) : []; const selectedMissing = selectedSlugs.filter((slug) => !manifest.agents.some((agent) => agent.slug === slug)); for (const missing of selectedMissing) { errors.push(`Selected agent slug not found in manifest: ${missing}`); } if (include.agents && selectedAgents.length === 0) { warnings.push("No agents selected for import."); } for (const agent of selectedAgents) { const filePath = ensureMarkdownPath(agent.path); const markdown = source.files[filePath]; if (typeof markdown !== "string") { errors.push(`Missing markdown file for agent ${agent.slug}: ${filePath}`); continue; } const parsed = parseFrontmatterMarkdown(markdown); if (parsed.frontmatter.kind !== "agent") { warnings.push(`Agent markdown ${filePath} does not declare kind: agent in frontmatter.`); } } let targetCompanyId: string | null = null; let targetCompanyName: string | null = null; if (input.target.mode === "existing_company") { const targetCompany = await companies.getById(input.target.companyId); if (!targetCompany) throw notFound("Target company not found"); targetCompanyId = targetCompany.id; targetCompanyName = targetCompany.name; } const agentPlans: CompanyPortabilityPreviewAgentPlan[] = []; const existingSlugToAgent = new Map(); const existingSlugs = new Set(); if (input.target.mode === "existing_company") { const existingAgents = await agents.list(input.target.companyId); for (const existing of existingAgents) { const slug = normalizeAgentUrlKey(existing.name) ?? existing.id; if (!existingSlugToAgent.has(slug)) existingSlugToAgent.set(slug, existing); existingSlugs.add(slug); } } for (const manifestAgent of selectedAgents) { const existing = existingSlugToAgent.get(manifestAgent.slug) ?? null; if (!existing) { agentPlans.push({ slug: manifestAgent.slug, action: "create", plannedName: manifestAgent.name, existingAgentId: null, reason: null, }); continue; } if (collisionStrategy === "replace") { agentPlans.push({ slug: manifestAgent.slug, action: "update", plannedName: existing.name, existingAgentId: existing.id, reason: "Existing slug matched; replace strategy.", }); continue; } if (collisionStrategy === "skip") { agentPlans.push({ slug: manifestAgent.slug, action: "skip", plannedName: existing.name, existingAgentId: existing.id, reason: "Existing slug matched; skip strategy.", }); continue; } const renamed = uniqueNameBySlug(manifestAgent.name, existingSlugs); existingSlugs.add(normalizeAgentUrlKey(renamed) ?? manifestAgent.slug); agentPlans.push({ slug: manifestAgent.slug, action: "create", plannedName: renamed, existingAgentId: existing.id, reason: "Existing slug matched; rename strategy.", }); } const preview: CompanyPortabilityPreviewResult = { include, targetCompanyId, targetCompanyName, collisionStrategy, selectedAgentSlugs: selectedAgents.map((agent) => agent.slug), plan: { companyAction: input.target.mode === "new_company" ? "create" : include.company ? "update" : "none", agentPlans, }, requiredSecrets: manifest.requiredSecrets ?? [], warnings, errors, }; return { preview, source, include, collisionStrategy, selectedAgents, }; } async function previewImport(input: CompanyPortabilityPreview): Promise { const plan = await buildPreview(input); return plan.preview; } async function importBundle( input: CompanyPortabilityImport, actorUserId: string | null | undefined, ): Promise { const plan = await buildPreview(input); if (plan.preview.errors.length > 0) { throw unprocessable(`Import preview has errors: ${plan.preview.errors.join("; ")}`); } const sourceManifest = plan.source.manifest; const warnings = [...plan.preview.warnings]; const include = plan.include; let targetCompany: { id: string; name: string } | null = null; let companyAction: "created" | "updated" | "unchanged" = "unchanged"; if (input.target.mode === "new_company") { const companyName = asString(input.target.newCompanyName) ?? sourceManifest.company?.name ?? sourceManifest.source?.companyName ?? "Imported Company"; const created = await companies.create({ name: companyName, description: include.company ? (sourceManifest.company?.description ?? null) : null, brandColor: include.company ? (sourceManifest.company?.brandColor ?? null) : null, requireBoardApprovalForNewAgents: include.company ? (sourceManifest.company?.requireBoardApprovalForNewAgents ?? true) : true, }); await access.ensureMembership(created.id, "user", actorUserId ?? "board", "owner", "active"); targetCompany = created; companyAction = "created"; } else { targetCompany = await companies.getById(input.target.companyId); if (!targetCompany) throw notFound("Target company not found"); if (include.company && sourceManifest.company) { const updated = await companies.update(targetCompany.id, { name: sourceManifest.company.name, description: sourceManifest.company.description, brandColor: sourceManifest.company.brandColor, requireBoardApprovalForNewAgents: sourceManifest.company.requireBoardApprovalForNewAgents, }); targetCompany = updated ?? targetCompany; companyAction = "updated"; } } if (!targetCompany) throw notFound("Target company not found"); const resultAgents: CompanyPortabilityImportResult["agents"] = []; const importedSlugToAgentId = new Map(); const existingSlugToAgentId = new Map(); const existingAgents = await agents.list(targetCompany.id); for (const existing of existingAgents) { existingSlugToAgentId.set(normalizeAgentUrlKey(existing.name) ?? existing.id, existing.id); } if (include.agents) { for (const planAgent of plan.preview.plan.agentPlans) { const manifestAgent = plan.selectedAgents.find((agent) => agent.slug === planAgent.slug); if (!manifestAgent) continue; if (planAgent.action === "skip") { resultAgents.push({ slug: planAgent.slug, id: planAgent.existingAgentId, action: "skipped", name: planAgent.plannedName, reason: planAgent.reason, }); continue; } const markdownRaw = plan.source.files[manifestAgent.path]; if (!markdownRaw) { warnings.push(`Missing AGENTS markdown for ${manifestAgent.slug}; imported without prompt template.`); } const markdown = markdownRaw ? parseFrontmatterMarkdown(markdownRaw) : { frontmatter: {}, body: "" }; const adapterConfig = { ...manifestAgent.adapterConfig, promptTemplate: markdown.body || asString((manifestAgent.adapterConfig as Record).promptTemplate) || "", } as Record; delete adapterConfig.instructionsFilePath; const patch = { name: planAgent.plannedName, role: manifestAgent.role, title: manifestAgent.title, icon: manifestAgent.icon, capabilities: manifestAgent.capabilities, reportsTo: null, adapterType: manifestAgent.adapterType, adapterConfig, runtimeConfig: manifestAgent.runtimeConfig, budgetMonthlyCents: manifestAgent.budgetMonthlyCents, permissions: manifestAgent.permissions, metadata: manifestAgent.metadata, }; if (planAgent.action === "update" && planAgent.existingAgentId) { const updated = await agents.update(planAgent.existingAgentId, patch); if (!updated) { warnings.push(`Skipped update for missing agent ${planAgent.existingAgentId}.`); resultAgents.push({ slug: planAgent.slug, id: null, action: "skipped", name: planAgent.plannedName, reason: "Existing target agent not found.", }); continue; } importedSlugToAgentId.set(planAgent.slug, updated.id); existingSlugToAgentId.set(normalizeAgentUrlKey(updated.name) ?? updated.id, updated.id); resultAgents.push({ slug: planAgent.slug, id: updated.id, action: "updated", name: updated.name, reason: planAgent.reason, }); continue; } const created = await agents.create(targetCompany.id, patch); importedSlugToAgentId.set(planAgent.slug, created.id); existingSlugToAgentId.set(normalizeAgentUrlKey(created.name) ?? created.id, created.id); resultAgents.push({ slug: planAgent.slug, id: created.id, action: "created", name: created.name, reason: planAgent.reason, }); } // Apply reporting links once all imported agent ids are available. for (const manifestAgent of plan.selectedAgents) { const agentId = importedSlugToAgentId.get(manifestAgent.slug); if (!agentId) continue; const managerSlug = manifestAgent.reportsToSlug; if (!managerSlug) continue; const managerId = importedSlugToAgentId.get(managerSlug) ?? existingSlugToAgentId.get(managerSlug) ?? null; if (!managerId || managerId === agentId) continue; try { await agents.update(agentId, { reportsTo: managerId }); } catch { warnings.push(`Could not assign manager ${managerSlug} for imported agent ${manifestAgent.slug}.`); } } } return { company: { id: targetCompany.id, name: targetCompany.name, action: companyAction, }, agents: resultAgents, requiredSecrets: sourceManifest.requiredSecrets ?? [], warnings, }; } return { exportBundle, previewImport, importBundle, }; }