feat: company portability — export/import companies and agents
Add company export, import preview, and import endpoints with manifest- based bundle format. Includes URL key utilities for agents and projects, collision detection/rename strategies, and secret requirement tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
22
packages/shared/src/agent-url-key.ts
Normal file
22
packages/shared/src/agent-url-key.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const AGENT_URL_KEY_DELIM_RE = /[^a-z0-9]+/g;
|
||||
const AGENT_URL_KEY_TRIM_RE = /^-+|-+$/g;
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function isUuidLike(value: string | null | undefined): boolean {
|
||||
if (typeof value !== "string") return false;
|
||||
return UUID_RE.test(value.trim());
|
||||
}
|
||||
|
||||
export function normalizeAgentUrlKey(value: string | null | undefined): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const normalized = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(AGENT_URL_KEY_DELIM_RE, "-")
|
||||
.replace(AGENT_URL_KEY_TRIM_RE, "");
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
export function deriveAgentUrlKey(name: string | null | undefined, fallback?: string | null): string {
|
||||
return normalizeAgentUrlKey(name) ?? normalizeAgentUrlKey(fallback) ?? "agent";
|
||||
}
|
||||
@@ -101,6 +101,22 @@ export type {
|
||||
Invite,
|
||||
JoinRequest,
|
||||
InstanceUserRoleGrant,
|
||||
CompanyPortabilityInclude,
|
||||
CompanyPortabilitySecretRequirement,
|
||||
CompanyPortabilityCompanyManifestEntry,
|
||||
CompanyPortabilityAgentManifestEntry,
|
||||
CompanyPortabilityManifest,
|
||||
CompanyPortabilityExportResult,
|
||||
CompanyPortabilitySource,
|
||||
CompanyPortabilityImportTarget,
|
||||
CompanyPortabilityAgentSelection,
|
||||
CompanyPortabilityCollisionStrategy,
|
||||
CompanyPortabilityPreviewRequest,
|
||||
CompanyPortabilityPreviewAgentPlan,
|
||||
CompanyPortabilityPreviewResult,
|
||||
CompanyPortabilityImportRequest,
|
||||
CompanyPortabilityImportResult,
|
||||
CompanyPortabilityExportRequest,
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
@@ -193,9 +209,25 @@ export {
|
||||
type ClaimJoinRequestApiKey,
|
||||
type UpdateMemberPermissions,
|
||||
type UpdateUserCompanyAccess,
|
||||
portabilityIncludeSchema,
|
||||
portabilitySecretRequirementSchema,
|
||||
portabilityCompanyManifestEntrySchema,
|
||||
portabilityAgentManifestEntrySchema,
|
||||
portabilityManifestSchema,
|
||||
portabilitySourceSchema,
|
||||
portabilityTargetSchema,
|
||||
portabilityAgentSelectionSchema,
|
||||
portabilityCollisionStrategySchema,
|
||||
companyPortabilityExportSchema,
|
||||
companyPortabilityPreviewSchema,
|
||||
companyPortabilityImportSchema,
|
||||
type CompanyPortabilityExport,
|
||||
type CompanyPortabilityPreview,
|
||||
type CompanyPortabilityImport,
|
||||
} from "./validators/index.js";
|
||||
|
||||
export { API_PREFIX, API } from "./api.js";
|
||||
export { normalizeAgentUrlKey, deriveAgentUrlKey } from "./agent-url-key.js";
|
||||
|
||||
export {
|
||||
paperclipConfigSchema,
|
||||
|
||||
16
packages/shared/src/project-url-key.ts
Normal file
16
packages/shared/src/project-url-key.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const PROJECT_URL_KEY_DELIM_RE = /[^a-z0-9]+/g;
|
||||
const PROJECT_URL_KEY_TRIM_RE = /^-+|-+$/g;
|
||||
|
||||
export function normalizeProjectUrlKey(value: string | null | undefined): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const normalized = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(PROJECT_URL_KEY_DELIM_RE, "-")
|
||||
.replace(PROJECT_URL_KEY_TRIM_RE, "");
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
export function deriveProjectUrlKey(name: string | null | undefined, fallback?: string | null): string {
|
||||
return normalizeProjectUrlKey(name) ?? normalizeProjectUrlKey(fallback) ?? "project";
|
||||
}
|
||||
138
packages/shared/src/types/company-portability.ts
Normal file
138
packages/shared/src/types/company-portability.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
export interface CompanyPortabilityInclude {
|
||||
company: boolean;
|
||||
agents: boolean;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilitySecretRequirement {
|
||||
key: string;
|
||||
description: string | null;
|
||||
agentSlug: string | null;
|
||||
providerHint: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityCompanyManifestEntry {
|
||||
path: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
brandColor: string | null;
|
||||
requireBoardApprovalForNewAgents: boolean;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityAgentManifestEntry {
|
||||
slug: string;
|
||||
name: string;
|
||||
path: string;
|
||||
role: string;
|
||||
title: string | null;
|
||||
icon: string | null;
|
||||
capabilities: string | null;
|
||||
reportsToSlug: string | null;
|
||||
adapterType: string;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
runtimeConfig: Record<string, unknown>;
|
||||
permissions: Record<string, unknown>;
|
||||
budgetMonthlyCents: number;
|
||||
metadata: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityManifest {
|
||||
schemaVersion: number;
|
||||
generatedAt: string;
|
||||
source: {
|
||||
companyId: string;
|
||||
companyName: string;
|
||||
} | null;
|
||||
includes: CompanyPortabilityInclude;
|
||||
company: CompanyPortabilityCompanyManifestEntry | null;
|
||||
agents: CompanyPortabilityAgentManifestEntry[];
|
||||
requiredSecrets: CompanyPortabilitySecretRequirement[];
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityExportResult {
|
||||
manifest: CompanyPortabilityManifest;
|
||||
files: Record<string, string>;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export type CompanyPortabilitySource =
|
||||
| {
|
||||
type: "inline";
|
||||
manifest: CompanyPortabilityManifest;
|
||||
files: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
type: "url";
|
||||
url: string;
|
||||
}
|
||||
| {
|
||||
type: "github";
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type CompanyPortabilityImportTarget =
|
||||
| {
|
||||
mode: "new_company";
|
||||
newCompanyName?: string | null;
|
||||
}
|
||||
| {
|
||||
mode: "existing_company";
|
||||
companyId: string;
|
||||
};
|
||||
|
||||
export type CompanyPortabilityAgentSelection = "all" | string[];
|
||||
|
||||
export type CompanyPortabilityCollisionStrategy = "rename" | "skip" | "replace";
|
||||
|
||||
export interface CompanyPortabilityPreviewRequest {
|
||||
source: CompanyPortabilitySource;
|
||||
include?: Partial<CompanyPortabilityInclude>;
|
||||
target: CompanyPortabilityImportTarget;
|
||||
agents?: CompanyPortabilityAgentSelection;
|
||||
collisionStrategy?: CompanyPortabilityCollisionStrategy;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityPreviewAgentPlan {
|
||||
slug: string;
|
||||
action: "create" | "update" | "skip";
|
||||
plannedName: string;
|
||||
existingAgentId: string | null;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityPreviewResult {
|
||||
include: CompanyPortabilityInclude;
|
||||
targetCompanyId: string | null;
|
||||
targetCompanyName: string | null;
|
||||
collisionStrategy: CompanyPortabilityCollisionStrategy;
|
||||
selectedAgentSlugs: string[];
|
||||
plan: {
|
||||
companyAction: "none" | "create" | "update";
|
||||
agentPlans: CompanyPortabilityPreviewAgentPlan[];
|
||||
};
|
||||
requiredSecrets: CompanyPortabilitySecretRequirement[];
|
||||
warnings: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityImportRequest extends CompanyPortabilityPreviewRequest {}
|
||||
|
||||
export interface CompanyPortabilityImportResult {
|
||||
company: {
|
||||
id: string;
|
||||
name: string;
|
||||
action: "created" | "updated" | "unchanged";
|
||||
};
|
||||
agents: {
|
||||
slug: string;
|
||||
id: string | null;
|
||||
action: "created" | "updated" | "skipped";
|
||||
name: string;
|
||||
reason: string | null;
|
||||
}[];
|
||||
requiredSecrets: CompanyPortabilitySecretRequirement[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityExportRequest {
|
||||
include?: Partial<CompanyPortabilityInclude>;
|
||||
}
|
||||
@@ -52,3 +52,21 @@ export type {
|
||||
JoinRequest,
|
||||
InstanceUserRoleGrant,
|
||||
} from "./access.js";
|
||||
export type {
|
||||
CompanyPortabilityInclude,
|
||||
CompanyPortabilitySecretRequirement,
|
||||
CompanyPortabilityCompanyManifestEntry,
|
||||
CompanyPortabilityAgentManifestEntry,
|
||||
CompanyPortabilityManifest,
|
||||
CompanyPortabilityExportResult,
|
||||
CompanyPortabilitySource,
|
||||
CompanyPortabilityImportTarget,
|
||||
CompanyPortabilityAgentSelection,
|
||||
CompanyPortabilityCollisionStrategy,
|
||||
CompanyPortabilityPreviewRequest,
|
||||
CompanyPortabilityPreviewAgentPlan,
|
||||
CompanyPortabilityPreviewResult,
|
||||
CompanyPortabilityImportRequest,
|
||||
CompanyPortabilityImportResult,
|
||||
CompanyPortabilityExportRequest,
|
||||
} from "./company-portability.js";
|
||||
|
||||
112
packages/shared/src/validators/company-portability.ts
Normal file
112
packages/shared/src/validators/company-portability.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const portabilityIncludeSchema = z
|
||||
.object({
|
||||
company: z.boolean().optional(),
|
||||
agents: z.boolean().optional(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const portabilitySecretRequirementSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
description: z.string().nullable(),
|
||||
agentSlug: z.string().min(1).nullable(),
|
||||
providerHint: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const portabilityCompanyManifestEntrySchema = z.object({
|
||||
path: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().nullable(),
|
||||
brandColor: z.string().nullable(),
|
||||
requireBoardApprovalForNewAgents: z.boolean(),
|
||||
});
|
||||
|
||||
export const portabilityAgentManifestEntrySchema = z.object({
|
||||
slug: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
path: z.string().min(1),
|
||||
role: z.string().min(1),
|
||||
title: z.string().nullable(),
|
||||
icon: z.string().nullable(),
|
||||
capabilities: z.string().nullable(),
|
||||
reportsToSlug: z.string().min(1).nullable(),
|
||||
adapterType: z.string().min(1),
|
||||
adapterConfig: z.record(z.unknown()),
|
||||
runtimeConfig: z.record(z.unknown()),
|
||||
permissions: z.record(z.unknown()),
|
||||
budgetMonthlyCents: z.number().int().nonnegative(),
|
||||
metadata: z.record(z.unknown()).nullable(),
|
||||
});
|
||||
|
||||
export const portabilityManifestSchema = z.object({
|
||||
schemaVersion: z.number().int().positive(),
|
||||
generatedAt: z.string().datetime(),
|
||||
source: z
|
||||
.object({
|
||||
companyId: z.string().uuid(),
|
||||
companyName: z.string().min(1),
|
||||
})
|
||||
.nullable(),
|
||||
includes: z.object({
|
||||
company: z.boolean(),
|
||||
agents: z.boolean(),
|
||||
}),
|
||||
company: portabilityCompanyManifestEntrySchema.nullable(),
|
||||
agents: z.array(portabilityAgentManifestEntrySchema),
|
||||
requiredSecrets: z.array(portabilitySecretRequirementSchema).default([]),
|
||||
});
|
||||
|
||||
export const portabilitySourceSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("inline"),
|
||||
manifest: portabilityManifestSchema,
|
||||
files: z.record(z.string()),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("url"),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("github"),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const portabilityTargetSchema = z.discriminatedUnion("mode", [
|
||||
z.object({
|
||||
mode: z.literal("new_company"),
|
||||
newCompanyName: z.string().min(1).optional().nullable(),
|
||||
}),
|
||||
z.object({
|
||||
mode: z.literal("existing_company"),
|
||||
companyId: z.string().uuid(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const portabilityAgentSelectionSchema = z.union([
|
||||
z.literal("all"),
|
||||
z.array(z.string().min(1)),
|
||||
]);
|
||||
|
||||
export const portabilityCollisionStrategySchema = z.enum(["rename", "skip", "replace"]);
|
||||
|
||||
export const companyPortabilityExportSchema = z.object({
|
||||
include: portabilityIncludeSchema.optional(),
|
||||
});
|
||||
|
||||
export type CompanyPortabilityExport = z.infer<typeof companyPortabilityExportSchema>;
|
||||
|
||||
export const companyPortabilityPreviewSchema = z.object({
|
||||
source: portabilitySourceSchema,
|
||||
include: portabilityIncludeSchema.optional(),
|
||||
target: portabilityTargetSchema,
|
||||
agents: portabilityAgentSelectionSchema.optional(),
|
||||
collisionStrategy: portabilityCollisionStrategySchema.optional(),
|
||||
});
|
||||
|
||||
export type CompanyPortabilityPreview = z.infer<typeof companyPortabilityPreviewSchema>;
|
||||
|
||||
export const companyPortabilityImportSchema = companyPortabilityPreviewSchema;
|
||||
|
||||
export type CompanyPortabilityImport = z.infer<typeof companyPortabilityImportSchema>;
|
||||
@@ -112,3 +112,21 @@ export {
|
||||
type UpdateMemberPermissions,
|
||||
type UpdateUserCompanyAccess,
|
||||
} from "./access.js";
|
||||
|
||||
export {
|
||||
portabilityIncludeSchema,
|
||||
portabilitySecretRequirementSchema,
|
||||
portabilityCompanyManifestEntrySchema,
|
||||
portabilityAgentManifestEntrySchema,
|
||||
portabilityManifestSchema,
|
||||
portabilitySourceSchema,
|
||||
portabilityTargetSchema,
|
||||
portabilityAgentSelectionSchema,
|
||||
portabilityCollisionStrategySchema,
|
||||
companyPortabilityExportSchema,
|
||||
companyPortabilityPreviewSchema,
|
||||
companyPortabilityImportSchema,
|
||||
type CompanyPortabilityExport,
|
||||
type CompanyPortabilityPreview,
|
||||
type CompanyPortabilityImport,
|
||||
} from "./company-portability.js";
|
||||
|
||||
Reference in New Issue
Block a user