mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Add clip manifest redaction foundation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
63821bfe4c
commit
cfb42dbb95
3 files changed
+1329
No files matched your search
@@ -0,0 +1,362 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CompanyPortabilityManifest } from "./types/company-portability.js";
|
||||
import {
|
||||
buildAgentClipSnapshot,
|
||||
buildBundleClipSnapshot,
|
||||
buildRoutineClipSnapshot,
|
||||
buildSkillClipSnapshot,
|
||||
buildTeamClipSnapshot,
|
||||
clipManifestSchema,
|
||||
} from "./clip-manifest.js";
|
||||
|
||||
const exportedAt = "2026-05-16T00:00:00.000Z";
|
||||
|
||||
function fixtureManifest(): CompanyPortabilityManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
generatedAt: "2026-05-15T00:00:00.000Z",
|
||||
source: {
|
||||
companyId: "67f12493-f655-41e7-b5f5-dbe2f44dd7ba",
|
||||
companyName: "Private Customer Ops",
|
||||
},
|
||||
includes: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
company: {
|
||||
path: "COMPANY.md",
|
||||
name: "Private Customer Ops",
|
||||
description: "Routes support for https://localhost:8443/internal and customer jane@example.com",
|
||||
brandColor: "#111111",
|
||||
logoPath: null,
|
||||
attachmentMaxBytes: null,
|
||||
requireBoardApprovalForNewAgents: true,
|
||||
feedbackDataSharingEnabled: false,
|
||||
feedbackDataSharingConsentAt: null,
|
||||
feedbackDataSharingConsentByUserId: null,
|
||||
feedbackDataSharingTermsVersion: null,
|
||||
},
|
||||
sidebar: {
|
||||
agents: ["ceo", "support"],
|
||||
projects: ["support"],
|
||||
},
|
||||
agents: [
|
||||
{
|
||||
slug: "support",
|
||||
name: "Support Engineer",
|
||||
path: "agents/support/AGENTS.md",
|
||||
skills: ["triage"],
|
||||
role: "engineer",
|
||||
title: "Support Engineer",
|
||||
icon: "code",
|
||||
capabilities: "Uses bearer token Bearer shh-secret-token for examples",
|
||||
reportsToSlug: "ceo",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {
|
||||
cwd: "/Users/dotta/private-support",
|
||||
command: "/Users/dotta/.local/bin/codex",
|
||||
env: {
|
||||
GH_TOKEN: {
|
||||
type: "secret_ref",
|
||||
secretId: "secret-gh-token",
|
||||
version: "latest",
|
||||
},
|
||||
INTERNAL_URL: {
|
||||
type: "plain",
|
||||
value: "http://localhost:3100/private",
|
||||
},
|
||||
SAFE_MODE: {
|
||||
type: "plain",
|
||||
value: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
github: {
|
||||
issues: {
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
browser: true,
|
||||
},
|
||||
budgetMonthlyCents: 1200,
|
||||
metadata: {
|
||||
privateRepoUrl: "https://git.internal/example/private",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "ceo",
|
||||
name: "CEO",
|
||||
path: "agents/ceo/AGENTS.md",
|
||||
skills: [],
|
||||
role: "ceo",
|
||||
title: "Chief Executive Officer",
|
||||
icon: null,
|
||||
capabilities: "Owns strategy",
|
||||
reportsToSlug: null,
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {
|
||||
promptTemplate: "You are CEO.",
|
||||
},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
budgetMonthlyCents: 0,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
key: "triage",
|
||||
slug: "triage",
|
||||
name: "Triage",
|
||||
path: "skills/triage/SKILL.md",
|
||||
description: "Classifies inbound support work.",
|
||||
sourceType: "bundled",
|
||||
sourceLocator: null,
|
||||
sourceRef: null,
|
||||
trustLevel: "company",
|
||||
compatibility: null,
|
||||
metadata: null,
|
||||
fileInventory: [{ path: "skills/triage/SKILL.md", kind: "skill" }],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
slug: "support",
|
||||
name: "Support",
|
||||
path: "projects/support/PROJECT.md",
|
||||
description: "Private support project",
|
||||
ownerAgentSlug: "support",
|
||||
leadAgentSlug: "support",
|
||||
targetDate: null,
|
||||
color: null,
|
||||
status: "in_progress",
|
||||
env: {
|
||||
SUPPORT_DSN: {
|
||||
type: "plain",
|
||||
value: "postgres://user:pass@localhost:5432/app",
|
||||
},
|
||||
},
|
||||
executionWorkspacePolicy: null,
|
||||
workspaces: [
|
||||
{
|
||||
key: "support-repo",
|
||||
name: "Support repo",
|
||||
sourceType: "git",
|
||||
repoUrl: "https://github.com/paperclipai/paperclip",
|
||||
repoRef: null,
|
||||
defaultRef: "main",
|
||||
visibility: "public",
|
||||
setupCommand: "pnpm install",
|
||||
cleanupCommand: null,
|
||||
metadata: null,
|
||||
isPrimary: true,
|
||||
},
|
||||
],
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
issues: [
|
||||
{
|
||||
slug: "triage-hourly",
|
||||
identifier: "PAP-1",
|
||||
title: "Triage hourly",
|
||||
path: "tasks/triage-hourly/TASK.md",
|
||||
projectSlug: "support",
|
||||
projectWorkspaceKey: "support-repo",
|
||||
assigneeAgentSlug: "support",
|
||||
description: "Runs on a webhook and schedule.",
|
||||
recurring: true,
|
||||
routine: {
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [
|
||||
{
|
||||
name: "CUSTOMER_EMAIL",
|
||||
type: "text",
|
||||
label: "Customer email",
|
||||
required: false,
|
||||
defaultValue: "jane@example.com",
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
triggers: [
|
||||
{
|
||||
kind: "webhook",
|
||||
label: "Inbound webhook",
|
||||
enabled: true,
|
||||
cronExpression: null,
|
||||
timezone: null,
|
||||
signingMode: "hmac_sha256",
|
||||
replayWindowSec: 120,
|
||||
},
|
||||
{
|
||||
kind: "schedule",
|
||||
label: "Hourly",
|
||||
enabled: true,
|
||||
cronExpression: "0 * * * *",
|
||||
timezone: "UTC",
|
||||
signingMode: null,
|
||||
replayWindowSec: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
legacyRecurrence: null,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
labelIds: [],
|
||||
billingCode: null,
|
||||
executionWorkspaceSettings: null,
|
||||
assigneeAdapterOverrides: null,
|
||||
comments: [
|
||||
{
|
||||
body: "Private comment with sk-testsecret1234567890",
|
||||
authorType: "agent",
|
||||
authorAgentSlug: "support",
|
||||
authorUserId: null,
|
||||
presentation: null,
|
||||
metadata: null,
|
||||
createdAt: exportedAt,
|
||||
},
|
||||
],
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
envInputs: [
|
||||
{
|
||||
key: "GH_TOKEN",
|
||||
description: "GitHub token",
|
||||
agentSlug: "support",
|
||||
projectSlug: null,
|
||||
kind: "secret",
|
||||
requirement: "required",
|
||||
defaultValue: "ghp_supersecretsecret",
|
||||
portability: "portable",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const clip = {
|
||||
slug: "support-triage",
|
||||
revisionNumber: 1,
|
||||
title: "Support Triage",
|
||||
summary: "Routes support tickets.",
|
||||
visibility: "public" as const,
|
||||
creator: {
|
||||
profileId: "creator_1",
|
||||
handle: "paperclip",
|
||||
displayName: "Paperclip",
|
||||
},
|
||||
};
|
||||
|
||||
describe("clip manifest foundation", () => {
|
||||
it("builds a deterministic versioned bundle manifest with checksums and dependencies", () => {
|
||||
const first = buildBundleClipSnapshot({
|
||||
clip,
|
||||
artifact: { manifest: fixtureManifest(), files: { "COMPANY.md": "# Company" } },
|
||||
source: { exportedAt },
|
||||
social: {
|
||||
sourceUrl: "https://paperclip.ing/clips/support-triage",
|
||||
revisionUrl: "https://paperclip.ing/clips/support-triage/revisions/1",
|
||||
},
|
||||
});
|
||||
const second = buildBundleClipSnapshot({
|
||||
clip,
|
||||
artifact: { manifest: fixtureManifest(), files: { "COMPANY.md": "# Company" } },
|
||||
source: { exportedAt },
|
||||
social: {
|
||||
sourceUrl: "https://paperclip.ing/clips/support-triage",
|
||||
revisionUrl: "https://paperclip.ing/clips/support-triage/revisions/1",
|
||||
},
|
||||
});
|
||||
|
||||
expect(clipManifestSchema.parse(first)).toEqual(first);
|
||||
expect(first.checksums).toEqual(second.checksums);
|
||||
expect(first.clip.revisionId).toEqual(second.clip.revisionId);
|
||||
expect(first.schema).toBe("paperclip.clip/v1");
|
||||
expect(first.artifact.version).toBe("agentcompanies/v1-draft");
|
||||
expect(first.dependencies.adapters.map((entry) => entry.type)).toEqual(["claude_local", "codex_local"]);
|
||||
expect(first.dependencies.skills.map((entry) => entry.key)).toEqual(["triage"]);
|
||||
expect(first.dependencies.secrets.map((entry) => entry.key)).toContain("GH_TOKEN");
|
||||
expect(first.dependencies.permissions.map((entry) => entry.capability)).toContain("github.issues.write");
|
||||
expect(first.dependencies.runtime).toMatchObject({
|
||||
browser: true,
|
||||
filesystem: "declared",
|
||||
webhooks: true,
|
||||
recurringRoutines: true,
|
||||
});
|
||||
expect(first.dependencies.budgetHints.monthlyCents).toBe(1200);
|
||||
});
|
||||
|
||||
it("omits secrets, local paths, private URLs, raw comments, and routine webhook signing details", () => {
|
||||
const manifest = buildRoutineClipSnapshot({
|
||||
clip: { ...clip, slug: "support-routine" },
|
||||
artifact: { manifest: fixtureManifest() },
|
||||
source: { exportedAt },
|
||||
routineSlug: "triage-hourly",
|
||||
});
|
||||
const serialized = JSON.stringify(manifest);
|
||||
|
||||
expect(serialized).not.toContain("secret-gh-token");
|
||||
expect(serialized).not.toContain("ghp_supersecretsecret");
|
||||
expect(serialized).not.toContain("sk-testsecret");
|
||||
expect(serialized).not.toContain("/Users/dotta");
|
||||
expect(serialized).not.toContain("localhost");
|
||||
expect(serialized).not.toContain("https://git.internal");
|
||||
expect(serialized).not.toContain("hmac_sha256");
|
||||
expect(manifest.artifact.payload.manifest.issues[0]?.comments).toEqual([]);
|
||||
expect(manifest.artifact.payload.manifest.issues[0]?.routine?.triggers.every((trigger) => !trigger.enabled)).toBe(true);
|
||||
expect(manifest.security.routinePolicy).toEqual({
|
||||
importedTriggersEnabledByDefault: false,
|
||||
webhookSecretsRegenerated: true,
|
||||
});
|
||||
expect(manifest.security.redactionReport.summary.omitted).toBeGreaterThan(0);
|
||||
expect(manifest.security.redactionReport.entries.map((entry) => entry.path)).toContain("issues[0].comments");
|
||||
});
|
||||
|
||||
it("scopes agent, team, skill, and routine snapshots to the selected portable object", () => {
|
||||
const agent = buildAgentClipSnapshot({
|
||||
clip: { ...clip, slug: "support-agent" },
|
||||
artifact: { manifest: fixtureManifest() },
|
||||
source: { exportedAt },
|
||||
agentSlug: "support",
|
||||
});
|
||||
const team = buildTeamClipSnapshot({
|
||||
clip: { ...clip, slug: "support-team" },
|
||||
artifact: { manifest: fixtureManifest() },
|
||||
source: { exportedAt },
|
||||
teamRootAgentSlug: "ceo",
|
||||
});
|
||||
const skill = buildSkillClipSnapshot({
|
||||
clip: { ...clip, slug: "triage-skill" },
|
||||
artifact: { manifest: fixtureManifest() },
|
||||
source: { exportedAt },
|
||||
skillKey: "triage",
|
||||
});
|
||||
const routine = buildRoutineClipSnapshot({
|
||||
clip: { ...clip, slug: "triage-routine" },
|
||||
artifact: { manifest: fixtureManifest() },
|
||||
source: { exportedAt },
|
||||
routineSlug: "triage-hourly",
|
||||
});
|
||||
|
||||
expect(agent.clip.type).toBe("agent");
|
||||
expect(agent.artifact.payload.manifest.agents.map((entry) => entry.slug)).toEqual(["support"]);
|
||||
expect(team.clip.type).toBe("team");
|
||||
expect(team.artifact.payload.manifest.agents.map((entry) => entry.slug)).toEqual(["ceo", "support"]);
|
||||
expect(skill.clip.type).toBe("skill");
|
||||
expect(skill.artifact.payload.manifest.skills.map((entry) => entry.key)).toEqual(["triage"]);
|
||||
expect(skill.artifact.payload.manifest.agents).toEqual([]);
|
||||
expect(routine.clip.type).toBe("routine");
|
||||
expect(routine.artifact.payload.manifest.issues.map((entry) => entry.slug)).toEqual(["triage-hourly"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,934 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import type {
|
||||
CompanyPortabilityAgentManifestEntry,
|
||||
CompanyPortabilityFileEntry,
|
||||
CompanyPortabilityManifest,
|
||||
CompanyPortabilitySkillManifestEntry,
|
||||
} from "./types/company-portability.js";
|
||||
|
||||
export const CLIP_MANIFEST_SCHEMA = "paperclip.clip/v1" as const;
|
||||
export const CLIP_MANIFEST_VERSION = 1 as const;
|
||||
export const CLIP_ARTIFACT_FORMAT = "agentcompanies" as const;
|
||||
export const CLIP_ARTIFACT_VERSION = "agentcompanies/v1-draft" as const;
|
||||
|
||||
const SECRET_KEY_PATTERN = /(?:api[_-]?key|auth|authorization|bearer|cookie|credential|dsn|oauth|password|private[_-]?key|secret|signing[_-]?secret|token|webhook[_-]?secret)/i;
|
||||
const LOCAL_PATH_PATTERN = /(?:^|["'\s])(?:\/Users\/|\/home\/|\/var\/folders\/|[A-Za-z]:\\)/;
|
||||
const PRIVATE_URL_PATTERN = /\b(?:https?:\/\/)(?:localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}|[^/\s]+\.local|[^/\s]+\.internal|[^/\s]+\.test)(?::\d+)?(?:[/?#][^\s]*)?/i;
|
||||
const SECRET_VALUE_PATTERN = /(?:Bearer\s+[A-Za-z0-9._~+/-]+=*|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_]{16,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/;
|
||||
|
||||
export type ClipManifestType = "team" | "agent" | "skill" | "routine" | "bundle";
|
||||
export type ClipManifestVisibility = "private_share" | "unlisted" | "public";
|
||||
export type ClipReviewState = "unreviewed" | "automated_checked" | "security_reviewed" | "blocked";
|
||||
export type ClipValidationStatus = "not_run" | "passed" | "failed" | "stale";
|
||||
export type ClipRedactionOutcome = "allow" | "redact" | "summarize" | "omit";
|
||||
export type ClipRuntimeFilesystem = "none" | "declared" | "broad";
|
||||
|
||||
export interface ClipManifestCreator {
|
||||
profileId: string | null;
|
||||
handle: string | null;
|
||||
displayName: string | null;
|
||||
}
|
||||
|
||||
export interface ClipManifestClip {
|
||||
id: string;
|
||||
slug: string;
|
||||
type: ClipManifestType;
|
||||
revisionId: string;
|
||||
revisionNumber: number;
|
||||
manifestVersion: typeof CLIP_MANIFEST_VERSION;
|
||||
title: string;
|
||||
summary: string;
|
||||
visibility: ClipManifestVisibility;
|
||||
creator: ClipManifestCreator | null;
|
||||
}
|
||||
|
||||
export interface ClipDependencyGraph {
|
||||
adapters: Array<{ type: string; required: boolean; sourceRefs: string[]; note: string | null }>;
|
||||
plugins: Array<{ key: string; requirement: "required" | "optional"; sourceRefs: string[]; note: string | null }>;
|
||||
skills: Array<{ key: string; slug: string; requirement: "required" | "optional"; sourceRefs: string[] }>;
|
||||
secrets: Array<{ key: string; kind: "secret"; requirement: "required" | "optional"; description: string | null; sourceRefs: string[] }>;
|
||||
permissions: Array<{ capability: string; reason: string | null; sourceRefs: string[] }>;
|
||||
runtime: {
|
||||
localShell: boolean;
|
||||
browser: boolean;
|
||||
filesystem: ClipRuntimeFilesystem;
|
||||
webhooks: boolean;
|
||||
recurringRoutines: boolean;
|
||||
};
|
||||
workspaces: Array<{ key: string; repoUrlRequired: boolean; pinnedRefRecommended: boolean; sourceRefs: string[] }>;
|
||||
budgetHints: {
|
||||
monthlyCents: number;
|
||||
sourceRefs: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClipRedactionReportEntry {
|
||||
path: string;
|
||||
outcome: ClipRedactionOutcome;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface ClipRedactionReport {
|
||||
schema: "paperclip.clip.redaction/v1";
|
||||
generatedAt: string;
|
||||
entries: ClipRedactionReportEntry[];
|
||||
summary: {
|
||||
allowed: number;
|
||||
redacted: number;
|
||||
summarized: number;
|
||||
omitted: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClipManifest {
|
||||
schema: typeof CLIP_MANIFEST_SCHEMA;
|
||||
clip: ClipManifestClip;
|
||||
publication: {
|
||||
source: {
|
||||
kind: "paperclip_company_object";
|
||||
objectType: ClipManifestType;
|
||||
exportedAt: string;
|
||||
};
|
||||
compatibility: {
|
||||
paperclip: string | null;
|
||||
manifest: typeof CLIP_ARTIFACT_VERSION;
|
||||
};
|
||||
};
|
||||
artifact: {
|
||||
format: typeof CLIP_ARTIFACT_FORMAT;
|
||||
version: typeof CLIP_ARTIFACT_VERSION;
|
||||
checksum: string;
|
||||
entrypoint: "COMPANY.md" | "TEAM.md" | "AGENTS.md" | "TASK.md" | "SKILL.md";
|
||||
paperclipExtension: ".paperclip.yaml";
|
||||
payload: {
|
||||
manifest: CompanyPortabilityManifest;
|
||||
};
|
||||
};
|
||||
dependencies: ClipDependencyGraph;
|
||||
security: {
|
||||
redactionReport: ClipRedactionReport;
|
||||
dangerousCapabilities: string[];
|
||||
routinePolicy: {
|
||||
importedTriggersEnabledByDefault: false;
|
||||
webhookSecretsRegenerated: true;
|
||||
};
|
||||
reviewState: ClipReviewState;
|
||||
};
|
||||
verification: {
|
||||
expectedFirstRun: Record<string, unknown> | null;
|
||||
sampleOutputs: Array<Record<string, unknown>>;
|
||||
validationStatus: ClipValidationStatus;
|
||||
};
|
||||
social: {
|
||||
sourceUrl: string | null;
|
||||
revisionUrl: string | null;
|
||||
};
|
||||
provenance: {
|
||||
publishedByProfileId: string | null;
|
||||
revisionPublishedAt: string;
|
||||
previousRevisionId: string | null;
|
||||
};
|
||||
checksums: {
|
||||
artifact: string;
|
||||
redactionReport: string;
|
||||
manifest: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClipSnapshotBuilderInput {
|
||||
clip: {
|
||||
id?: string | null;
|
||||
slug: string;
|
||||
revisionNumber: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
visibility?: ClipManifestVisibility | null;
|
||||
creator?: ClipManifestCreator | null;
|
||||
previousRevisionId?: string | null;
|
||||
};
|
||||
artifact: {
|
||||
manifest: CompanyPortabilityManifest;
|
||||
files?: Record<string, CompanyPortabilityFileEntry>;
|
||||
};
|
||||
source?: {
|
||||
exportedAt?: string | null;
|
||||
paperclipCompatibility?: string | null;
|
||||
} | null;
|
||||
social?: {
|
||||
sourceUrl?: string | null;
|
||||
revisionUrl?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ClipScopedSnapshotBuilderInput extends ClipSnapshotBuilderInput {
|
||||
agentSlug?: string;
|
||||
teamRootAgentSlug?: string;
|
||||
skillKey?: string;
|
||||
skillSlug?: string;
|
||||
routineSlug?: string;
|
||||
}
|
||||
|
||||
export const clipTypeSchema = z.enum(["team", "agent", "skill", "routine", "bundle"]);
|
||||
export const clipManifestCreatorSchema = z.object({
|
||||
profileId: z.string().min(1).nullable(),
|
||||
handle: z.string().min(1).nullable(),
|
||||
displayName: z.string().min(1).nullable(),
|
||||
});
|
||||
|
||||
export const clipRedactionReportSchema = z.object({
|
||||
schema: z.literal("paperclip.clip.redaction/v1"),
|
||||
generatedAt: z.string().datetime(),
|
||||
entries: z.array(z.object({
|
||||
path: z.string().min(1),
|
||||
outcome: z.enum(["allow", "redact", "summarize", "omit"]),
|
||||
reason: z.string().min(1),
|
||||
})),
|
||||
summary: z.object({
|
||||
allowed: z.number().int().nonnegative(),
|
||||
redacted: z.number().int().nonnegative(),
|
||||
summarized: z.number().int().nonnegative(),
|
||||
omitted: z.number().int().nonnegative(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const clipDependencyGraphSchema = z.object({
|
||||
adapters: z.array(z.object({
|
||||
type: z.string().min(1),
|
||||
required: z.boolean(),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
note: z.string().nullable(),
|
||||
})),
|
||||
plugins: z.array(z.object({
|
||||
key: z.string().min(1),
|
||||
requirement: z.enum(["required", "optional"]),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
note: z.string().nullable(),
|
||||
})),
|
||||
skills: z.array(z.object({
|
||||
key: z.string().min(1),
|
||||
slug: z.string().min(1),
|
||||
requirement: z.enum(["required", "optional"]),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
})),
|
||||
secrets: z.array(z.object({
|
||||
key: z.string().min(1),
|
||||
kind: z.literal("secret"),
|
||||
requirement: z.enum(["required", "optional"]),
|
||||
description: z.string().nullable(),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
})),
|
||||
permissions: z.array(z.object({
|
||||
capability: z.string().min(1),
|
||||
reason: z.string().nullable(),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
})),
|
||||
runtime: z.object({
|
||||
localShell: z.boolean(),
|
||||
browser: z.boolean(),
|
||||
filesystem: z.enum(["none", "declared", "broad"]),
|
||||
webhooks: z.boolean(),
|
||||
recurringRoutines: z.boolean(),
|
||||
}),
|
||||
workspaces: z.array(z.object({
|
||||
key: z.string().min(1),
|
||||
repoUrlRequired: z.boolean(),
|
||||
pinnedRefRecommended: z.boolean(),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
})),
|
||||
budgetHints: z.object({
|
||||
monthlyCents: z.number().int().nonnegative(),
|
||||
sourceRefs: z.array(z.string().min(1)),
|
||||
}),
|
||||
});
|
||||
|
||||
export const clipManifestSchema = z.object({
|
||||
schema: z.literal(CLIP_MANIFEST_SCHEMA),
|
||||
clip: z.object({
|
||||
id: z.string().min(1),
|
||||
slug: z.string().min(1),
|
||||
type: clipTypeSchema,
|
||||
revisionId: z.string().min(1),
|
||||
revisionNumber: z.number().int().positive(),
|
||||
manifestVersion: z.literal(CLIP_MANIFEST_VERSION),
|
||||
title: z.string().min(1),
|
||||
summary: z.string(),
|
||||
visibility: z.enum(["private_share", "unlisted", "public"]),
|
||||
creator: clipManifestCreatorSchema.nullable(),
|
||||
}),
|
||||
publication: z.object({
|
||||
source: z.object({
|
||||
kind: z.literal("paperclip_company_object"),
|
||||
objectType: clipTypeSchema,
|
||||
exportedAt: z.string().datetime(),
|
||||
}),
|
||||
compatibility: z.object({
|
||||
paperclip: z.string().nullable(),
|
||||
manifest: z.literal(CLIP_ARTIFACT_VERSION),
|
||||
}),
|
||||
}),
|
||||
artifact: z.object({
|
||||
format: z.literal(CLIP_ARTIFACT_FORMAT),
|
||||
version: z.literal(CLIP_ARTIFACT_VERSION),
|
||||
checksum: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
||||
entrypoint: z.enum(["COMPANY.md", "TEAM.md", "AGENTS.md", "TASK.md", "SKILL.md"]),
|
||||
paperclipExtension: z.literal(".paperclip.yaml"),
|
||||
payload: z.object({
|
||||
manifest: z.custom<CompanyPortabilityManifest>(),
|
||||
}),
|
||||
}),
|
||||
dependencies: clipDependencyGraphSchema,
|
||||
security: z.object({
|
||||
redactionReport: clipRedactionReportSchema,
|
||||
dangerousCapabilities: z.array(z.string().min(1)),
|
||||
routinePolicy: z.object({
|
||||
importedTriggersEnabledByDefault: z.literal(false),
|
||||
webhookSecretsRegenerated: z.literal(true),
|
||||
}),
|
||||
reviewState: z.enum(["unreviewed", "automated_checked", "security_reviewed", "blocked"]),
|
||||
}),
|
||||
verification: z.object({
|
||||
expectedFirstRun: z.record(z.string(), z.unknown()).nullable(),
|
||||
sampleOutputs: z.array(z.record(z.string(), z.unknown())),
|
||||
validationStatus: z.enum(["not_run", "passed", "failed", "stale"]),
|
||||
}),
|
||||
social: z.object({
|
||||
sourceUrl: z.string().url().nullable(),
|
||||
revisionUrl: z.string().url().nullable(),
|
||||
}),
|
||||
provenance: z.object({
|
||||
publishedByProfileId: z.string().min(1).nullable(),
|
||||
revisionPublishedAt: z.string().datetime(),
|
||||
previousRevisionId: z.string().min(1).nullable(),
|
||||
}),
|
||||
checksums: z.object({
|
||||
artifact: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
||||
redactionReport: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
||||
manifest: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
||||
}),
|
||||
});
|
||||
|
||||
export function buildBundleClipSnapshot(input: ClipSnapshotBuilderInput): ClipManifest {
|
||||
return buildClipSnapshot("bundle", input);
|
||||
}
|
||||
|
||||
export function buildAgentClipSnapshot(input: ClipScopedSnapshotBuilderInput): ClipManifest {
|
||||
const agentSlug = input.agentSlug ?? input.artifact.manifest.agents[0]?.slug;
|
||||
const manifest = agentSlug ? scopeManifestToAgents(input.artifact.manifest, [agentSlug]) : input.artifact.manifest;
|
||||
return buildClipSnapshot("agent", { ...input, artifact: { ...input.artifact, manifest } });
|
||||
}
|
||||
|
||||
export function buildTeamClipSnapshot(input: ClipScopedSnapshotBuilderInput): ClipManifest {
|
||||
const rootSlug = input.teamRootAgentSlug ?? input.agentSlug ?? input.artifact.manifest.agents.find((agent) => !agent.reportsToSlug)?.slug;
|
||||
const agentSlugs = rootSlug ? collectAgentSubtreeSlugs(input.artifact.manifest.agents, rootSlug) : [];
|
||||
const manifest = agentSlugs.length > 0 ? scopeManifestToAgents(input.artifact.manifest, agentSlugs) : input.artifact.manifest;
|
||||
return buildClipSnapshot("team", { ...input, artifact: { ...input.artifact, manifest } });
|
||||
}
|
||||
|
||||
export function buildSkillClipSnapshot(input: ClipScopedSnapshotBuilderInput): ClipManifest {
|
||||
const manifest = cloneManifest(input.artifact.manifest);
|
||||
const skill = findSkill(manifest.skills, input.skillKey, input.skillSlug) ?? manifest.skills[0] ?? null;
|
||||
manifest.agents = [];
|
||||
manifest.projects = [];
|
||||
manifest.issues = [];
|
||||
manifest.skills = skill ? [skill] : [];
|
||||
manifest.includes = { company: false, agents: false, projects: false, issues: false, skills: manifest.skills.length > 0 };
|
||||
return buildClipSnapshot("skill", { ...input, artifact: { ...input.artifact, manifest } });
|
||||
}
|
||||
|
||||
export function buildRoutineClipSnapshot(input: ClipScopedSnapshotBuilderInput): ClipManifest {
|
||||
const manifest = cloneManifest(input.artifact.manifest);
|
||||
const routine = manifest.issues.find((issue) => (
|
||||
issue.slug === input.routineSlug
|
||||
|| issue.identifier === input.routineSlug
|
||||
|| (!input.routineSlug && (issue.recurring || issue.routine))
|
||||
));
|
||||
const assigneeSlug = routine?.assigneeAgentSlug ?? null;
|
||||
manifest.issues = routine ? [routine] : [];
|
||||
manifest.agents = assigneeSlug ? manifest.agents.filter((agent) => agent.slug === assigneeSlug) : [];
|
||||
manifest.projects = [];
|
||||
manifest.skills = filterSkillsForAgents(manifest.skills, manifest.agents);
|
||||
manifest.includes = {
|
||||
company: false,
|
||||
agents: manifest.agents.length > 0,
|
||||
projects: false,
|
||||
issues: manifest.issues.length > 0,
|
||||
skills: manifest.skills.length > 0,
|
||||
};
|
||||
return buildClipSnapshot("routine", { ...input, artifact: { ...input.artifact, manifest } });
|
||||
}
|
||||
|
||||
export function buildClipSnapshot(type: ClipManifestType, input: ClipSnapshotBuilderInput): ClipManifest {
|
||||
const exportedAt = input.source?.exportedAt ?? new Date(0).toISOString();
|
||||
const sanitized = sanitizeManifest(input.artifact.manifest, exportedAt);
|
||||
const dependencyGraph = buildDependencyGraph(input.artifact.manifest);
|
||||
const dangerousCapabilities = buildDangerousCapabilities(dependencyGraph);
|
||||
const artifactChecksum = sha256Stable({
|
||||
manifest: sanitized.manifest,
|
||||
files: normalizeFilesForChecksum(input.artifact.files ?? {}),
|
||||
});
|
||||
const redactionReportChecksum = sha256Stable(sanitized.redactionReport);
|
||||
const revisionSeed = sha256Stable({
|
||||
schema: CLIP_MANIFEST_SCHEMA,
|
||||
slug: input.clip.slug,
|
||||
type,
|
||||
revisionNumber: input.clip.revisionNumber,
|
||||
artifactChecksum,
|
||||
redactionReportChecksum,
|
||||
});
|
||||
const revisionId = `cliprev_${revisionSeed.slice("sha256:".length, "sha256:".length + 16)}`;
|
||||
|
||||
const withoutManifestChecksum: ClipManifest = {
|
||||
schema: CLIP_MANIFEST_SCHEMA,
|
||||
clip: {
|
||||
id: input.clip.id ?? `clip_${normalizeSlug(input.clip.slug)}`,
|
||||
slug: normalizeSlug(input.clip.slug),
|
||||
type,
|
||||
revisionId,
|
||||
revisionNumber: input.clip.revisionNumber,
|
||||
manifestVersion: CLIP_MANIFEST_VERSION,
|
||||
title: input.clip.title,
|
||||
summary: input.clip.summary,
|
||||
visibility: input.clip.visibility ?? "unlisted",
|
||||
creator: input.clip.creator ?? null,
|
||||
},
|
||||
publication: {
|
||||
source: {
|
||||
kind: "paperclip_company_object",
|
||||
objectType: type,
|
||||
exportedAt,
|
||||
},
|
||||
compatibility: {
|
||||
paperclip: input.source?.paperclipCompatibility ?? null,
|
||||
manifest: CLIP_ARTIFACT_VERSION,
|
||||
},
|
||||
},
|
||||
artifact: {
|
||||
format: CLIP_ARTIFACT_FORMAT,
|
||||
version: CLIP_ARTIFACT_VERSION,
|
||||
checksum: artifactChecksum,
|
||||
entrypoint: entrypointForClipType(type),
|
||||
paperclipExtension: ".paperclip.yaml",
|
||||
payload: {
|
||||
manifest: sanitized.manifest,
|
||||
},
|
||||
},
|
||||
dependencies: dependencyGraph,
|
||||
security: {
|
||||
redactionReport: sanitized.redactionReport,
|
||||
dangerousCapabilities,
|
||||
routinePolicy: {
|
||||
importedTriggersEnabledByDefault: false,
|
||||
webhookSecretsRegenerated: true,
|
||||
},
|
||||
reviewState: "unreviewed",
|
||||
},
|
||||
verification: {
|
||||
expectedFirstRun: null,
|
||||
sampleOutputs: [],
|
||||
validationStatus: "not_run",
|
||||
},
|
||||
social: {
|
||||
sourceUrl: input.social?.sourceUrl ?? null,
|
||||
revisionUrl: input.social?.revisionUrl ?? null,
|
||||
},
|
||||
provenance: {
|
||||
publishedByProfileId: input.clip.creator?.profileId ?? null,
|
||||
revisionPublishedAt: exportedAt,
|
||||
previousRevisionId: input.clip.previousRevisionId ?? null,
|
||||
},
|
||||
checksums: {
|
||||
artifact: artifactChecksum,
|
||||
redactionReport: redactionReportChecksum,
|
||||
manifest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
},
|
||||
};
|
||||
|
||||
const manifestChecksum = sha256Stable(withoutManifestChecksum);
|
||||
const result = {
|
||||
...withoutManifestChecksum,
|
||||
checksums: {
|
||||
...withoutManifestChecksum.checksums,
|
||||
manifest: manifestChecksum,
|
||||
},
|
||||
};
|
||||
clipManifestSchema.parse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitizeManifest(source: CompanyPortabilityManifest, generatedAt: string) {
|
||||
const entries: ClipRedactionReportEntry[] = [];
|
||||
const manifest = cloneManifest(source);
|
||||
manifest.generatedAt = generatedAt;
|
||||
manifest.source = null;
|
||||
manifest.envInputs = manifest.envInputs.map((envInput, index) => {
|
||||
if (envInput.defaultValue !== null) {
|
||||
entries.push({
|
||||
path: `envInputs[${index}].defaultValue`,
|
||||
outcome: "omit",
|
||||
reason: "environment default values are not published in clip manifests",
|
||||
});
|
||||
}
|
||||
return {
|
||||
...envInput,
|
||||
defaultValue: null,
|
||||
description: sanitizeText(envInput.description, `envInputs[${index}].description`, entries),
|
||||
};
|
||||
});
|
||||
manifest.company = manifest.company
|
||||
? {
|
||||
...manifest.company,
|
||||
name: sanitizeText(manifest.company.name, "company.name", entries) ?? manifest.company.name,
|
||||
description: sanitizeText(manifest.company.description, "company.description", entries),
|
||||
}
|
||||
: null;
|
||||
manifest.agents = manifest.agents.map((agent, index) => ({
|
||||
...agent,
|
||||
capabilities: sanitizeText(agent.capabilities, `agents[${index}].capabilities`, entries),
|
||||
adapterConfig: sanitizeValue(agent.adapterConfig, `agents[${index}].adapterConfig`, entries) as Record<string, unknown>,
|
||||
runtimeConfig: sanitizeValue(agent.runtimeConfig, `agents[${index}].runtimeConfig`, entries) as Record<string, unknown>,
|
||||
permissions: sanitizeValue(agent.permissions, `agents[${index}].permissions`, entries) as Record<string, unknown>,
|
||||
metadata: sanitizeNullableRecord(agent.metadata, `agents[${index}].metadata`, entries),
|
||||
}));
|
||||
manifest.projects = manifest.projects.map((project, index) => ({
|
||||
...project,
|
||||
description: sanitizeText(project.description, `projects[${index}].description`, entries),
|
||||
env: sanitizeValue(project.env, `projects[${index}].env`, entries) as typeof project.env,
|
||||
executionWorkspacePolicy: sanitizeNullableRecord(project.executionWorkspacePolicy, `projects[${index}].executionWorkspacePolicy`, entries),
|
||||
workspaces: project.workspaces.map((workspace, workspaceIndex) => ({
|
||||
...workspace,
|
||||
repoUrl: shouldOmitString(workspace.repoUrl)
|
||||
? recordOmission(`projects[${index}].workspaces[${workspaceIndex}].repoUrl`, "private workspace URLs are not published", entries)
|
||||
: workspace.repoUrl,
|
||||
setupCommand: sanitizeText(workspace.setupCommand, `projects[${index}].workspaces[${workspaceIndex}].setupCommand`, entries),
|
||||
cleanupCommand: sanitizeText(workspace.cleanupCommand, `projects[${index}].workspaces[${workspaceIndex}].cleanupCommand`, entries),
|
||||
metadata: sanitizeNullableRecord(workspace.metadata, `projects[${index}].workspaces[${workspaceIndex}].metadata`, entries),
|
||||
})),
|
||||
metadata: sanitizeNullableRecord(project.metadata, `projects[${index}].metadata`, entries),
|
||||
}));
|
||||
manifest.issues = manifest.issues.map((issue, index) => ({
|
||||
...issue,
|
||||
description: sanitizeText(issue.description, `issues[${index}].description`, entries),
|
||||
routine: issue.routine
|
||||
? {
|
||||
...issue.routine,
|
||||
variables: issue.routine.variables?.map((variable, variableIndex) => (
|
||||
sanitizeValue(variable, `issues[${index}].routine.variables[${variableIndex}]`, entries)
|
||||
)) as typeof issue.routine.variables,
|
||||
triggers: issue.routine.triggers.map((trigger, triggerIndex) => ({
|
||||
...trigger,
|
||||
enabled: false,
|
||||
signingMode: trigger.kind === "webhook" ? null : trigger.signingMode,
|
||||
replayWindowSec: trigger.kind === "webhook" ? null : trigger.replayWindowSec,
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
executionWorkspaceSettings: sanitizeNullableRecord(issue.executionWorkspaceSettings, `issues[${index}].executionWorkspaceSettings`, entries),
|
||||
assigneeAdapterOverrides: sanitizeNullableRecord(issue.assigneeAdapterOverrides, `issues[${index}].assigneeAdapterOverrides`, entries),
|
||||
comments: [],
|
||||
metadata: sanitizeNullableRecord(issue.metadata, `issues[${index}].metadata`, entries),
|
||||
}));
|
||||
for (const [index, issue] of source.issues.entries()) {
|
||||
if (issue.comments.length > 0) {
|
||||
entries.push({
|
||||
path: `issues[${index}].comments`,
|
||||
outcome: "omit",
|
||||
reason: "raw issue comments are private source-company state",
|
||||
});
|
||||
}
|
||||
issue.routine?.triggers.forEach((trigger, triggerIndex) => {
|
||||
if (trigger.enabled) {
|
||||
entries.push({
|
||||
path: `issues[${index}].routine.triggers[${triggerIndex}].enabled`,
|
||||
outcome: "redact",
|
||||
reason: "imported routine triggers are disabled by default",
|
||||
});
|
||||
}
|
||||
if (trigger.kind === "webhook" && (trigger.signingMode || trigger.replayWindowSec !== null)) {
|
||||
entries.push({
|
||||
path: `issues[${index}].routine.triggers[${triggerIndex}]`,
|
||||
outcome: "omit",
|
||||
reason: "webhook signing details are regenerated on import",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const redactionReport = buildRedactionReport(entries, generatedAt);
|
||||
return { manifest: sortManifest(manifest), redactionReport };
|
||||
}
|
||||
|
||||
function buildDependencyGraph(manifest: CompanyPortabilityManifest): ClipDependencyGraph {
|
||||
const adapters = new Map<string, { type: string; required: boolean; sourceRefs: Set<string>; note: string | null }>();
|
||||
const plugins = new Map<string, { key: string; requirement: "required" | "optional"; sourceRefs: Set<string>; note: string | null }>();
|
||||
const skills = new Map<string, { key: string; slug: string; requirement: "required" | "optional"; sourceRefs: Set<string> }>();
|
||||
const secrets = new Map<string, { key: string; kind: "secret"; requirement: "required" | "optional"; description: string | null; sourceRefs: Set<string> }>();
|
||||
const permissions = new Map<string, { capability: string; reason: string | null; sourceRefs: Set<string> }>();
|
||||
const workspaces = new Map<string, { key: string; repoUrlRequired: boolean; pinnedRefRecommended: boolean; sourceRefs: Set<string> }>();
|
||||
const runtime = {
|
||||
localShell: false,
|
||||
browser: false,
|
||||
filesystem: "none" as ClipRuntimeFilesystem,
|
||||
webhooks: false,
|
||||
recurringRoutines: false,
|
||||
};
|
||||
let monthlyCents = 0;
|
||||
const budgetRefs = new Set<string>();
|
||||
|
||||
for (const [index, agent] of manifest.agents.entries()) {
|
||||
const sourceRef = `agents.${agent.slug || index}`;
|
||||
addMapEntry(adapters, agent.adapterType, {
|
||||
type: agent.adapterType,
|
||||
required: true,
|
||||
sourceRefs: new Set([sourceRef]),
|
||||
note: null,
|
||||
});
|
||||
monthlyCents += agent.budgetMonthlyCents;
|
||||
if (agent.budgetMonthlyCents > 0) budgetRefs.add(sourceRef);
|
||||
for (const skillRef of agent.skills) {
|
||||
addSkill(skills, skillRef, skillRef, sourceRef);
|
||||
}
|
||||
for (const key of collectSecretKeys(agent.adapterConfig)) {
|
||||
addSecret(secrets, key, "optional", null, sourceRef);
|
||||
}
|
||||
for (const permission of collectPermissionCapabilities(agent.permissions)) {
|
||||
addPermission(permissions, permission, null, sourceRef);
|
||||
}
|
||||
if (agent.adapterType === "process" || hasAnyKey(agent.adapterConfig, ["command", "shell", "cwd"])) {
|
||||
runtime.localShell = true;
|
||||
}
|
||||
if (hasAnyKey(agent.permissions, ["browser", "browserAutomation"])) {
|
||||
runtime.browser = true;
|
||||
}
|
||||
if (hasAnyKey(agent.adapterConfig, ["cwd", "workspace", "filesystem", "path"])) {
|
||||
runtime.filesystem = "declared";
|
||||
}
|
||||
}
|
||||
|
||||
for (const skill of manifest.skills) {
|
||||
addSkill(skills, skill.key, skill.slug, `skills.${skill.slug}`);
|
||||
}
|
||||
|
||||
for (const [index, envInput] of manifest.envInputs.entries()) {
|
||||
if (envInput.kind === "secret") {
|
||||
addSecret(secrets, envInput.key, envInput.requirement, envInput.description, `envInputs.${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const project of manifest.projects) {
|
||||
for (const key of collectSecretKeys(project.env)) {
|
||||
addSecret(secrets, key, "optional", null, `projects.${project.slug}.env`);
|
||||
}
|
||||
for (const workspace of project.workspaces) {
|
||||
addMapEntry(workspaces, workspace.key, {
|
||||
key: workspace.key,
|
||||
repoUrlRequired: Boolean(workspace.repoUrl),
|
||||
pinnedRefRecommended: Boolean(workspace.repoUrl && !workspace.repoRef),
|
||||
sourceRefs: new Set([`projects.${project.slug}.workspaces.${workspace.key}`]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const issue of manifest.issues) {
|
||||
if (issue.recurring || issue.routine) {
|
||||
runtime.recurringRoutines = true;
|
||||
}
|
||||
if (issue.routine?.triggers.some((trigger) => trigger.kind === "webhook")) {
|
||||
runtime.webhooks = true;
|
||||
addPermission(permissions, "webhook.receive", "Routine has a webhook trigger placeholder.", `issues.${issue.slug}.routine`);
|
||||
}
|
||||
for (const trigger of issue.routine?.triggers ?? []) {
|
||||
if (trigger.kind === "schedule") {
|
||||
addPermission(permissions, "routine.schedule", "Routine has a recurring schedule.", `issues.${issue.slug}.routine`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
adapters: finalizeMap(adapters),
|
||||
plugins: finalizeMap(plugins),
|
||||
skills: finalizeMap(skills),
|
||||
secrets: finalizeMap(secrets),
|
||||
permissions: finalizeMap(permissions),
|
||||
runtime,
|
||||
workspaces: finalizeMap(workspaces),
|
||||
budgetHints: {
|
||||
monthlyCents,
|
||||
sourceRefs: Array.from(budgetRefs).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildDangerousCapabilities(graph: ClipDependencyGraph) {
|
||||
const capabilities = new Set<string>();
|
||||
if (graph.runtime.browser) capabilities.add("browser");
|
||||
if (graph.runtime.localShell) capabilities.add("shell");
|
||||
if (graph.runtime.filesystem !== "none") capabilities.add("filesystem");
|
||||
if (graph.runtime.webhooks) capabilities.add("webhook");
|
||||
if (graph.runtime.recurringRoutines) capabilities.add("recurring_routine");
|
||||
for (const permission of graph.permissions) {
|
||||
const root = permission.capability.split(".")[0];
|
||||
if (["calendar", "drive", "email", "filesystem", "github", "slack"].includes(root)) {
|
||||
capabilities.add(root);
|
||||
}
|
||||
}
|
||||
return Array.from(capabilities).sort();
|
||||
}
|
||||
|
||||
function sanitizeValue(value: unknown, path: string, entries: ClipRedactionReportEntry[]): unknown {
|
||||
if (value === null || value === undefined) return value;
|
||||
if (typeof value === "string") return sanitizeText(value, path, entries);
|
||||
if (typeof value !== "object") return value;
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry, index) => sanitizeValue(entry, `${path}[${index}]`, entries));
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
if (record.type === "secret_ref") {
|
||||
entries.push({ path, outcome: "omit", reason: "source secret references are replaced with setup inputs" });
|
||||
return { type: "secret_input" };
|
||||
}
|
||||
const next: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(record).sort()) {
|
||||
const childPath = `${path}.${key}`;
|
||||
const child = record[key];
|
||||
if (SECRET_KEY_PATTERN.test(key) && child !== null && child !== undefined) {
|
||||
entries.push({ path: childPath, outcome: "omit", reason: "secret-like fields are not published" });
|
||||
continue;
|
||||
}
|
||||
if (typeof child === "string" && shouldOmitString(child)) {
|
||||
entries.push({ path: childPath, outcome: "omit", reason: "private local paths, URLs, or credentials are not published" });
|
||||
continue;
|
||||
}
|
||||
next[key] = sanitizeValue(child, childPath, entries);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function sanitizeText(value: string | null, path: string, entries: ClipRedactionReportEntry[]) {
|
||||
if (value === null) return null;
|
||||
if (shouldOmitString(value)) {
|
||||
entries.push({ path, outcome: "redact", reason: "text contained private paths, URLs, or secret-looking values" });
|
||||
return "[redacted]";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sanitizeNullableRecord(value: Record<string, unknown> | null, path: string, entries: ClipRedactionReportEntry[]) {
|
||||
return value === null ? null : sanitizeValue(value, path, entries) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function shouldOmitString(value: string | null) {
|
||||
return Boolean(value && (SECRET_VALUE_PATTERN.test(value) || LOCAL_PATH_PATTERN.test(value) || PRIVATE_URL_PATTERN.test(value)));
|
||||
}
|
||||
|
||||
function recordOmission(path: string, reason: string, entries: ClipRedactionReportEntry[]) {
|
||||
entries.push({ path, outcome: "omit", reason });
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildRedactionReport(entries: ClipRedactionReportEntry[], generatedAt: string): ClipRedactionReport {
|
||||
const sortedEntries = [...entries].sort((left, right) => left.path.localeCompare(right.path) || left.reason.localeCompare(right.reason));
|
||||
return {
|
||||
schema: "paperclip.clip.redaction/v1",
|
||||
generatedAt,
|
||||
entries: sortedEntries,
|
||||
summary: {
|
||||
allowed: 0,
|
||||
redacted: sortedEntries.filter((entry) => entry.outcome === "redact").length,
|
||||
summarized: sortedEntries.filter((entry) => entry.outcome === "summarize").length,
|
||||
omitted: sortedEntries.filter((entry) => entry.outcome === "omit").length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function scopeManifestToAgents(source: CompanyPortabilityManifest, agentSlugs: string[]) {
|
||||
const selected = new Set(agentSlugs);
|
||||
const manifest = cloneManifest(source);
|
||||
manifest.agents = manifest.agents.filter((agent) => selected.has(agent.slug));
|
||||
manifest.skills = filterSkillsForAgents(manifest.skills, manifest.agents);
|
||||
manifest.projects = [];
|
||||
manifest.issues = manifest.issues.filter((issue) => issue.assigneeAgentSlug ? selected.has(issue.assigneeAgentSlug) : false);
|
||||
manifest.includes = {
|
||||
company: false,
|
||||
agents: manifest.agents.length > 0,
|
||||
projects: false,
|
||||
issues: manifest.issues.length > 0,
|
||||
skills: manifest.skills.length > 0,
|
||||
};
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function collectAgentSubtreeSlugs(agents: CompanyPortabilityAgentManifestEntry[], rootSlug: string) {
|
||||
const result = new Set<string>();
|
||||
const visit = (slug: string) => {
|
||||
if (result.has(slug)) return;
|
||||
result.add(slug);
|
||||
for (const child of agents.filter((agent) => agent.reportsToSlug === slug)) {
|
||||
visit(child.slug);
|
||||
}
|
||||
};
|
||||
visit(rootSlug);
|
||||
return Array.from(result).sort();
|
||||
}
|
||||
|
||||
function filterSkillsForAgents(skills: CompanyPortabilitySkillManifestEntry[], agents: CompanyPortabilityAgentManifestEntry[]) {
|
||||
const refs = new Set(agents.flatMap((agent) => agent.skills));
|
||||
return skills.filter((skill) => refs.has(skill.key) || refs.has(skill.slug));
|
||||
}
|
||||
|
||||
function findSkill(skills: CompanyPortabilitySkillManifestEntry[], key?: string, slug?: string) {
|
||||
return skills.find((skill) => (key && skill.key === key) || (slug && skill.slug === slug)) ?? null;
|
||||
}
|
||||
|
||||
function sortManifest(manifest: CompanyPortabilityManifest): CompanyPortabilityManifest {
|
||||
return {
|
||||
...manifest,
|
||||
agents: [...manifest.agents].sort(bySlug),
|
||||
skills: [...manifest.skills].sort((left, right) => left.key.localeCompare(right.key) || left.slug.localeCompare(right.slug)),
|
||||
projects: [...manifest.projects].sort(bySlug),
|
||||
issues: [...manifest.issues].sort(bySlug),
|
||||
envInputs: [...manifest.envInputs].sort((left, right) => left.key.localeCompare(right.key)),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFilesForChecksum(files: Record<string, CompanyPortabilityFileEntry>) {
|
||||
return Object.fromEntries(Object.entries(files).sort(([left], [right]) => left.localeCompare(right)));
|
||||
}
|
||||
|
||||
function entrypointForClipType(type: ClipManifestType) {
|
||||
if (type === "agent") return "AGENTS.md";
|
||||
if (type === "skill") return "SKILL.md";
|
||||
if (type === "routine") return "TASK.md";
|
||||
if (type === "team") return "TEAM.md";
|
||||
return "COMPANY.md";
|
||||
}
|
||||
|
||||
function collectSecretKeys(value: unknown): string[] {
|
||||
const keys = new Set<string>();
|
||||
const visit = (entry: unknown, keyHint: string | null) => {
|
||||
if (entry === null || entry === undefined) return;
|
||||
if (typeof entry !== "object") {
|
||||
if (keyHint && SECRET_KEY_PATTERN.test(keyHint)) keys.add(keyHint);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(entry)) {
|
||||
entry.forEach((item) => visit(item, keyHint));
|
||||
return;
|
||||
}
|
||||
const record = entry as Record<string, unknown>;
|
||||
if (record.type === "secret_ref") {
|
||||
if (keyHint) keys.add(keyHint);
|
||||
return;
|
||||
}
|
||||
for (const [key, child] of Object.entries(record)) {
|
||||
if (SECRET_KEY_PATTERN.test(key)) keys.add(key);
|
||||
visit(child, key);
|
||||
}
|
||||
};
|
||||
visit(value, null);
|
||||
return Array.from(keys).sort();
|
||||
}
|
||||
|
||||
function collectPermissionCapabilities(value: unknown): string[] {
|
||||
const capabilities = new Set<string>();
|
||||
const visit = (entry: unknown, path: string[]) => {
|
||||
if (entry === null || entry === undefined) return;
|
||||
if (typeof entry === "boolean") {
|
||||
if (entry && path.length > 0) capabilities.add(path.join("."));
|
||||
return;
|
||||
}
|
||||
if (typeof entry === "string") {
|
||||
if (entry === "true" || entry === "allow" || entry === "write") capabilities.add(path.join("."));
|
||||
return;
|
||||
}
|
||||
if (typeof entry !== "object" || Array.isArray(entry)) return;
|
||||
for (const [key, child] of Object.entries(entry as Record<string, unknown>)) {
|
||||
visit(child, [...path, key]);
|
||||
}
|
||||
};
|
||||
visit(value, []);
|
||||
return Array.from(capabilities).sort();
|
||||
}
|
||||
|
||||
function hasAnyKey(value: unknown, keys: string[]) {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const wanted = new Set(keys);
|
||||
const visit = (entry: unknown): boolean => {
|
||||
if (!entry || typeof entry !== "object") return false;
|
||||
if (Array.isArray(entry)) return entry.some(visit);
|
||||
return Object.entries(entry as Record<string, unknown>).some(([key, child]) => wanted.has(key) || visit(child));
|
||||
};
|
||||
return visit(value);
|
||||
}
|
||||
|
||||
function addSecret(
|
||||
secrets: Map<string, { key: string; kind: "secret"; requirement: "required" | "optional"; description: string | null; sourceRefs: Set<string> }>,
|
||||
key: string,
|
||||
requirement: "required" | "optional",
|
||||
description: string | null,
|
||||
sourceRef: string,
|
||||
) {
|
||||
addMapEntry(secrets, key, { key, kind: "secret", requirement, description, sourceRefs: new Set([sourceRef]) });
|
||||
}
|
||||
|
||||
function addSkill(
|
||||
skills: Map<string, { key: string; slug: string; requirement: "required" | "optional"; sourceRefs: Set<string> }>,
|
||||
key: string,
|
||||
slug: string,
|
||||
sourceRef: string,
|
||||
) {
|
||||
addMapEntry(skills, key, { key, slug, requirement: "required", sourceRefs: new Set([sourceRef]) });
|
||||
}
|
||||
|
||||
function addPermission(
|
||||
permissions: Map<string, { capability: string; reason: string | null; sourceRefs: Set<string> }>,
|
||||
capability: string,
|
||||
reason: string | null,
|
||||
sourceRef: string,
|
||||
) {
|
||||
addMapEntry(permissions, capability, { capability, reason, sourceRefs: new Set([sourceRef]) });
|
||||
}
|
||||
|
||||
function addMapEntry<T extends { sourceRefs: Set<string> }>(map: Map<string, T>, key: string, value: T) {
|
||||
const existing = map.get(key);
|
||||
if (!existing) {
|
||||
map.set(key, value);
|
||||
return;
|
||||
}
|
||||
for (const sourceRef of value.sourceRefs) {
|
||||
existing.sourceRefs.add(sourceRef);
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeMap<T extends { sourceRefs: Set<string> }>(map: Map<string, T>) {
|
||||
return Array.from(map.values())
|
||||
.map((entry) => ({ ...entry, sourceRefs: Array.from(entry.sourceRefs).sort() }))
|
||||
.sort((left, right) => stableStringify(left).localeCompare(stableStringify(right)));
|
||||
}
|
||||
|
||||
function bySlug<T extends { slug: string }>(left: T, right: T) {
|
||||
return left.slug.localeCompare(right.slug);
|
||||
}
|
||||
|
||||
function normalizeSlug(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "clip";
|
||||
}
|
||||
|
||||
function cloneManifest(manifest: CompanyPortabilityManifest): CompanyPortabilityManifest {
|
||||
return JSON.parse(JSON.stringify(manifest)) as CompanyPortabilityManifest;
|
||||
}
|
||||
|
||||
function sha256Stable(value: unknown) {
|
||||
return `sha256:${createHash("sha256").update(stableStringify(value)).digest("hex")}`;
|
||||
}
|
||||
|
||||
function stableStringify(value: unknown): string {
|
||||
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
||||
return `{${Object.keys(value as Record<string, unknown>).sort().map((key) => `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key])}`).join(",")}}`;
|
||||
}
|
||||
@@ -652,6 +652,39 @@ export {
|
||||
} from "./validators/sidebar-preferences.js";
|
||||
|
||||
export { workspaceRuntimeControlTargetSchema } from "./validators/execution-workspace.js";
|
||||
|
||||
export {
|
||||
CLIP_ARTIFACT_FORMAT,
|
||||
CLIP_ARTIFACT_VERSION,
|
||||
CLIP_MANIFEST_SCHEMA,
|
||||
CLIP_MANIFEST_VERSION,
|
||||
buildAgentClipSnapshot,
|
||||
buildBundleClipSnapshot,
|
||||
buildClipSnapshot,
|
||||
buildRoutineClipSnapshot,
|
||||
buildSkillClipSnapshot,
|
||||
buildTeamClipSnapshot,
|
||||
clipDependencyGraphSchema,
|
||||
clipManifestCreatorSchema,
|
||||
clipManifestSchema,
|
||||
clipRedactionReportSchema,
|
||||
clipTypeSchema,
|
||||
type ClipDependencyGraph,
|
||||
type ClipManifest,
|
||||
type ClipManifestClip,
|
||||
type ClipManifestCreator,
|
||||
type ClipManifestType,
|
||||
type ClipManifestVisibility,
|
||||
type ClipRedactionOutcome,
|
||||
type ClipRedactionReport,
|
||||
type ClipRedactionReportEntry,
|
||||
type ClipReviewState,
|
||||
type ClipRuntimeFilesystem,
|
||||
type ClipScopedSnapshotBuilderInput,
|
||||
type ClipSnapshotBuilderInput,
|
||||
type ClipValidationStatus,
|
||||
} from "./clip-manifest.js";
|
||||
|
||||
export {
|
||||
findWorkspaceCommandDefinition,
|
||||
listWorkspaceCommandDefinitions,
|
||||
|
||||
Reference in new issue
Block a user