forked from farhoodlabs/paperclip
Merge upstream/master into dev (76 commits)
Resolved 5 conflicts: - .github/workflows/docker.yml, release.yml: kept fork stubs (CI handled by build-prod/build-dev) - server/src/routes/secrets.ts: kept fork's /usages route alongside upstream's /usage, /access-events - server/src/services/secrets.ts: kept fork's usages() function and in-use deletion guard, layered before upstream's soft-delete + provider cleanup in remove() - ui/src/api/secrets.ts: kept fork's usages() method alongside upstream's vault methods Typechecks pass on @paperclipai/shared, @paperclipai/server, @paperclipai/ui.
This commit is contained in:
@@ -11,6 +11,7 @@ export const API = {
|
||||
goals: `${API_PREFIX}/goals`,
|
||||
approvals: `${API_PREFIX}/approvals`,
|
||||
secrets: `${API_PREFIX}/secrets`,
|
||||
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
|
||||
costs: `${API_PREFIX}/costs`,
|
||||
activity: `${API_PREFIX}/activity`,
|
||||
dashboard: `${API_PREFIX}/dashboard`,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { paperclipConfigSchema } from "./config-schema.js";
|
||||
|
||||
describe("paperclip config schema", () => {
|
||||
it("defaults omitted runtime paths to legacy instance-root locations", () => {
|
||||
const parsed = paperclipConfigSchema.parse({
|
||||
$meta: {
|
||||
version: 1,
|
||||
updatedAt: "2026-05-10T00:00:00.000Z",
|
||||
source: "configure",
|
||||
},
|
||||
database: {
|
||||
mode: "embedded-postgres",
|
||||
},
|
||||
logging: {
|
||||
mode: "file",
|
||||
},
|
||||
server: {},
|
||||
});
|
||||
|
||||
expect(parsed.database.embeddedPostgresDataDir).toBe("~/.paperclip/instances/default/db");
|
||||
expect(parsed.database.backup.dir).toBe("~/.paperclip/instances/default/data/backups");
|
||||
expect(parsed.logging.logDir).toBe("~/.paperclip/instances/default/logs");
|
||||
expect(parsed.storage.localDisk.baseDir).toBe("~/.paperclip/instances/default/data/storage");
|
||||
expect(parsed.secrets.localEncrypted.keyFilePath).toBe("~/.paperclip/instances/default/secrets/master.key");
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,7 @@ export const AGENT_ADAPTER_TYPES = [
|
||||
"acpx_local",
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"cursor_cloud",
|
||||
"gemini_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
@@ -146,8 +147,29 @@ export const INBOX_MINE_ISSUE_STATUS_FILTER = INBOX_MINE_ISSUE_STATUSES.join(","
|
||||
|
||||
export const ISSUE_PRIORITIES = ["critical", "high", "medium", "low"] as const;
|
||||
export type IssuePriority = (typeof ISSUE_PRIORITIES)[number];
|
||||
export const ISSUE_WORK_MODES = ["standard", "planning"] as const;
|
||||
export type IssueWorkMode = (typeof ISSUE_WORK_MODES)[number];
|
||||
export const MAX_ISSUE_REQUEST_DEPTH = 1024;
|
||||
|
||||
export const ISSUE_COMMENT_AUTHOR_TYPES = ["user", "agent", "system"] as const;
|
||||
export type IssueCommentAuthorType = (typeof ISSUE_COMMENT_AUTHOR_TYPES)[number];
|
||||
|
||||
export const ISSUE_COMMENT_PRESENTATION_KINDS = ["message", "system_notice"] as const;
|
||||
export type IssueCommentPresentationKind = (typeof ISSUE_COMMENT_PRESENTATION_KINDS)[number];
|
||||
|
||||
export const ISSUE_COMMENT_PRESENTATION_TONES = ["neutral", "info", "success", "warning", "danger"] as const;
|
||||
export type IssueCommentPresentationTone = (typeof ISSUE_COMMENT_PRESENTATION_TONES)[number];
|
||||
|
||||
export const ISSUE_COMMENT_METADATA_ROW_TYPES = [
|
||||
"text",
|
||||
"code",
|
||||
"key_value",
|
||||
"issue_link",
|
||||
"agent_link",
|
||||
"run_link",
|
||||
] as const;
|
||||
export type IssueCommentMetadataRowType = (typeof ISSUE_COMMENT_METADATA_ROW_TYPES)[number];
|
||||
|
||||
export function clampIssueRequestDepth(value: number | null | undefined): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
||||
return Math.min(MAX_ISSUE_REQUEST_DEPTH, Math.max(0, Math.floor(value)));
|
||||
@@ -190,6 +212,16 @@ export const ISSUE_ORIGIN_KINDS = [
|
||||
export type BuiltInIssueOriginKind = (typeof ISSUE_ORIGIN_KINDS)[number];
|
||||
export type PluginIssueOriginKind = `plugin:${string}`;
|
||||
export type IssueOriginKind = BuiltInIssueOriginKind | PluginIssueOriginKind;
|
||||
export const ISSUE_SURFACE_VISIBILITIES = ["default", "plugin_operation"] as const;
|
||||
export type IssueSurfaceVisibility = (typeof ISSUE_SURFACE_VISIBILITIES)[number];
|
||||
|
||||
export function pluginOperationIssueOriginKind(pluginKey: string): PluginIssueOriginKind {
|
||||
return `plugin:${pluginKey}:operation`;
|
||||
}
|
||||
|
||||
export function isPluginOperationIssueOriginKind(originKind: string | null | undefined): boolean {
|
||||
return typeof originKind === "string" && /^plugin:[^:]+:operation(?::|$)/.test(originKind);
|
||||
}
|
||||
|
||||
export const ISSUE_RELATION_TYPES = ["blocks"] as const;
|
||||
export type IssueRelationType = (typeof ISSUE_RELATION_TYPES)[number];
|
||||
@@ -221,9 +253,39 @@ export type IssueExecutionPolicyMode = (typeof ISSUE_EXECUTION_POLICY_MODES)[num
|
||||
export const ISSUE_EXECUTION_STAGE_TYPES = ["review", "approval"] as const;
|
||||
export type IssueExecutionStageType = (typeof ISSUE_EXECUTION_STAGE_TYPES)[number];
|
||||
|
||||
export const ISSUE_MONITOR_SCHEDULED_BY = ["assignee", "board"] as const;
|
||||
export type IssueMonitorScheduledBy = (typeof ISSUE_MONITOR_SCHEDULED_BY)[number];
|
||||
|
||||
export const ISSUE_EXECUTION_MONITOR_KINDS = ["external_service"] as const;
|
||||
export type IssueExecutionMonitorKind = (typeof ISSUE_EXECUTION_MONITOR_KINDS)[number];
|
||||
|
||||
export const ISSUE_EXECUTION_MONITOR_RECOVERY_POLICIES = [
|
||||
"wake_owner",
|
||||
"create_recovery_issue",
|
||||
"escalate_to_board",
|
||||
] as const;
|
||||
export type IssueExecutionMonitorRecoveryPolicy =
|
||||
(typeof ISSUE_EXECUTION_MONITOR_RECOVERY_POLICIES)[number];
|
||||
|
||||
export const ISSUE_EXECUTION_STATE_STATUSES = ["idle", "pending", "changes_requested", "completed"] as const;
|
||||
export type IssueExecutionStateStatus = (typeof ISSUE_EXECUTION_STATE_STATUSES)[number];
|
||||
|
||||
export const ISSUE_EXECUTION_MONITOR_STATE_STATUSES = ["scheduled", "triggered", "cleared"] as const;
|
||||
export type IssueExecutionMonitorStateStatus = (typeof ISSUE_EXECUTION_MONITOR_STATE_STATUSES)[number];
|
||||
|
||||
export const ISSUE_EXECUTION_MONITOR_CLEAR_REASONS = [
|
||||
"manual",
|
||||
"triggered",
|
||||
"done",
|
||||
"cancelled",
|
||||
"invalid_status",
|
||||
"invalid_assignee",
|
||||
"dispatch_skipped",
|
||||
"timeout_exceeded",
|
||||
"max_attempts_exhausted",
|
||||
] as const;
|
||||
export type IssueExecutionMonitorClearReason = (typeof ISSUE_EXECUTION_MONITOR_CLEAR_REASONS)[number];
|
||||
|
||||
export const ISSUE_EXECUTION_DECISION_OUTCOMES = ["approved", "changes_requested"] as const;
|
||||
export type IssueExecutionDecisionOutcome = (typeof ISSUE_EXECUTION_DECISION_OUTCOMES)[number];
|
||||
|
||||
@@ -334,6 +396,54 @@ export const SECRET_PROVIDERS = [
|
||||
] as const;
|
||||
export type SecretProvider = (typeof SECRET_PROVIDERS)[number];
|
||||
|
||||
export const SECRET_PROVIDER_CONFIG_STATUSES = [
|
||||
"ready",
|
||||
"warning",
|
||||
"coming_soon",
|
||||
"disabled",
|
||||
] as const;
|
||||
export type SecretProviderConfigStatus = (typeof SECRET_PROVIDER_CONFIG_STATUSES)[number];
|
||||
|
||||
export const SECRET_PROVIDER_CONFIG_HEALTH_STATUSES = [
|
||||
"ready",
|
||||
"warning",
|
||||
"error",
|
||||
"coming_soon",
|
||||
"disabled",
|
||||
] as const;
|
||||
export type SecretProviderConfigHealthStatus =
|
||||
(typeof SECRET_PROVIDER_CONFIG_HEALTH_STATUSES)[number];
|
||||
|
||||
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
||||
export type SecretStatus = (typeof SECRET_STATUSES)[number];
|
||||
|
||||
export const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference"] as const;
|
||||
export type SecretManagedMode = (typeof SECRET_MANAGED_MODES)[number];
|
||||
|
||||
export const SECRET_VERSION_STATUSES = [
|
||||
"current",
|
||||
"previous",
|
||||
"disabled",
|
||||
"destroyed",
|
||||
"failed",
|
||||
] as const;
|
||||
export type SecretVersionStatus = (typeof SECRET_VERSION_STATUSES)[number];
|
||||
|
||||
export const SECRET_BINDING_TARGET_TYPES = [
|
||||
"agent",
|
||||
"project",
|
||||
"environment",
|
||||
"routine",
|
||||
"plugin",
|
||||
"issue",
|
||||
"run",
|
||||
"system",
|
||||
] as const;
|
||||
export type SecretBindingTargetType = (typeof SECRET_BINDING_TARGET_TYPES)[number];
|
||||
|
||||
export const SECRET_ACCESS_OUTCOMES = ["success", "failure"] as const;
|
||||
export type SecretAccessOutcome = (typeof SECRET_ACCESS_OUTCOMES)[number];
|
||||
|
||||
export const STORAGE_PROVIDERS = ["local_disk", "s3"] as const;
|
||||
export type StorageProvider = (typeof STORAGE_PROVIDERS)[number];
|
||||
|
||||
@@ -604,9 +714,13 @@ export const PLUGIN_CAPABILITIES = [
|
||||
"issue.comments.create",
|
||||
"issue.interactions.create",
|
||||
"issue.documents.write",
|
||||
"projects.managed",
|
||||
"routines.managed",
|
||||
"skills.managed",
|
||||
"agents.pause",
|
||||
"agents.resume",
|
||||
"agents.invoke",
|
||||
"agents.managed",
|
||||
"agent.sessions.create",
|
||||
"agent.sessions.list",
|
||||
"agent.sessions.send",
|
||||
@@ -628,6 +742,7 @@ export const PLUGIN_CAPABILITIES = [
|
||||
"http.outbound",
|
||||
"secrets.read-ref",
|
||||
"environment.drivers.register",
|
||||
"local.folders",
|
||||
// Agent Tools
|
||||
"agent.tools.register",
|
||||
// UI
|
||||
@@ -698,6 +813,7 @@ export const PLUGIN_UI_SLOT_TYPES = [
|
||||
"taskDetailView",
|
||||
"dashboardWidget",
|
||||
"sidebar",
|
||||
"routeSidebar",
|
||||
"sidebarPanel",
|
||||
"projectSidebarItem",
|
||||
"globalToolbarButton",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveDefaultBackupDir,
|
||||
resolveDefaultEmbeddedPostgresDir,
|
||||
resolveDefaultLogsDir,
|
||||
resolveDefaultSecretsKeyFilePath,
|
||||
resolveDefaultStorageDir,
|
||||
resolvePaperclipConfigPathForInstance,
|
||||
resolvePaperclipInstanceRoot,
|
||||
} from "./home-paths.js";
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
});
|
||||
|
||||
describe("home path resolution", () => {
|
||||
it("resolves config and runtime data directly under the instance root", () => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-home-paths-"));
|
||||
process.env.PAPERCLIP_HOME = home;
|
||||
delete process.env.PAPERCLIP_INSTANCE_ID;
|
||||
|
||||
const instanceRoot = path.join(home, "instances", "default");
|
||||
expect(resolvePaperclipInstanceRoot()).toBe(instanceRoot);
|
||||
expect(resolvePaperclipConfigPathForInstance()).toBe(path.join(instanceRoot, "config.json"));
|
||||
expect(resolveDefaultEmbeddedPostgresDir()).toBe(path.join(instanceRoot, "db"));
|
||||
expect(resolveDefaultBackupDir()).toBe(path.join(instanceRoot, "data", "backups"));
|
||||
expect(resolveDefaultLogsDir()).toBe(path.join(instanceRoot, "logs"));
|
||||
expect(resolveDefaultStorageDir()).toBe(path.join(instanceRoot, "data", "storage"));
|
||||
expect(resolveDefaultSecretsKeyFilePath()).toBe(path.join(instanceRoot, "secrets", "master.key"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export const DEFAULT_PAPERCLIP_INSTANCE_ID = "default";
|
||||
export const PAPERCLIP_CONFIG_BASENAME = "config.json";
|
||||
export const PAPERCLIP_ENV_FILENAME = ".env";
|
||||
|
||||
const PATH_SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
export function expandHomePrefix(value: string): string {
|
||||
if (value === "~") return os.homedir();
|
||||
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
|
||||
return value;
|
||||
}
|
||||
|
||||
export function resolvePaperclipHomeDir(homeOverride?: string): string {
|
||||
const raw = homeOverride?.trim() || process.env.PAPERCLIP_HOME?.trim();
|
||||
if (raw) return path.resolve(expandHomePrefix(raw));
|
||||
return path.resolve(os.homedir(), ".paperclip");
|
||||
}
|
||||
|
||||
export function resolvePaperclipInstanceId(instanceIdOverride?: string): string {
|
||||
const raw = instanceIdOverride?.trim() || process.env.PAPERCLIP_INSTANCE_ID?.trim() || DEFAULT_PAPERCLIP_INSTANCE_ID;
|
||||
if (!PATH_SEGMENT_RE.test(raw)) {
|
||||
throw new Error(`Invalid PAPERCLIP_INSTANCE_ID '${raw}'.`);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function resolvePaperclipInstanceRoot(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipHomeDir(input.homeDir), "instances", resolvePaperclipInstanceId(input.instanceId));
|
||||
}
|
||||
|
||||
export function resolvePaperclipInstanceConfigPath(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipInstanceRoot(input), PAPERCLIP_CONFIG_BASENAME);
|
||||
}
|
||||
|
||||
export function resolvePaperclipConfigPathForInstance(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return resolvePaperclipInstanceConfigPath(input);
|
||||
}
|
||||
|
||||
export function resolvePaperclipEnvPathForConfig(configPath: string): string {
|
||||
return path.resolve(path.dirname(configPath), PAPERCLIP_ENV_FILENAME);
|
||||
}
|
||||
|
||||
export function resolveDefaultEmbeddedPostgresDir(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipInstanceRoot(input), "db");
|
||||
}
|
||||
|
||||
export function resolveDefaultLogsDir(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipInstanceRoot(input), "logs");
|
||||
}
|
||||
|
||||
export function resolveDefaultSecretsKeyFilePath(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipInstanceRoot(input), "secrets", "master.key");
|
||||
}
|
||||
|
||||
export function resolveDefaultStorageDir(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipInstanceRoot(input), "data", "storage");
|
||||
}
|
||||
|
||||
export function resolveDefaultBackupDir(input: {
|
||||
homeDir?: string;
|
||||
instanceId?: string;
|
||||
} = {}): string {
|
||||
return path.resolve(resolvePaperclipInstanceRoot(input), "data", "backups");
|
||||
}
|
||||
|
||||
export function resolveHomeAwarePath(value: string): string {
|
||||
return path.resolve(expandHomePrefix(value));
|
||||
}
|
||||
@@ -19,12 +19,20 @@ export {
|
||||
INBOX_MINE_ISSUE_STATUSES,
|
||||
INBOX_MINE_ISSUE_STATUS_FILTER,
|
||||
ISSUE_PRIORITIES,
|
||||
ISSUE_WORK_MODES,
|
||||
MAX_ISSUE_REQUEST_DEPTH,
|
||||
ISSUE_COMMENT_AUTHOR_TYPES,
|
||||
ISSUE_COMMENT_METADATA_ROW_TYPES,
|
||||
ISSUE_COMMENT_PRESENTATION_KINDS,
|
||||
ISSUE_COMMENT_PRESENTATION_TONES,
|
||||
clampIssueRequestDepth,
|
||||
ISSUE_THREAD_INTERACTION_KINDS,
|
||||
ISSUE_THREAD_INTERACTION_STATUSES,
|
||||
ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES,
|
||||
ISSUE_ORIGIN_KINDS,
|
||||
ISSUE_SURFACE_VISIBILITIES,
|
||||
pluginOperationIssueOriginKind,
|
||||
isPluginOperationIssueOriginKind,
|
||||
ISSUE_RELATION_TYPES,
|
||||
ISSUE_TREE_CONTROL_MODES,
|
||||
ISSUE_TREE_HOLD_RELEASE_POLICY_STRATEGIES,
|
||||
@@ -35,7 +43,12 @@ export {
|
||||
ISSUE_REFERENCE_SOURCE_KINDS,
|
||||
ISSUE_EXECUTION_POLICY_MODES,
|
||||
ISSUE_EXECUTION_STAGE_TYPES,
|
||||
ISSUE_MONITOR_SCHEDULED_BY,
|
||||
ISSUE_EXECUTION_MONITOR_KINDS,
|
||||
ISSUE_EXECUTION_MONITOR_RECOVERY_POLICIES,
|
||||
ISSUE_EXECUTION_STATE_STATUSES,
|
||||
ISSUE_EXECUTION_MONITOR_STATE_STATUSES,
|
||||
ISSUE_EXECUTION_MONITOR_CLEAR_REASONS,
|
||||
ISSUE_EXECUTION_DECISION_OUTCOMES,
|
||||
GOAL_LEVELS,
|
||||
GOAL_STATUSES,
|
||||
@@ -58,6 +71,8 @@ export {
|
||||
APPROVAL_TYPES,
|
||||
APPROVAL_STATUSES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
|
||||
STORAGE_PROVIDERS,
|
||||
BILLING_TYPES,
|
||||
FINANCE_EVENT_KINDS,
|
||||
@@ -122,12 +137,18 @@ export {
|
||||
type AgentIconName,
|
||||
type IssueStatus,
|
||||
type IssuePriority,
|
||||
type IssueWorkMode,
|
||||
type IssueCommentAuthorType,
|
||||
type IssueCommentMetadataRowType,
|
||||
type IssueCommentPresentationKind,
|
||||
type IssueCommentPresentationTone,
|
||||
type IssueThreadInteractionKind,
|
||||
type IssueThreadInteractionStatus,
|
||||
type IssueThreadInteractionContinuationPolicy,
|
||||
type BuiltInIssueOriginKind,
|
||||
type PluginIssueOriginKind,
|
||||
type IssueOriginKind,
|
||||
type IssueSurfaceVisibility,
|
||||
type IssueRelationType,
|
||||
type IssueTreeControlMode,
|
||||
type IssueTreeHoldReleasePolicyStrategy,
|
||||
@@ -136,7 +157,12 @@ export {
|
||||
type IssueReferenceSourceKind,
|
||||
type IssueExecutionPolicyMode,
|
||||
type IssueExecutionStageType,
|
||||
type IssueMonitorScheduledBy,
|
||||
type IssueExecutionMonitorKind,
|
||||
type IssueExecutionMonitorRecoveryPolicy,
|
||||
type IssueExecutionStateStatus,
|
||||
type IssueExecutionMonitorStateStatus,
|
||||
type IssueExecutionMonitorClearReason,
|
||||
type IssueExecutionDecisionOutcome,
|
||||
type GoalLevel,
|
||||
type GoalStatus,
|
||||
@@ -158,6 +184,8 @@ export {
|
||||
type ApprovalType,
|
||||
type ApprovalStatus,
|
||||
type SecretProvider,
|
||||
type SecretProviderConfigStatus,
|
||||
type SecretProviderConfigHealthStatus,
|
||||
type StorageProvider,
|
||||
type BillingType,
|
||||
type FinanceEventKind,
|
||||
@@ -293,7 +321,15 @@ export type {
|
||||
ProjectCodebase,
|
||||
ProjectCodebaseOrigin,
|
||||
ProjectGoalRef,
|
||||
ProjectManagedByPlugin,
|
||||
ProjectWorkspace,
|
||||
CompanySearchHighlight,
|
||||
CompanySearchIssueSummary,
|
||||
CompanySearchResponse,
|
||||
CompanySearchResult,
|
||||
CompanySearchResultType,
|
||||
CompanySearchScope,
|
||||
CompanySearchSnippet,
|
||||
ExecutionWorkspace,
|
||||
ExecutionWorkspaceSummary,
|
||||
ExecutionWorkspaceConfig,
|
||||
@@ -337,9 +373,17 @@ export type {
|
||||
IssueBlockerAttentionState,
|
||||
IssueProductivityReview,
|
||||
IssueProductivityReviewTrigger,
|
||||
SuccessfulRunHandoffState,
|
||||
SuccessfulRunHandoffStateKind,
|
||||
IssueScheduledRetry,
|
||||
IssueScheduledRetryStatus,
|
||||
IssueRetryNowOutcome,
|
||||
IssueRetryNowResponse,
|
||||
IssueReferenceSource,
|
||||
IssueRelatedWorkItem,
|
||||
IssueRelatedWorkSummary,
|
||||
IssueExecutionMonitorPolicy,
|
||||
IssueExecutionMonitorState,
|
||||
IssueRelation,
|
||||
IssueRelationIssueSummary,
|
||||
IssueExecutionPolicy,
|
||||
@@ -349,6 +393,16 @@ export type {
|
||||
IssueExecutionStagePrincipal,
|
||||
IssueExecutionDecision,
|
||||
IssueComment,
|
||||
IssueCommentMetadata,
|
||||
IssueCommentMetadataSection,
|
||||
IssueCommentMetadataRow,
|
||||
IssueCommentMetadataTextRow,
|
||||
IssueCommentMetadataCodeRow,
|
||||
IssueCommentMetadataKeyValueRow,
|
||||
IssueCommentMetadataIssueLinkRow,
|
||||
IssueCommentMetadataAgentLinkRow,
|
||||
IssueCommentMetadataRunLinkRow,
|
||||
IssueCommentPresentation,
|
||||
IssueThreadInteractionActorFields,
|
||||
SuggestedTaskDraft,
|
||||
SuggestTasksPayload,
|
||||
@@ -458,6 +512,7 @@ export type {
|
||||
CompanyPortabilityProjectWorkspaceManifestEntry,
|
||||
CompanyPortabilityIssueRoutineTriggerManifestEntry,
|
||||
CompanyPortabilityIssueRoutineManifestEntry,
|
||||
CompanyPortabilityIssueCommentManifestEntry,
|
||||
CompanyPortabilityIssueManifestEntry,
|
||||
CompanyPortabilityManifest,
|
||||
CompanyPortabilityExportResult,
|
||||
@@ -480,10 +535,38 @@ export type {
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
SecretProviderConfigHealthResponse,
|
||||
CompanySecretBinding,
|
||||
CompanySecretBindingTarget,
|
||||
CompanySecretUsageBinding,
|
||||
CompanySecretVersion,
|
||||
SecretAccessEvent,
|
||||
RemoteSecretImportCandidate,
|
||||
RemoteSecretImportCandidateStatus,
|
||||
RemoteSecretImportConflict,
|
||||
RemoteSecretImportPreviewResult,
|
||||
RemoteSecretImportResult,
|
||||
RemoteSecretImportRowResult,
|
||||
RemoteSecretImportRowStatus,
|
||||
SecretAccessOutcome,
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretProviderDescriptor,
|
||||
SecretStatus,
|
||||
SecretVersionSelector,
|
||||
SecretVersionStatus,
|
||||
Routine,
|
||||
RoutineManagedByPlugin,
|
||||
RoutineVariable,
|
||||
RoutineVariableDefaultValue,
|
||||
RoutineRevisionSnapshotRoutineV1,
|
||||
RoutineRevisionSnapshotTriggerV1,
|
||||
RoutineRevisionSnapshotV1,
|
||||
RoutineRevisionSnapshot,
|
||||
RoutineRevision,
|
||||
RoutineTrigger,
|
||||
RoutineRun,
|
||||
RoutineTriggerSecretMaterial,
|
||||
@@ -496,6 +579,18 @@ export type {
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginEnvironmentDriverDeclaration,
|
||||
PluginManagedAgentDeclaration,
|
||||
PluginManagedProjectDeclaration,
|
||||
PluginManagedRoutineDeclaration,
|
||||
PluginManagedSkillDeclaration,
|
||||
PluginManagedSkillFileDeclaration,
|
||||
PluginLocalFolderDeclaration,
|
||||
PluginManagedAgentResolution,
|
||||
PluginManagedProjectResolution,
|
||||
PluginManagedRoutineResolution,
|
||||
PluginManagedSkillResolution,
|
||||
PluginManagedResourceKind,
|
||||
PluginManagedResourceRef,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
PluginLauncherRenderDeclaration,
|
||||
@@ -512,6 +607,7 @@ export type {
|
||||
PluginMigrationRecord,
|
||||
PluginStateRecord,
|
||||
PluginConfig,
|
||||
PluginCompanySettings,
|
||||
PluginEntityRecord,
|
||||
PluginEntityQuery,
|
||||
PluginJobRecord,
|
||||
@@ -520,6 +616,7 @@ export type {
|
||||
QuotaWindow,
|
||||
ProviderQuotaResult,
|
||||
} from "./types/index.js";
|
||||
export { COMPANY_SEARCH_SCOPES } from "./types/index.js";
|
||||
export {
|
||||
ISSUE_REFERENCE_IDENTIFIER_RE,
|
||||
buildIssueReferenceHref,
|
||||
@@ -644,8 +741,17 @@ export {
|
||||
type CreateProjectWorkspace,
|
||||
type UpdateProjectWorkspace,
|
||||
projectExecutionWorkspacePolicySchema,
|
||||
companySearchQuerySchema,
|
||||
COMPANY_SEARCH_DEFAULT_LIMIT,
|
||||
COMPANY_SEARCH_MAX_LIMIT,
|
||||
COMPANY_SEARCH_MAX_OFFSET,
|
||||
COMPANY_SEARCH_MAX_QUERY_LENGTH,
|
||||
COMPANY_SEARCH_MAX_TOKENS,
|
||||
type CompanySearchQuery,
|
||||
createIssueSchema,
|
||||
createIssueInputSchema,
|
||||
createChildIssueSchema,
|
||||
resolveCreateIssueStatusDefault,
|
||||
createIssueLabelSchema,
|
||||
updateIssueSchema,
|
||||
issueExecutionPolicySchema,
|
||||
@@ -653,6 +759,11 @@ export {
|
||||
issueReviewRequestSchema,
|
||||
issueExecutionWorkspaceSettingsSchema,
|
||||
checkoutIssueSchema,
|
||||
issueCommentAuthorTypeSchema,
|
||||
issueCommentPresentationSchema,
|
||||
issueCommentMetadataRowSchema,
|
||||
issueCommentMetadataSectionSchema,
|
||||
issueCommentMetadataSchema,
|
||||
addIssueCommentSchema,
|
||||
issueThreadInteractionStatusSchema,
|
||||
issueThreadInteractionKindSchema,
|
||||
@@ -745,7 +856,19 @@ export {
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
remoteSecretImportSelectionSchema,
|
||||
localEncryptedProviderConfigSchema,
|
||||
awsSecretsManagerProviderConfigSchema,
|
||||
gcpSecretManagerProviderConfigSchema,
|
||||
vaultProviderConfigSchema,
|
||||
secretProviderConfigPayloadSchema,
|
||||
createSecretBindingSchema,
|
||||
rotateSecretSchema,
|
||||
secretBindingTargetSchema,
|
||||
updateSecretSchema,
|
||||
createRoutineSchema,
|
||||
updateRoutineSchema,
|
||||
@@ -754,7 +877,16 @@ export {
|
||||
routineVariableSchema,
|
||||
runRoutineSchema,
|
||||
rotateRoutineTriggerSecretSchema,
|
||||
routineRevisionSnapshotRoutineV1Schema,
|
||||
routineRevisionSnapshotTriggerV1Schema,
|
||||
routineRevisionSnapshotV1Schema,
|
||||
routineRevisionSnapshotSchema,
|
||||
type CreateSecret,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type RemoteSecretImportPreview,
|
||||
type RemoteSecretImport,
|
||||
type RemoteSecretImportSelection,
|
||||
type RotateSecret,
|
||||
type UpdateSecret,
|
||||
type CreateRoutine,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
describe("issue references", () => {
|
||||
it("normalizes identifiers to uppercase", () => {
|
||||
expect(normalizeIssueIdentifier("pap-123")).toBe("PAP-123");
|
||||
expect(normalizeIssueIdentifier("pc1a2-7")).toBe("PC1A2-7");
|
||||
expect(normalizeIssueIdentifier("not-an-issue")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -27,14 +28,14 @@ describe("issue references", () => {
|
||||
});
|
||||
|
||||
it("finds identifiers and issue paths in plain text", () => {
|
||||
expect(findIssueReferenceMatches("See PAP-1, /issues/PAP-2, and https://x.test/PAP/issues/pap-3.")).toEqual([
|
||||
expect(findIssueReferenceMatches("See PAP-1, /issues/PC1A2-2, and https://x.test/PAP/issues/pc1a2-3.")).toEqual([
|
||||
{ index: 4, length: 5, identifier: "PAP-1", matchedText: "PAP-1" },
|
||||
{ index: 11, length: 13, identifier: "PAP-2", matchedText: "/issues/PAP-2" },
|
||||
{ index: 11, length: 15, identifier: "PC1A2-2", matchedText: "/issues/PC1A2-2" },
|
||||
{
|
||||
index: 30,
|
||||
length: 31,
|
||||
identifier: "PAP-3",
|
||||
matchedText: "https://x.test/PAP/issues/pap-3",
|
||||
index: 32,
|
||||
length: 33,
|
||||
identifier: "PC1A2-3",
|
||||
matchedText: "https://x.test/PAP/issues/pc1a2-3",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ISSUE_REFERENCE_IDENTIFIER_RE = /^[A-Z]+-\d+$/;
|
||||
export const ISSUE_REFERENCE_IDENTIFIER_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
||||
|
||||
export interface IssueReferenceMatch {
|
||||
index: number;
|
||||
@@ -7,7 +7,7 @@ export interface IssueReferenceMatch {
|
||||
matchedText: string;
|
||||
}
|
||||
|
||||
const ISSUE_REFERENCE_TOKEN_RE = /https?:\/\/[^\s<>()]+|\/[^\s<>()]+|[A-Z]+-\d+/gi;
|
||||
const ISSUE_REFERENCE_TOKEN_RE = /https?:\/\/[^\s<>()]+|\/[^\s<>()]+|[A-Z][A-Z0-9]*-\d+/gi;
|
||||
|
||||
function preserveNewlinesAsWhitespace(value: string) {
|
||||
return value.replace(/[^\n]/g, " ");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { AgentEnvConfig, SecretProvider } from "./secrets.js";
|
||||
import type { RoutineVariable } from "./routine.js";
|
||||
import type { IssueCommentAuthorType } from "../constants.js";
|
||||
import type { IssueCommentMetadata, IssueCommentPresentation } from "./issue.js";
|
||||
|
||||
export interface CompanyPortabilityInclude {
|
||||
company: boolean;
|
||||
@@ -98,6 +100,16 @@ export interface CompanyPortabilityIssueRoutineManifestEntry {
|
||||
triggers: CompanyPortabilityIssueRoutineTriggerManifestEntry[];
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityIssueCommentManifestEntry {
|
||||
body: string;
|
||||
authorType: IssueCommentAuthorType;
|
||||
authorAgentSlug: string | null;
|
||||
authorUserId: string | null;
|
||||
presentation: IssueCommentPresentation | null;
|
||||
metadata: IssueCommentMetadata | null;
|
||||
createdAt: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityIssueManifestEntry {
|
||||
slug: string;
|
||||
identifier: string | null;
|
||||
@@ -116,6 +128,7 @@ export interface CompanyPortabilityIssueManifestEntry {
|
||||
billingCode: string | null;
|
||||
executionWorkspaceSettings: Record<string, unknown> | null;
|
||||
assigneeAdapterOverrides: Record<string, unknown> | null;
|
||||
comments: CompanyPortabilityIssueCommentManifestEntry[];
|
||||
metadata: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ export interface IssueCostSummary {
|
||||
inputTokens: number;
|
||||
cachedInputTokens: number;
|
||||
outputTokens: number;
|
||||
/** number of distinct heartbeat runs aggregated across the issue tree */
|
||||
runCount: number;
|
||||
/** sum of wall-clock duration of each run in the tree (ms);
|
||||
* still-running runs contribute (now - startedAt) so this ticks up live */
|
||||
runtimeMs: number;
|
||||
}
|
||||
|
||||
export interface CostByAgent {
|
||||
|
||||
@@ -89,7 +89,17 @@ export type {
|
||||
AdapterEnvironmentTestResult,
|
||||
} from "./agent.js";
|
||||
export type { AssetImage } from "./asset.js";
|
||||
export type { Project, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectWorkspace } from "./project.js";
|
||||
export type { Project, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectManagedByPlugin, ProjectWorkspace } from "./project.js";
|
||||
export type {
|
||||
CompanySearchHighlight,
|
||||
CompanySearchIssueSummary,
|
||||
CompanySearchResponse,
|
||||
CompanySearchResult,
|
||||
CompanySearchResultType,
|
||||
CompanySearchScope,
|
||||
CompanySearchSnippet,
|
||||
} from "./search.js";
|
||||
export { COMPANY_SEARCH_SCOPES } from "./search.js";
|
||||
export type {
|
||||
ExecutionWorkspace,
|
||||
ExecutionWorkspaceSummary,
|
||||
@@ -134,17 +144,26 @@ export type {
|
||||
} from "./work-product.js";
|
||||
export type {
|
||||
Issue,
|
||||
IssueWorkMode,
|
||||
IssueAssigneeAdapterOverrides,
|
||||
IssueBlockerAttention,
|
||||
IssueBlockerAttentionReason,
|
||||
IssueBlockerAttentionState,
|
||||
IssueProductivityReview,
|
||||
IssueProductivityReviewTrigger,
|
||||
SuccessfulRunHandoffState,
|
||||
SuccessfulRunHandoffStateKind,
|
||||
IssueScheduledRetry,
|
||||
IssueScheduledRetryStatus,
|
||||
IssueRetryNowOutcome,
|
||||
IssueRetryNowResponse,
|
||||
IssueReferenceSource,
|
||||
IssueRelatedWorkItem,
|
||||
IssueRelatedWorkSummary,
|
||||
IssueRelation,
|
||||
IssueRelationIssueSummary,
|
||||
IssueExecutionMonitorPolicy,
|
||||
IssueExecutionMonitorState,
|
||||
IssueExecutionPolicy,
|
||||
IssueExecutionState,
|
||||
IssueExecutionStage,
|
||||
@@ -153,6 +172,16 @@ export type {
|
||||
IssueReviewRequest,
|
||||
IssueExecutionDecision,
|
||||
IssueComment,
|
||||
IssueCommentMetadata,
|
||||
IssueCommentMetadataSection,
|
||||
IssueCommentMetadataRow,
|
||||
IssueCommentMetadataTextRow,
|
||||
IssueCommentMetadataCodeRow,
|
||||
IssueCommentMetadataKeyValueRow,
|
||||
IssueCommentMetadataIssueLinkRow,
|
||||
IssueCommentMetadataAgentLinkRow,
|
||||
IssueCommentMetadataRunLinkRow,
|
||||
IssueCommentPresentation,
|
||||
IssueThreadInteractionActorFields,
|
||||
SuggestedTaskDraft,
|
||||
SuggestTasksPayload,
|
||||
@@ -215,12 +244,39 @@ export type {
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
SecretProviderConfigHealthResponse,
|
||||
CompanySecretBinding,
|
||||
CompanySecretBindingTarget,
|
||||
CompanySecretUsageBinding,
|
||||
CompanySecretVersion,
|
||||
SecretAccessEvent,
|
||||
RemoteSecretImportCandidate,
|
||||
RemoteSecretImportCandidateStatus,
|
||||
RemoteSecretImportConflict,
|
||||
RemoteSecretImportPreviewResult,
|
||||
RemoteSecretImportResult,
|
||||
RemoteSecretImportRowResult,
|
||||
RemoteSecretImportRowStatus,
|
||||
SecretAccessOutcome,
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretProviderDescriptor,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
} from "./secrets.js";
|
||||
export type {
|
||||
Routine,
|
||||
RoutineManagedByPlugin,
|
||||
RoutineVariable,
|
||||
RoutineVariableDefaultValue,
|
||||
RoutineRevisionSnapshotRoutineV1,
|
||||
RoutineRevisionSnapshotTriggerV1,
|
||||
RoutineRevisionSnapshotV1,
|
||||
RoutineRevisionSnapshot,
|
||||
RoutineRevision,
|
||||
RoutineTrigger,
|
||||
RoutineRun,
|
||||
RoutineTriggerSecretMaterial,
|
||||
@@ -288,6 +344,7 @@ export type {
|
||||
CompanyPortabilityProjectWorkspaceManifestEntry,
|
||||
CompanyPortabilityIssueRoutineTriggerManifestEntry,
|
||||
CompanyPortabilityIssueRoutineManifestEntry,
|
||||
CompanyPortabilityIssueCommentManifestEntry,
|
||||
CompanyPortabilityIssueManifestEntry,
|
||||
CompanyPortabilityManifest,
|
||||
CompanyPortabilityExportResult,
|
||||
@@ -314,6 +371,18 @@ export type {
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginEnvironmentDriverDeclaration,
|
||||
PluginManagedAgentDeclaration,
|
||||
PluginManagedProjectDeclaration,
|
||||
PluginManagedRoutineDeclaration,
|
||||
PluginManagedSkillDeclaration,
|
||||
PluginManagedSkillFileDeclaration,
|
||||
PluginLocalFolderDeclaration,
|
||||
PluginManagedAgentResolution,
|
||||
PluginManagedProjectResolution,
|
||||
PluginManagedRoutineResolution,
|
||||
PluginManagedSkillResolution,
|
||||
PluginManagedResourceKind,
|
||||
PluginManagedResourceRef,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
PluginLauncherRenderDeclaration,
|
||||
@@ -330,6 +399,7 @@ export type {
|
||||
PluginMigrationRecord,
|
||||
PluginStateRecord,
|
||||
PluginConfig,
|
||||
PluginCompanySettings,
|
||||
PluginEntityRecord,
|
||||
PluginEntityQuery,
|
||||
PluginJobRecord,
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import type {
|
||||
IssueCommentAuthorType,
|
||||
IssueCommentMetadataRowType,
|
||||
IssueCommentPresentationKind,
|
||||
IssueCommentPresentationTone,
|
||||
IssueExecutionMonitorClearReason,
|
||||
IssueExecutionMonitorKind,
|
||||
IssueExecutionMonitorRecoveryPolicy,
|
||||
IssueExecutionMonitorStateStatus,
|
||||
IssueExecutionDecisionOutcome,
|
||||
IssueMonitorScheduledBy,
|
||||
IssueExecutionPolicyMode,
|
||||
IssueReferenceSourceKind,
|
||||
IssueExecutionStageType,
|
||||
IssueExecutionStateStatus,
|
||||
IssueOriginKind,
|
||||
IssuePriority,
|
||||
IssueWorkMode,
|
||||
ModelProfileKey,
|
||||
IssueThreadInteractionContinuationPolicy,
|
||||
IssueThreadInteractionKind,
|
||||
@@ -17,6 +27,8 @@ import type { Project, ProjectWorkspace } from "./project.js";
|
||||
import type { ExecutionWorkspace, IssueExecutionWorkspaceSettings } from "./workspace-runtime.js";
|
||||
import type { IssueWorkProduct } from "./work-product.js";
|
||||
|
||||
export type { IssueWorkMode };
|
||||
|
||||
export interface IssueAncestorProject {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -157,6 +169,46 @@ export interface IssueProductivityReview {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type SuccessfulRunHandoffStateKind = "required" | "resolved" | "escalated";
|
||||
|
||||
export interface SuccessfulRunHandoffState {
|
||||
state: SuccessfulRunHandoffStateKind;
|
||||
required: boolean;
|
||||
sourceRunId: string | null;
|
||||
correctiveRunId: string | null;
|
||||
assigneeAgentId: string | null;
|
||||
detectedProgressSummary: string | null;
|
||||
createdAt: Date | string | null;
|
||||
}
|
||||
|
||||
export type IssueScheduledRetryStatus = "scheduled_retry" | "queued" | "running" | "cancelled";
|
||||
|
||||
export interface IssueScheduledRetry {
|
||||
runId: string;
|
||||
status: IssueScheduledRetryStatus;
|
||||
agentId: string;
|
||||
agentName: string | null;
|
||||
retryOfRunId: string | null;
|
||||
scheduledRetryAt: Date | string | null;
|
||||
scheduledRetryAttempt: number;
|
||||
scheduledRetryReason: string | null;
|
||||
retryExhaustedReason?: string | null;
|
||||
error?: string | null;
|
||||
errorCode?: string | null;
|
||||
}
|
||||
|
||||
export type IssueRetryNowOutcome =
|
||||
| "promoted"
|
||||
| "already_promoted"
|
||||
| "no_scheduled_retry"
|
||||
| "gate_suppressed";
|
||||
|
||||
export interface IssueRetryNowResponse {
|
||||
outcome: IssueRetryNowOutcome;
|
||||
message: string;
|
||||
scheduledRetry: IssueScheduledRetry | null;
|
||||
}
|
||||
|
||||
export interface IssueRelation {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -201,10 +253,40 @@ export interface IssueExecutionStage {
|
||||
participants: IssueExecutionStageParticipant[];
|
||||
}
|
||||
|
||||
export interface IssueExecutionMonitorPolicy {
|
||||
nextCheckAt: string;
|
||||
notes: string | null;
|
||||
scheduledBy: IssueMonitorScheduledBy;
|
||||
kind?: IssueExecutionMonitorKind | null;
|
||||
serviceName?: string | null;
|
||||
externalRef?: string | null;
|
||||
timeoutAt?: string | null;
|
||||
maxAttempts?: number | null;
|
||||
recoveryPolicy?: IssueExecutionMonitorRecoveryPolicy | null;
|
||||
}
|
||||
|
||||
export interface IssueExecutionPolicy {
|
||||
mode: IssueExecutionPolicyMode;
|
||||
commentRequired: boolean;
|
||||
stages: IssueExecutionStage[];
|
||||
monitor?: IssueExecutionMonitorPolicy | null;
|
||||
}
|
||||
|
||||
export interface IssueExecutionMonitorState {
|
||||
status: IssueExecutionMonitorStateStatus;
|
||||
nextCheckAt: string | null;
|
||||
lastTriggeredAt: string | null;
|
||||
attemptCount: number;
|
||||
notes: string | null;
|
||||
scheduledBy: IssueMonitorScheduledBy | null;
|
||||
kind?: IssueExecutionMonitorKind | null;
|
||||
serviceName?: string | null;
|
||||
externalRef?: string | null;
|
||||
timeoutAt?: string | null;
|
||||
maxAttempts?: number | null;
|
||||
recoveryPolicy?: IssueExecutionMonitorRecoveryPolicy | null;
|
||||
clearedAt: string | null;
|
||||
clearReason: IssueExecutionMonitorClearReason | null;
|
||||
}
|
||||
|
||||
export interface IssueReviewRequest {
|
||||
@@ -222,6 +304,7 @@ export interface IssueExecutionState {
|
||||
completedStageIds: string[];
|
||||
lastDecisionId: string | null;
|
||||
lastDecisionOutcome: IssueExecutionDecisionOutcome | null;
|
||||
monitor?: IssueExecutionMonitorState | null;
|
||||
}
|
||||
|
||||
export interface IssueExecutionDecision {
|
||||
@@ -250,6 +333,7 @@ export interface Issue {
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: IssueStatus;
|
||||
workMode: IssueWorkMode;
|
||||
priority: IssuePriority;
|
||||
assigneeAgentId: string | null;
|
||||
assigneeUserId: string | null;
|
||||
@@ -270,6 +354,11 @@ export interface Issue {
|
||||
assigneeAdapterOverrides: IssueAssigneeAdapterOverrides | null;
|
||||
executionPolicy?: IssueExecutionPolicy | null;
|
||||
executionState?: IssueExecutionState | null;
|
||||
monitorNextCheckAt?: Date | null;
|
||||
monitorLastTriggeredAt?: Date | null;
|
||||
monitorAttemptCount?: number;
|
||||
monitorNotes?: string | null;
|
||||
monitorScheduledBy?: IssueMonitorScheduledBy | null;
|
||||
executionWorkspaceId: string | null;
|
||||
executionWorkspacePreference: string | null;
|
||||
executionWorkspaceSettings: IssueExecutionWorkspaceSettings | null;
|
||||
@@ -283,6 +372,8 @@ export interface Issue {
|
||||
blocks?: IssueRelationIssueSummary[];
|
||||
blockerAttention?: IssueBlockerAttention;
|
||||
productivityReview?: IssueProductivityReview | null;
|
||||
successfulRunHandoff?: SuccessfulRunHandoffState | null;
|
||||
scheduledRetry?: IssueScheduledRetry | null;
|
||||
relatedWork?: IssueRelatedWorkSummary;
|
||||
referencedIssueIdentifiers?: string[];
|
||||
planDocument?: IssueDocument | null;
|
||||
@@ -305,14 +396,84 @@ export interface IssueComment {
|
||||
id: string;
|
||||
companyId: string;
|
||||
issueId: string;
|
||||
authorType: IssueCommentAuthorType;
|
||||
authorAgentId: string | null;
|
||||
authorUserId: string | null;
|
||||
body: string;
|
||||
presentation: IssueCommentPresentation | null;
|
||||
metadata: IssueCommentMetadata | null;
|
||||
followUpRequested?: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface IssueCommentMetadataRowBase {
|
||||
type: IssueCommentMetadataRowType;
|
||||
label?: string | null;
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadataTextRow extends IssueCommentMetadataRowBase {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadataCodeRow extends IssueCommentMetadataRowBase {
|
||||
type: "code";
|
||||
code: string;
|
||||
language?: string | null;
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadataKeyValueRow extends IssueCommentMetadataRowBase {
|
||||
type: "key_value";
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadataIssueLinkRow extends IssueCommentMetadataRowBase {
|
||||
type: "issue_link";
|
||||
issueId?: string | null;
|
||||
identifier?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadataAgentLinkRow extends IssueCommentMetadataRowBase {
|
||||
type: "agent_link";
|
||||
agentId: string;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadataRunLinkRow extends IssueCommentMetadataRowBase {
|
||||
type: "run_link";
|
||||
runId: string;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
export type IssueCommentMetadataRow =
|
||||
| IssueCommentMetadataTextRow
|
||||
| IssueCommentMetadataCodeRow
|
||||
| IssueCommentMetadataKeyValueRow
|
||||
| IssueCommentMetadataIssueLinkRow
|
||||
| IssueCommentMetadataAgentLinkRow
|
||||
| IssueCommentMetadataRunLinkRow;
|
||||
|
||||
export interface IssueCommentMetadataSection {
|
||||
title?: string | null;
|
||||
rows: IssueCommentMetadataRow[];
|
||||
}
|
||||
|
||||
export interface IssueCommentMetadata {
|
||||
version: 1;
|
||||
sourceRunId?: string | null;
|
||||
sections: IssueCommentMetadataSection[];
|
||||
}
|
||||
|
||||
export interface IssueCommentPresentation {
|
||||
kind: IssueCommentPresentationKind;
|
||||
tone: IssueCommentPresentationTone;
|
||||
title?: string | null;
|
||||
detailsDefaultOpen: boolean;
|
||||
}
|
||||
|
||||
export interface IssueThreadInteractionActorFields {
|
||||
createdByAgentId?: string | null;
|
||||
createdByUserId?: string | null;
|
||||
@@ -327,6 +488,7 @@ export interface SuggestedTaskDraft {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
priority?: IssuePriority | null;
|
||||
workMode?: IssueWorkMode | null;
|
||||
assigneeAgentId?: string | null;
|
||||
assigneeUserId?: string | null;
|
||||
projectId?: string | null;
|
||||
|
||||
@@ -16,7 +16,20 @@ import type {
|
||||
PluginDatabaseMigrationStatus,
|
||||
PluginDatabaseNamespaceMode,
|
||||
PluginDatabaseNamespaceStatus,
|
||||
AgentAdapterType,
|
||||
AgentRole,
|
||||
AgentStatus,
|
||||
IssuePriority,
|
||||
ProjectStatus,
|
||||
RoutineCatchUpPolicy,
|
||||
RoutineConcurrencyPolicy,
|
||||
RoutineStatus,
|
||||
IssueSurfaceVisibility,
|
||||
} from "../constants.js";
|
||||
import type { Agent } from "./agent.js";
|
||||
import type { CompanySkill } from "./company-skill.js";
|
||||
import type { Project } from "./project.js";
|
||||
import type { Routine, RoutineTrigger, RoutineVariable } from "./routine.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON Schema placeholder – plugins declare config schemas as JSON Schema
|
||||
@@ -113,6 +126,206 @@ export interface PluginEnvironmentDriverDeclaration {
|
||||
configSchema: JsonSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a normal Paperclip agent that a plugin can provision and later
|
||||
* resolve by stable key within each company.
|
||||
*/
|
||||
export interface PluginManagedAgentDeclaration {
|
||||
/** Stable identifier for this managed agent, unique within the plugin. */
|
||||
agentKey: string;
|
||||
/** Suggested visible agent name. */
|
||||
displayName: string;
|
||||
/** Optional suggested role. Defaults to `general`. */
|
||||
role?: AgentRole | string;
|
||||
/** Optional suggested title shown in agent surfaces. */
|
||||
title?: string | null;
|
||||
/** Optional icon for agent list/detail surfaces. */
|
||||
icon?: string | null;
|
||||
/** Suggested capability summary for the agent. */
|
||||
capabilities?: string | null;
|
||||
/** Suggested adapter type. Defaults to `process`. */
|
||||
adapterType?: AgentAdapterType | string;
|
||||
/**
|
||||
* Optional ordered list of compatible adapter types. When present, the host
|
||||
* prefers the most-used compatible adapter already configured in the company,
|
||||
* falling back to `adapterType`.
|
||||
*/
|
||||
adapterPreference?: Array<AgentAdapterType | string>;
|
||||
/** Suggested adapter configuration. */
|
||||
adapterConfig?: Record<string, unknown>;
|
||||
/** Suggested Paperclip runtime configuration. */
|
||||
runtimeConfig?: Record<string, unknown>;
|
||||
/** Suggested permissions object. Normalized by the host on create/reset. */
|
||||
permissions?: Record<string, unknown>;
|
||||
/** Suggested starting status when no board approval is required. */
|
||||
status?: Extract<AgentStatus, "idle" | "paused">;
|
||||
/** Suggested monthly budget in cents. */
|
||||
budgetMonthlyCents?: number;
|
||||
/** Optional managed instructions content or pointer metadata for plugin UI. */
|
||||
instructions?: {
|
||||
entryFile?: string;
|
||||
content?: string;
|
||||
files?: Record<string, string>;
|
||||
assetPath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a company-scoped local folder a trusted plugin wants the operator
|
||||
* to configure. The host treats this as a generic filesystem root: plugin
|
||||
* code may request required relative folders/files, then use SDK helpers for
|
||||
* path-safe reads and atomic writes under that root.
|
||||
*/
|
||||
export interface PluginLocalFolderDeclaration {
|
||||
/** Stable identifier for this folder, unique within the plugin. */
|
||||
folderKey: string;
|
||||
/** Human-readable name shown in plugin settings. */
|
||||
displayName: string;
|
||||
/** Optional operator-facing description. */
|
||||
description?: string;
|
||||
/** Access level requested by the plugin. Defaults to `readWrite`. */
|
||||
access?: "read" | "readWrite";
|
||||
/** Relative directories expected to exist under the configured root. */
|
||||
requiredDirectories?: string[];
|
||||
/** Relative files expected to exist under the configured root. */
|
||||
requiredFiles?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a normal Paperclip project that a plugin can provision and later
|
||||
* resolve by stable key within each company.
|
||||
*/
|
||||
export interface PluginManagedProjectDeclaration {
|
||||
/** Stable identifier for this managed project, unique within the plugin. */
|
||||
projectKey: string;
|
||||
/** Suggested visible project name. */
|
||||
displayName: string;
|
||||
/** Suggested project description. */
|
||||
description?: string | null;
|
||||
/** Suggested starting status. Defaults to `in_progress`. */
|
||||
status?: ProjectStatus;
|
||||
/** Suggested project color. Defaults to the normal project palette. */
|
||||
color?: string | null;
|
||||
/** Optional plugin-specific defaults retained for reset/reconcile UI. */
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PluginManagedSkillFileDeclaration {
|
||||
/** Relative path inside the skill folder, for example `references/guide.md`. */
|
||||
path: string;
|
||||
/** File contents written when the skill is installed or reset. */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a company skill that a plugin can install into each company's
|
||||
* skills library and later resolve by stable key.
|
||||
*/
|
||||
export interface PluginManagedSkillDeclaration {
|
||||
/** Stable identifier for this managed skill, unique within the plugin. */
|
||||
skillKey: string;
|
||||
/** Suggested visible skill name. */
|
||||
displayName: string;
|
||||
/** Suggested skill slug. Defaults to `skillKey`. */
|
||||
slug?: string;
|
||||
/** Suggested skill description. */
|
||||
description?: string | null;
|
||||
/** Full `SKILL.md` contents. Defaults to generated markdown from display metadata. */
|
||||
markdown?: string;
|
||||
/** Additional files installed with the skill. */
|
||||
files?: PluginManagedSkillFileDeclaration[];
|
||||
}
|
||||
|
||||
export type PluginManagedResourceKind = "agent" | "project" | "routine" | "skill";
|
||||
|
||||
export interface PluginManagedResourceRef {
|
||||
pluginKey?: string;
|
||||
resourceKind: PluginManagedResourceKind;
|
||||
resourceKey: string;
|
||||
}
|
||||
|
||||
export interface PluginManagedRoutineDeclaration {
|
||||
/** Stable identifier for this managed routine, unique within the plugin. */
|
||||
routineKey: string;
|
||||
/** Suggested routine title template. */
|
||||
title: string;
|
||||
/** Suggested routine description template. */
|
||||
description?: string | null;
|
||||
/** Stable managed agent reference for the default assignee. */
|
||||
assigneeRef?: PluginManagedResourceRef | null;
|
||||
/** Stable managed project reference for routine-created issues. */
|
||||
projectRef?: PluginManagedResourceRef | null;
|
||||
/** Optional goal id to set on the routine in this company. */
|
||||
goalId?: string | null;
|
||||
/** Suggested starting status. Defaults to `paused` when no assignee is resolved, otherwise `active`. */
|
||||
status?: RoutineStatus;
|
||||
/** Suggested issue priority. Defaults to `medium`. */
|
||||
priority?: IssuePriority;
|
||||
/** Suggested concurrency behavior. Defaults to core routine default. */
|
||||
concurrencyPolicy?: RoutineConcurrencyPolicy;
|
||||
/** Suggested missed-trigger behavior. Defaults to core routine default. */
|
||||
catchUpPolicy?: RoutineCatchUpPolicy;
|
||||
/** Suggested routine variables. */
|
||||
variables?: RoutineVariable[];
|
||||
/** Suggested triggers created when the routine is first reconciled. */
|
||||
triggers?: Array<Pick<RoutineTrigger, "kind" | "label" | "enabled" | "cronExpression" | "timezone" | "signingMode" | "replayWindowSec">>;
|
||||
/** Defaults for issues created by this routine. */
|
||||
issueTemplate?: {
|
||||
surfaceVisibility?: IssueSurfaceVisibility;
|
||||
originId?: string | null;
|
||||
billingCode?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginManagedAgentResolution {
|
||||
pluginKey: string;
|
||||
resourceKind: "agent";
|
||||
resourceKey: string;
|
||||
companyId: string;
|
||||
agentId: string | null;
|
||||
agent: Agent | null;
|
||||
status: "missing" | "resolved" | "created" | "relinked" | "reset";
|
||||
approvalId?: string | null;
|
||||
defaultDrift?: {
|
||||
entryFile: string;
|
||||
changedFiles: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface PluginManagedProjectResolution {
|
||||
pluginKey: string;
|
||||
resourceKind: "project";
|
||||
resourceKey: string;
|
||||
companyId: string;
|
||||
projectId: string | null;
|
||||
project: Project | null;
|
||||
status: "missing" | "resolved" | "created" | "relinked" | "reset";
|
||||
}
|
||||
|
||||
export interface PluginManagedRoutineResolution {
|
||||
pluginKey: string;
|
||||
resourceKind: "routine";
|
||||
resourceKey: string;
|
||||
companyId: string;
|
||||
routineId: string | null;
|
||||
routine: Routine | null;
|
||||
status: "missing" | "missing_refs" | "resolved" | "created" | "relinked" | "reset";
|
||||
missingRefs?: PluginManagedResourceRef[];
|
||||
}
|
||||
|
||||
export interface PluginManagedSkillResolution {
|
||||
pluginKey: string;
|
||||
resourceKind: "skill";
|
||||
resourceKey: string;
|
||||
companyId: string;
|
||||
skillId: string | null;
|
||||
skill: CompanySkill | null;
|
||||
status: "missing" | "resolved" | "created" | "relinked" | "reset";
|
||||
defaultDrift?: {
|
||||
changedFiles: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a UI extension slot the plugin fills with a React component.
|
||||
*
|
||||
@@ -133,7 +346,7 @@ export interface PluginUiSlotDeclaration {
|
||||
*/
|
||||
entityTypes?: PluginUiSlotEntityType[];
|
||||
/**
|
||||
* Optional company-scoped route segment for page slots.
|
||||
* Optional company-scoped route segment for page and routeSidebar slots.
|
||||
* Example: `kitchensink` becomes `/:companyPrefix/kitchensink`.
|
||||
*/
|
||||
routePath?: string;
|
||||
@@ -322,6 +535,16 @@ export interface PaperclipPluginManifestV1 {
|
||||
apiRoutes?: PluginApiRouteDeclaration[];
|
||||
/** Environment drivers this plugin contributes. Requires `environment.drivers.register` capability. */
|
||||
environmentDrivers?: PluginEnvironmentDriverDeclaration[];
|
||||
/** Suggested company-scoped agents this plugin can provision and resolve by stable key. */
|
||||
agents?: PluginManagedAgentDeclaration[];
|
||||
/** Suggested company-scoped projects this plugin can provision and resolve by stable key. */
|
||||
projects?: PluginManagedProjectDeclaration[];
|
||||
/** Suggested company-scoped routines this plugin can provision and resolve by stable key. */
|
||||
routines?: PluginManagedRoutineDeclaration[];
|
||||
/** Suggested company skills this plugin can install and resolve by stable key. */
|
||||
skills?: PluginManagedSkillDeclaration[];
|
||||
/** Trusted local folders this plugin can configure and access by stable key. */
|
||||
localFolders?: PluginLocalFolderDeclaration[];
|
||||
/**
|
||||
* Legacy top-level launcher declarations.
|
||||
* Prefer `ui.launchers` for new manifests.
|
||||
@@ -455,6 +678,22 @@ export interface PluginConfig {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Company-scoped plugin settings row. This is intentionally generic; plugin
|
||||
* features such as local folders live inside `settingsJson` under namespaced
|
||||
* keys instead of requiring feature-specific database columns.
|
||||
*/
|
||||
export interface PluginCompanySettings {
|
||||
id: string;
|
||||
companyId: string;
|
||||
pluginId: string;
|
||||
enabled: boolean;
|
||||
settingsJson: Record<string, unknown>;
|
||||
lastError: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query filter for `ctx.entities.list`.
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,18 @@ export interface ProjectCodebase {
|
||||
origin: ProjectCodebaseOrigin;
|
||||
}
|
||||
|
||||
export interface ProjectManagedByPlugin {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
pluginKey: string;
|
||||
pluginDisplayName: string;
|
||||
resourceKind: "project";
|
||||
resourceKey: string;
|
||||
defaultsJson: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -73,6 +85,7 @@ export interface Project {
|
||||
codebase: ProjectCodebase;
|
||||
workspaces: ProjectWorkspace[];
|
||||
primaryWorkspace: ProjectWorkspace | null;
|
||||
managedByPlugin?: ProjectManagedByPlugin | null;
|
||||
archivedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import type { IssueOriginKind, RoutineVariableType } from "../constants.js";
|
||||
import type {
|
||||
IssueOriginKind,
|
||||
IssuePriority,
|
||||
RoutineCatchUpPolicy,
|
||||
RoutineConcurrencyPolicy,
|
||||
RoutineStatus,
|
||||
RoutineTriggerKind,
|
||||
RoutineTriggerSigningMode,
|
||||
RoutineVariableType,
|
||||
} from "../constants.js";
|
||||
|
||||
export interface RoutineProjectSummary {
|
||||
id: string;
|
||||
@@ -50,6 +59,8 @@ export interface Routine {
|
||||
concurrencyPolicy: string;
|
||||
catchUpPolicy: string;
|
||||
variables: RoutineVariable[];
|
||||
latestRevisionId: string | null;
|
||||
latestRevisionNumber: number;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
updatedByAgentId: string | null;
|
||||
@@ -58,6 +69,71 @@ export interface Routine {
|
||||
lastEnqueuedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
managedByPlugin?: RoutineManagedByPlugin | null;
|
||||
}
|
||||
|
||||
export interface RoutineManagedByPlugin {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
pluginKey: string;
|
||||
pluginDisplayName: string;
|
||||
resourceKind: "routine";
|
||||
resourceKey: string;
|
||||
defaultsJson: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface RoutineRevisionSnapshotRoutineV1 {
|
||||
id: string;
|
||||
companyId: string;
|
||||
projectId: string | null;
|
||||
goalId: string | null;
|
||||
parentIssueId: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
assigneeAgentId: string | null;
|
||||
priority: IssuePriority;
|
||||
status: RoutineStatus;
|
||||
concurrencyPolicy: RoutineConcurrencyPolicy;
|
||||
catchUpPolicy: RoutineCatchUpPolicy;
|
||||
variables: RoutineVariable[];
|
||||
}
|
||||
|
||||
export interface RoutineRevisionSnapshotTriggerV1 {
|
||||
id: string;
|
||||
kind: RoutineTriggerKind;
|
||||
label: string | null;
|
||||
enabled: boolean;
|
||||
cronExpression: string | null;
|
||||
timezone: string | null;
|
||||
publicId: string | null;
|
||||
signingMode: RoutineTriggerSigningMode | null;
|
||||
replayWindowSec: number | null;
|
||||
}
|
||||
|
||||
export interface RoutineRevisionSnapshotV1 {
|
||||
version: 1;
|
||||
routine: RoutineRevisionSnapshotRoutineV1;
|
||||
triggers: RoutineRevisionSnapshotTriggerV1[];
|
||||
}
|
||||
|
||||
export type RoutineRevisionSnapshot = RoutineRevisionSnapshotV1;
|
||||
|
||||
export interface RoutineRevision {
|
||||
id: string;
|
||||
companyId: string;
|
||||
routineId: string;
|
||||
revisionNumber: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
snapshot: RoutineRevisionSnapshot;
|
||||
changeSummary: string | null;
|
||||
restoredFromRevisionId: string | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
createdByRunId: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface RoutineTrigger {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { IssuePriority, IssueStatus } from "../constants.js";
|
||||
|
||||
export const COMPANY_SEARCH_SCOPES = ["all", "issues", "comments", "documents", "agents", "projects"] as const;
|
||||
export type CompanySearchScope = (typeof COMPANY_SEARCH_SCOPES)[number];
|
||||
|
||||
export type CompanySearchResultType = "issue" | "agent" | "project";
|
||||
|
||||
export interface CompanySearchHighlight {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface CompanySearchSnippet {
|
||||
field: string;
|
||||
label: string;
|
||||
text: string;
|
||||
highlights: CompanySearchHighlight[];
|
||||
}
|
||||
|
||||
export interface CompanySearchIssueSummary {
|
||||
id: string;
|
||||
identifier: string | null;
|
||||
title: string;
|
||||
status: IssueStatus;
|
||||
priority: IssuePriority;
|
||||
assigneeAgentId: string | null;
|
||||
assigneeUserId: string | null;
|
||||
projectId: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CompanySearchResult {
|
||||
id: string;
|
||||
type: CompanySearchResultType;
|
||||
score: number;
|
||||
title: string;
|
||||
href: string;
|
||||
matchedFields: string[];
|
||||
sourceLabel: string | null;
|
||||
snippet: string | null;
|
||||
snippets: CompanySearchSnippet[];
|
||||
issue?: CompanySearchIssueSummary;
|
||||
updatedAt: string | null;
|
||||
previewImageUrl: string | null;
|
||||
}
|
||||
|
||||
export interface CompanySearchResponse {
|
||||
query: string;
|
||||
normalizedQuery: string;
|
||||
scope: CompanySearchScope;
|
||||
limit: number;
|
||||
offset: number;
|
||||
results: CompanySearchResult[];
|
||||
countsByType: Record<CompanySearchResultType, number>;
|
||||
hasMore: boolean;
|
||||
}
|
||||
@@ -1,8 +1,24 @@
|
||||
export type SecretProvider =
|
||||
| "local_encrypted"
|
||||
| "aws_secrets_manager"
|
||||
| "gcp_secret_manager"
|
||||
| "vault";
|
||||
import type {
|
||||
SecretAccessOutcome,
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretProvider,
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
} from "../constants.js";
|
||||
|
||||
export type {
|
||||
SecretAccessOutcome,
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretProvider,
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
};
|
||||
|
||||
export type SecretVersionSelector = number | "latest";
|
||||
|
||||
@@ -25,13 +41,22 @@ export type AgentEnvConfig = Record<string, EnvBinding>;
|
||||
export interface CompanySecret {
|
||||
id: string;
|
||||
companyId: string;
|
||||
key: string;
|
||||
name: string;
|
||||
provider: SecretProvider;
|
||||
status: SecretStatus;
|
||||
managedMode: SecretManagedMode;
|
||||
externalRef: string | null;
|
||||
providerConfigId: string | null;
|
||||
providerMetadata: Record<string, unknown> | null;
|
||||
latestVersion: number;
|
||||
description: string | null;
|
||||
lastResolvedAt: Date | null;
|
||||
lastRotatedAt: Date | null;
|
||||
deletedAt: Date | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
referenceCount?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -40,4 +65,180 @@ export interface SecretProviderDescriptor {
|
||||
id: SecretProvider;
|
||||
label: string;
|
||||
requiresExternalRef: boolean;
|
||||
supportsManagedValues?: boolean;
|
||||
supportsExternalReferences?: boolean;
|
||||
configured?: boolean;
|
||||
}
|
||||
|
||||
export interface LocalEncryptedProviderConfig {
|
||||
backupReminderAcknowledged?: boolean;
|
||||
}
|
||||
|
||||
export interface AwsSecretsManagerProviderConfig {
|
||||
region: string;
|
||||
namespace?: string | null;
|
||||
secretNamePrefix?: string | null;
|
||||
kmsKeyId?: string | null;
|
||||
ownerTag?: string | null;
|
||||
environmentTag?: string | null;
|
||||
}
|
||||
|
||||
export interface GcpSecretManagerProviderConfig {
|
||||
projectId?: string | null;
|
||||
location?: string | null;
|
||||
namespace?: string | null;
|
||||
secretNamePrefix?: string | null;
|
||||
}
|
||||
|
||||
export interface VaultProviderConfig {
|
||||
address?: string | null;
|
||||
namespace?: string | null;
|
||||
mountPath?: string | null;
|
||||
secretPathPrefix?: string | null;
|
||||
}
|
||||
|
||||
export type SecretProviderConfigPayload =
|
||||
| LocalEncryptedProviderConfig
|
||||
| AwsSecretsManagerProviderConfig
|
||||
| GcpSecretManagerProviderConfig
|
||||
| VaultProviderConfig;
|
||||
|
||||
export interface SecretProviderConfigHealthDetails {
|
||||
code: string;
|
||||
message: string;
|
||||
missingFields?: string[];
|
||||
guidance?: string[];
|
||||
}
|
||||
|
||||
export interface CompanySecretProviderConfig {
|
||||
id: string;
|
||||
companyId: string;
|
||||
provider: SecretProvider;
|
||||
displayName: string;
|
||||
status: SecretProviderConfigStatus;
|
||||
isDefault: boolean;
|
||||
config: SecretProviderConfigPayload;
|
||||
healthStatus: SecretProviderConfigHealthStatus | null;
|
||||
healthCheckedAt: Date | null;
|
||||
healthMessage: string | null;
|
||||
healthDetails: SecretProviderConfigHealthDetails | null;
|
||||
disabledAt: Date | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface SecretProviderConfigHealthResponse {
|
||||
configId: string;
|
||||
provider: SecretProvider;
|
||||
status: SecretProviderConfigHealthStatus;
|
||||
message: string;
|
||||
details: SecretProviderConfigHealthDetails;
|
||||
checkedAt: Date;
|
||||
}
|
||||
|
||||
export interface CompanySecretVersion {
|
||||
id: string;
|
||||
secretId: string;
|
||||
version: number;
|
||||
providerVersionRef: string | null;
|
||||
status: SecretVersionStatus;
|
||||
fingerprintSha256: string;
|
||||
rotationJobId: string | null;
|
||||
createdAt: Date;
|
||||
revokedAt: Date | null;
|
||||
}
|
||||
|
||||
export interface CompanySecretBinding {
|
||||
id: string;
|
||||
companyId: string;
|
||||
secretId: string;
|
||||
targetType: SecretBindingTargetType;
|
||||
targetId: string;
|
||||
configPath: string;
|
||||
versionSelector: SecretVersionSelector;
|
||||
required: boolean;
|
||||
label: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CompanySecretBindingTarget {
|
||||
type: SecretBindingTargetType;
|
||||
id: string;
|
||||
label: string;
|
||||
href: string | null;
|
||||
status: string | null;
|
||||
}
|
||||
|
||||
export interface CompanySecretUsageBinding extends CompanySecretBinding {
|
||||
target: CompanySecretBindingTarget;
|
||||
}
|
||||
|
||||
export interface SecretAccessEvent {
|
||||
id: string;
|
||||
companyId: string;
|
||||
secretId: string;
|
||||
version: number | null;
|
||||
provider: SecretProvider;
|
||||
actorType: "agent" | "user" | "system" | "plugin";
|
||||
actorId: string | null;
|
||||
consumerType: SecretBindingTargetType;
|
||||
consumerId: string;
|
||||
configPath: string | null;
|
||||
issueId: string | null;
|
||||
heartbeatRunId: string | null;
|
||||
pluginId: string | null;
|
||||
outcome: SecretAccessOutcome;
|
||||
errorCode: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export type RemoteSecretImportCandidateStatus = "ready" | "duplicate" | "conflict";
|
||||
|
||||
export interface RemoteSecretImportConflict {
|
||||
type: "exact_reference" | "name" | "key" | "provider_guardrail";
|
||||
message: string;
|
||||
existingSecretId?: string;
|
||||
}
|
||||
|
||||
export interface RemoteSecretImportCandidate {
|
||||
externalRef: string;
|
||||
remoteName: string;
|
||||
name: string;
|
||||
key: string;
|
||||
providerVersionRef: string | null;
|
||||
providerMetadata: Record<string, unknown> | null;
|
||||
status: RemoteSecretImportCandidateStatus;
|
||||
importable: boolean;
|
||||
conflicts: RemoteSecretImportConflict[];
|
||||
}
|
||||
|
||||
export interface RemoteSecretImportPreviewResult {
|
||||
providerConfigId: string;
|
||||
provider: SecretProvider;
|
||||
nextToken: string | null;
|
||||
candidates: RemoteSecretImportCandidate[];
|
||||
}
|
||||
|
||||
export type RemoteSecretImportRowStatus = "imported" | "skipped" | "error";
|
||||
|
||||
export interface RemoteSecretImportRowResult {
|
||||
externalRef: string;
|
||||
name: string;
|
||||
key: string;
|
||||
status: RemoteSecretImportRowStatus;
|
||||
reason: string | null;
|
||||
secretId: string | null;
|
||||
conflicts: RemoteSecretImportConflict[];
|
||||
}
|
||||
|
||||
export interface RemoteSecretImportResult {
|
||||
providerConfigId: string;
|
||||
provider: SecretProvider;
|
||||
importedCount: number;
|
||||
skippedCount: number;
|
||||
errorCount: number;
|
||||
results: RemoteSecretImportRowResult[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { MAX_COMPANY_ATTACHMENT_MAX_BYTES } from "../constants.js";
|
||||
import {
|
||||
issueCommentAuthorTypeSchema,
|
||||
issueCommentMetadataSchema,
|
||||
issueCommentPresentationSchema,
|
||||
} from "./issue.js";
|
||||
import { routineVariableSchema } from "./routine.js";
|
||||
|
||||
export const portabilityIncludeSchema = z
|
||||
@@ -134,6 +139,16 @@ export const portabilityIssueRoutineManifestEntrySchema = z.object({
|
||||
triggers: z.array(portabilityIssueRoutineTriggerManifestEntrySchema).default([]),
|
||||
});
|
||||
|
||||
export const portabilityIssueCommentManifestEntrySchema = z.object({
|
||||
body: z.string().min(1),
|
||||
authorType: issueCommentAuthorTypeSchema,
|
||||
authorAgentSlug: z.string().min(1).nullable(),
|
||||
authorUserId: z.string().nullable(),
|
||||
presentation: issueCommentPresentationSchema.nullable(),
|
||||
metadata: issueCommentMetadataSchema.nullable(),
|
||||
createdAt: z.string().datetime().nullable(),
|
||||
});
|
||||
|
||||
export const portabilityIssueManifestEntrySchema = z.object({
|
||||
slug: z.string().min(1),
|
||||
identifier: z.string().min(1).nullable(),
|
||||
@@ -152,6 +167,7 @@ export const portabilityIssueManifestEntrySchema = z.object({
|
||||
billingCode: z.string().nullable(),
|
||||
executionWorkspaceSettings: z.record(z.unknown()).nullable(),
|
||||
assigneeAdapterOverrides: z.record(z.unknown()).nullable(),
|
||||
comments: z.array(portabilityIssueCommentManifestEntrySchema).default([]),
|
||||
metadata: z.record(z.unknown()).nullable(),
|
||||
});
|
||||
|
||||
|
||||
@@ -151,7 +151,9 @@ export {
|
||||
|
||||
export {
|
||||
createIssueSchema,
|
||||
createIssueInputSchema,
|
||||
createChildIssueSchema,
|
||||
resolveCreateIssueStatusDefault,
|
||||
createIssueLabelSchema,
|
||||
updateIssueSchema,
|
||||
issueExecutionPolicySchema,
|
||||
@@ -159,6 +161,11 @@ export {
|
||||
issueReviewRequestSchema,
|
||||
issueExecutionWorkspaceSettingsSchema,
|
||||
checkoutIssueSchema,
|
||||
issueCommentAuthorTypeSchema,
|
||||
issueCommentPresentationSchema,
|
||||
issueCommentMetadataRowSchema,
|
||||
issueCommentMetadataSectionSchema,
|
||||
issueCommentMetadataSchema,
|
||||
addIssueCommentSchema,
|
||||
issueThreadInteractionStatusSchema,
|
||||
issueThreadInteractionKindSchema,
|
||||
@@ -207,6 +214,16 @@ export {
|
||||
type RestoreIssueDocumentRevision,
|
||||
} from "./issue.js";
|
||||
|
||||
export {
|
||||
COMPANY_SEARCH_DEFAULT_LIMIT,
|
||||
COMPANY_SEARCH_MAX_LIMIT,
|
||||
COMPANY_SEARCH_MAX_OFFSET,
|
||||
COMPANY_SEARCH_MAX_QUERY_LENGTH,
|
||||
COMPANY_SEARCH_MAX_TOKENS,
|
||||
companySearchQuerySchema,
|
||||
type CompanySearchQuery,
|
||||
} from "./search.js";
|
||||
|
||||
export {
|
||||
createIssueTreeHoldSchema,
|
||||
issueTreeControlModeSchema,
|
||||
@@ -267,9 +284,27 @@ export {
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
remoteSecretImportSelectionSchema,
|
||||
localEncryptedProviderConfigSchema,
|
||||
awsSecretsManagerProviderConfigSchema,
|
||||
gcpSecretManagerProviderConfigSchema,
|
||||
vaultProviderConfigSchema,
|
||||
secretProviderConfigPayloadSchema,
|
||||
createSecretBindingSchema,
|
||||
rotateSecretSchema,
|
||||
secretBindingTargetSchema,
|
||||
updateSecretSchema,
|
||||
type CreateSecretBinding,
|
||||
type CreateSecret,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type RemoteSecretImportPreview,
|
||||
type RemoteSecretImport,
|
||||
type RemoteSecretImportSelection,
|
||||
type RotateSecret,
|
||||
type UpdateSecret,
|
||||
} from "./secret.js";
|
||||
@@ -280,6 +315,10 @@ export {
|
||||
createRoutineTriggerSchema,
|
||||
updateRoutineTriggerSchema,
|
||||
routineVariableSchema,
|
||||
routineRevisionSnapshotRoutineV1Schema,
|
||||
routineRevisionSnapshotTriggerV1Schema,
|
||||
routineRevisionSnapshotV1Schema,
|
||||
routineRevisionSnapshotSchema,
|
||||
runRoutineSchema,
|
||||
rotateRoutineTriggerSecretSchema,
|
||||
type CreateRoutine,
|
||||
@@ -357,6 +396,8 @@ export {
|
||||
pluginLauncherRenderDeclarationSchema,
|
||||
pluginLauncherDeclarationSchema,
|
||||
pluginDatabaseDeclarationSchema,
|
||||
pluginManagedSkillFileDeclarationSchema,
|
||||
pluginManagedSkillDeclarationSchema,
|
||||
pluginApiRouteDeclarationSchema,
|
||||
pluginManifestV1Schema,
|
||||
installPluginSchema,
|
||||
@@ -376,6 +417,8 @@ export {
|
||||
type PluginLauncherRenderDeclarationInput,
|
||||
type PluginLauncherDeclarationInput,
|
||||
type PluginDatabaseDeclarationInput,
|
||||
type PluginManagedSkillFileDeclarationInput,
|
||||
type PluginManagedSkillDeclarationInput,
|
||||
type PluginApiRouteDeclarationInput,
|
||||
type PluginManifestV1Input,
|
||||
type InstallPlugin,
|
||||
|
||||
@@ -54,6 +54,48 @@ describe("issue validators", () => {
|
||||
expect(parsed.body).toBe("Progress update\n\nNext action.");
|
||||
});
|
||||
|
||||
it("accepts structured issue comment presentation and metadata", () => {
|
||||
const parsed = addIssueCommentSchema.parse({
|
||||
body: "Paperclip needs a disposition before this issue can continue.",
|
||||
authorType: "system",
|
||||
presentation: {
|
||||
kind: "system_notice",
|
||||
tone: "warning",
|
||||
title: "Needs disposition",
|
||||
},
|
||||
metadata: {
|
||||
version: 1,
|
||||
sourceRunId: "11111111-1111-4111-8111-111111111111",
|
||||
sections: [
|
||||
{
|
||||
title: "Evidence",
|
||||
rows: [
|
||||
{ type: "key_value", label: "Cause", value: "successful_run_missing_state" },
|
||||
{ type: "issue_link", label: "Source issue", identifier: "PAP-3440" },
|
||||
{ type: "run_link", label: "Run", runId: "11111111-1111-4111-8111-111111111111" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.presentation?.detailsDefaultOpen).toBe(false);
|
||||
expect(parsed.metadata?.sourceRunId).toBe("11111111-1111-4111-8111-111111111111");
|
||||
expect(parsed.metadata?.sections[0]?.rows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("rejects arbitrary issue comment metadata", () => {
|
||||
const parsed = addIssueCommentSchema.safeParse({
|
||||
body: "Hidden details",
|
||||
metadata: {
|
||||
version: 1,
|
||||
transcript: "raw log dump",
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes escaped line breaks in generated task drafts", () => {
|
||||
const parsed = suggestedTaskDraftSchema.parse({
|
||||
clientKey: "task-1",
|
||||
@@ -87,6 +129,39 @@ describe("issue validators", () => {
|
||||
expect(parsed.requestDepth).toBe(MAX_ISSUE_REQUEST_DEPTH);
|
||||
});
|
||||
|
||||
it("defaults omitted create status to todo when an assignee is present", () => {
|
||||
expect(createIssueSchema.parse({
|
||||
title: "Assigned work",
|
||||
assigneeAgentId: "22222222-2222-4222-8222-222222222222",
|
||||
}).status).toBe("todo");
|
||||
expect(createIssueSchema.parse({ title: "Unassigned work" }).status).toBe("backlog");
|
||||
expect(createIssueSchema.parse({
|
||||
title: "Deliberately parked",
|
||||
assigneeAgentId: "22222222-2222-4222-8222-222222222222",
|
||||
status: "backlog",
|
||||
}).status).toBe("backlog");
|
||||
});
|
||||
|
||||
it("defaults issue work mode to standard and accepts planning", () => {
|
||||
expect(createIssueSchema.parse({ title: "Plan first" }).workMode).toBe("standard");
|
||||
expect(createIssueSchema.parse({ title: "Plan first", workMode: "planning" }).workMode).toBe("planning");
|
||||
expect(updateIssueSchema.parse({ workMode: "planning" }).workMode).toBe("planning");
|
||||
expect(suggestedTaskDraftSchema.parse({
|
||||
clientKey: "planning-child",
|
||||
title: "Plan child",
|
||||
workMode: "planning",
|
||||
}).workMode).toBe("planning");
|
||||
});
|
||||
|
||||
it("rejects unknown issue work modes", () => {
|
||||
expect(createIssueSchema.safeParse({ title: "Plan first", workMode: "normal" }).success).toBe(false);
|
||||
expect(suggestedTaskDraftSchema.safeParse({
|
||||
clientKey: "bad-child",
|
||||
title: "Bad child",
|
||||
workMode: "analysis",
|
||||
}).success).toBe(false);
|
||||
});
|
||||
|
||||
it("clamps oversized requestDepth values on update", () => {
|
||||
const parsed = updateIssueSchema.parse({
|
||||
requestDepth: MAX_ISSUE_REQUEST_DEPTH + 1,
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ISSUE_EXECUTION_DECISION_OUTCOMES,
|
||||
ISSUE_EXECUTION_MONITOR_CLEAR_REASONS,
|
||||
ISSUE_EXECUTION_MONITOR_KINDS,
|
||||
ISSUE_EXECUTION_MONITOR_RECOVERY_POLICIES,
|
||||
ISSUE_EXECUTION_MONITOR_STATE_STATUSES,
|
||||
ISSUE_EXECUTION_POLICY_MODES,
|
||||
ISSUE_EXECUTION_STAGE_TYPES,
|
||||
ISSUE_EXECUTION_STATE_STATUSES,
|
||||
ISSUE_COMMENT_AUTHOR_TYPES,
|
||||
ISSUE_COMMENT_METADATA_ROW_TYPES,
|
||||
ISSUE_COMMENT_PRESENTATION_KINDS,
|
||||
ISSUE_COMMENT_PRESENTATION_TONES,
|
||||
ISSUE_MONITOR_SCHEDULED_BY,
|
||||
ISSUE_PRIORITIES,
|
||||
ISSUE_WORK_MODES,
|
||||
clampIssueRequestDepth,
|
||||
ISSUE_STATUSES,
|
||||
ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES,
|
||||
@@ -103,10 +113,40 @@ export const issueExecutionStageSchema = z.object({
|
||||
participants: z.array(issueExecutionStageParticipantSchema).default([]),
|
||||
});
|
||||
|
||||
export const issueExecutionMonitorPolicySchema = z.object({
|
||||
nextCheckAt: z.string().datetime(),
|
||||
notes: z.string().max(500).optional().nullable().default(null),
|
||||
scheduledBy: z.enum(ISSUE_MONITOR_SCHEDULED_BY).optional().default("assignee"),
|
||||
kind: z.enum(ISSUE_EXECUTION_MONITOR_KINDS).optional().nullable().default(null),
|
||||
serviceName: z.string().trim().min(1).max(120).optional().nullable().default(null),
|
||||
externalRef: z.string().trim().min(1).max(500).optional().nullable().default(null),
|
||||
timeoutAt: z.string().datetime().optional().nullable().default(null),
|
||||
maxAttempts: z.number().int().positive().max(100).optional().nullable().default(null),
|
||||
recoveryPolicy: z.enum(ISSUE_EXECUTION_MONITOR_RECOVERY_POLICIES).optional().nullable().default(null),
|
||||
});
|
||||
|
||||
export const issueExecutionPolicySchema = z.object({
|
||||
mode: z.enum(ISSUE_EXECUTION_POLICY_MODES).optional().default("normal"),
|
||||
commentRequired: z.boolean().optional().default(true),
|
||||
stages: z.array(issueExecutionStageSchema).default([]),
|
||||
monitor: issueExecutionMonitorPolicySchema.optional().nullable(),
|
||||
});
|
||||
|
||||
export const issueExecutionMonitorStateSchema = z.object({
|
||||
status: z.enum(ISSUE_EXECUTION_MONITOR_STATE_STATUSES),
|
||||
nextCheckAt: z.string().datetime().nullable(),
|
||||
lastTriggeredAt: z.string().datetime().nullable(),
|
||||
attemptCount: z.number().int().nonnegative().default(0),
|
||||
notes: z.string().max(500).nullable(),
|
||||
scheduledBy: z.enum(ISSUE_MONITOR_SCHEDULED_BY).nullable(),
|
||||
kind: z.enum(ISSUE_EXECUTION_MONITOR_KINDS).nullable().optional().default(null),
|
||||
serviceName: z.string().trim().min(1).max(120).nullable().optional().default(null),
|
||||
externalRef: z.string().trim().min(1).max(500).nullable().optional().default(null),
|
||||
timeoutAt: z.string().datetime().nullable().optional().default(null),
|
||||
maxAttempts: z.number().int().positive().max(100).nullable().optional().default(null),
|
||||
recoveryPolicy: z.enum(ISSUE_EXECUTION_MONITOR_RECOVERY_POLICIES).nullable().optional().default(null),
|
||||
clearedAt: z.string().datetime().nullable(),
|
||||
clearReason: z.enum(ISSUE_EXECUTION_MONITOR_CLEAR_REASONS).nullable(),
|
||||
});
|
||||
|
||||
export const issueReviewRequestSchema = z.object({
|
||||
@@ -124,6 +164,7 @@ export const issueExecutionStateSchema = z.object({
|
||||
completedStageIds: z.array(z.string().uuid()).default([]),
|
||||
lastDecisionId: z.string().uuid().nullable(),
|
||||
lastDecisionOutcome: z.enum(ISSUE_EXECUTION_DECISION_OUTCOMES).nullable(),
|
||||
monitor: issueExecutionMonitorStateSchema.optional().nullable(),
|
||||
});
|
||||
|
||||
const issueRequestDepthInputSchema = z
|
||||
@@ -132,7 +173,48 @@ const issueRequestDepthInputSchema = z
|
||||
.nonnegative()
|
||||
.transform((value) => clampIssueRequestDepth(value));
|
||||
|
||||
export const createIssueSchema = z.object({
|
||||
type IssueCreateStatusDefaultInput = {
|
||||
status?: unknown;
|
||||
assigneeAgentId?: unknown;
|
||||
assigneeUserId?: unknown;
|
||||
};
|
||||
|
||||
export function resolveCreateIssueStatusDefault(input: IssueCreateStatusDefaultInput): {
|
||||
status: (typeof ISSUE_STATUSES)[number];
|
||||
defaulted: boolean;
|
||||
reason: "explicit" | "assigned_omitted_status" | "unassigned_omitted_status";
|
||||
} {
|
||||
if (typeof input.status === "string") {
|
||||
return {
|
||||
status: input.status as (typeof ISSUE_STATUSES)[number],
|
||||
defaulted: false,
|
||||
reason: "explicit",
|
||||
};
|
||||
}
|
||||
|
||||
const hasAssignee =
|
||||
(typeof input.assigneeAgentId === "string" && input.assigneeAgentId.length > 0)
|
||||
|| (typeof input.assigneeUserId === "string" && input.assigneeUserId.length > 0);
|
||||
return {
|
||||
status: hasAssignee ? "todo" : "backlog",
|
||||
defaulted: true,
|
||||
reason: hasAssignee ? "assigned_omitted_status" : "unassigned_omitted_status",
|
||||
};
|
||||
}
|
||||
|
||||
function withCreateIssueStatusDefault<T extends z.ZodRawShape>(schema: z.ZodObject<T>) {
|
||||
return z.preprocess((input) => {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
||||
const raw = input as Record<string, unknown>;
|
||||
if (raw.status !== undefined) return input;
|
||||
return {
|
||||
...raw,
|
||||
status: resolveCreateIssueStatusDefault(raw).status,
|
||||
};
|
||||
}, schema);
|
||||
}
|
||||
|
||||
const createIssueBaseSchema = z.object({
|
||||
projectId: z.string().uuid().optional().nullable(),
|
||||
projectWorkspaceId: z.string().uuid().optional().nullable(),
|
||||
goalId: z.string().uuid().optional().nullable(),
|
||||
@@ -141,7 +223,8 @@ export const createIssueSchema = z.object({
|
||||
inheritExecutionWorkspaceFromIssueId: z.string().uuid().optional().nullable(),
|
||||
title: z.string().min(1),
|
||||
description: multilineTextSchema.optional().nullable(),
|
||||
status: z.enum(ISSUE_STATUSES).optional().default("backlog"),
|
||||
status: z.enum(ISSUE_STATUSES),
|
||||
workMode: z.enum(ISSUE_WORK_MODES).optional().default("standard"),
|
||||
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
|
||||
assigneeAgentId: z.string().uuid().optional().nullable(),
|
||||
assigneeUserId: z.string().optional().nullable(),
|
||||
@@ -155,9 +238,15 @@ export const createIssueSchema = z.object({
|
||||
labelIds: z.array(z.string().uuid()).optional(),
|
||||
});
|
||||
|
||||
export const createIssueInputSchema = createIssueBaseSchema.extend({
|
||||
status: createIssueBaseSchema.shape.status.optional(),
|
||||
});
|
||||
|
||||
export const createIssueSchema = withCreateIssueStatusDefault(createIssueBaseSchema);
|
||||
|
||||
export type CreateIssue = z.infer<typeof createIssueSchema>;
|
||||
|
||||
export const createChildIssueSchema = createIssueSchema
|
||||
export const createChildIssueSchema = withCreateIssueStatusDefault(createIssueBaseSchema
|
||||
.omit({
|
||||
parentId: true,
|
||||
inheritExecutionWorkspaceFromIssueId: true,
|
||||
@@ -165,7 +254,7 @@ export const createChildIssueSchema = createIssueSchema
|
||||
.extend({
|
||||
acceptanceCriteria: z.array(z.string().trim().min(1).max(500)).max(20).optional(),
|
||||
blockParentUntilDone: z.boolean().optional().default(false),
|
||||
});
|
||||
}));
|
||||
|
||||
export type CreateChildIssue = z.infer<typeof createChildIssueSchema>;
|
||||
|
||||
@@ -176,7 +265,7 @@ export const createIssueLabelSchema = z.object({
|
||||
|
||||
export type CreateIssueLabel = z.infer<typeof createIssueLabelSchema>;
|
||||
|
||||
export const updateIssueSchema = createIssueSchema.partial().extend({
|
||||
export const updateIssueSchema = createIssueBaseSchema.partial().extend({
|
||||
requestDepth: issueRequestDepthInputSchema.optional(),
|
||||
assigneeAgentId: z.string().trim().min(1).optional().nullable(),
|
||||
comment: multilineTextSchema.pipe(z.string().min(1)).optional(),
|
||||
@@ -197,8 +286,96 @@ export const checkoutIssueSchema = z.object({
|
||||
|
||||
export type CheckoutIssue = z.infer<typeof checkoutIssueSchema>;
|
||||
|
||||
const commentMetadataLabelSchema = z.string().trim().min(1).max(120);
|
||||
const commentMetadataTextSchema = z.string().trim().min(1).max(2000);
|
||||
|
||||
export const issueCommentAuthorTypeSchema = z.enum(ISSUE_COMMENT_AUTHOR_TYPES);
|
||||
|
||||
export const issueCommentPresentationSchema = z.object({
|
||||
kind: z.enum(ISSUE_COMMENT_PRESENTATION_KINDS).default("message"),
|
||||
tone: z.enum(ISSUE_COMMENT_PRESENTATION_TONES).default("neutral"),
|
||||
title: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
detailsDefaultOpen: z.boolean().optional().default(false),
|
||||
}).strict();
|
||||
|
||||
export type IssueCommentPresentation = z.infer<typeof issueCommentPresentationSchema>;
|
||||
|
||||
const issueCommentMetadataBaseRowSchema = z.object({
|
||||
type: z.enum(ISSUE_COMMENT_METADATA_ROW_TYPES),
|
||||
label: commentMetadataLabelSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
const issueCommentMetadataTextRowSchema = issueCommentMetadataBaseRowSchema.extend({
|
||||
type: z.literal("text"),
|
||||
text: commentMetadataTextSchema,
|
||||
}).strict();
|
||||
|
||||
const issueCommentMetadataCodeRowSchema = issueCommentMetadataBaseRowSchema.extend({
|
||||
type: z.literal("code"),
|
||||
code: z.string().min(1).max(4000),
|
||||
language: z.string().trim().min(1).max(40).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const issueCommentMetadataKeyValueRowSchema = issueCommentMetadataBaseRowSchema.extend({
|
||||
type: z.literal("key_value"),
|
||||
label: commentMetadataLabelSchema,
|
||||
value: commentMetadataTextSchema,
|
||||
}).strict();
|
||||
|
||||
const issueCommentMetadataIssueLinkRowSchema = issueCommentMetadataBaseRowSchema.extend({
|
||||
type: z.literal("issue_link"),
|
||||
issueId: z.string().uuid().nullable().optional(),
|
||||
identifier: z.string().trim().min(1).max(80).nullable().optional(),
|
||||
title: z.string().trim().min(1).max(240).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const issueCommentMetadataAgentLinkRowSchema = issueCommentMetadataBaseRowSchema.extend({
|
||||
type: z.literal("agent_link"),
|
||||
agentId: z.string().uuid(),
|
||||
name: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const issueCommentMetadataRunLinkRowSchema = issueCommentMetadataBaseRowSchema.extend({
|
||||
type: z.literal("run_link"),
|
||||
runId: z.string().uuid(),
|
||||
title: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
export const issueCommentMetadataRowSchema = z.discriminatedUnion("type", [
|
||||
issueCommentMetadataTextRowSchema,
|
||||
issueCommentMetadataCodeRowSchema,
|
||||
issueCommentMetadataKeyValueRowSchema,
|
||||
issueCommentMetadataIssueLinkRowSchema,
|
||||
issueCommentMetadataAgentLinkRowSchema,
|
||||
issueCommentMetadataRunLinkRowSchema,
|
||||
]).superRefine((value, ctx) => {
|
||||
if (value.type === "issue_link" && !value.issueId && !value.identifier) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Issue link rows require issueId or identifier",
|
||||
path: ["issueId"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const issueCommentMetadataSectionSchema = z.object({
|
||||
title: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
rows: z.array(issueCommentMetadataRowSchema).min(1).max(50),
|
||||
}).strict();
|
||||
|
||||
export const issueCommentMetadataSchema = z.object({
|
||||
version: z.literal(1),
|
||||
sourceRunId: z.string().uuid().nullable().optional(),
|
||||
sections: z.array(issueCommentMetadataSectionSchema).min(1).max(20),
|
||||
}).strict();
|
||||
|
||||
export type IssueCommentMetadata = z.infer<typeof issueCommentMetadataSchema>;
|
||||
|
||||
export const addIssueCommentSchema = z.object({
|
||||
body: multilineTextSchema.pipe(z.string().min(1)),
|
||||
authorType: issueCommentAuthorTypeSchema.optional(),
|
||||
presentation: issueCommentPresentationSchema.nullable().optional(),
|
||||
metadata: issueCommentMetadataSchema.nullable().optional(),
|
||||
reopen: z.boolean().optional(),
|
||||
resume: z.boolean().optional(),
|
||||
interrupt: z.boolean().optional(),
|
||||
@@ -226,6 +403,7 @@ export const suggestedTaskDraftSchema = z.object({
|
||||
title: z.string().trim().min(1).max(240),
|
||||
description: multilineTextSchema.pipe(z.string().trim().max(20000)).nullable().optional(),
|
||||
priority: z.enum(ISSUE_PRIORITIES).nullable().optional(),
|
||||
workMode: z.enum(ISSUE_WORK_MODES).nullable().optional(),
|
||||
assigneeAgentId: z.string().uuid().nullable().optional(),
|
||||
assigneeUserId: z.string().trim().min(1).nullable().optional(),
|
||||
projectId: z.string().uuid().nullable().optional(),
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PLUGIN_CAPABILITIES } from "../constants.js";
|
||||
import { pluginManagedRoutineDeclarationSchema, pluginManifestV1Schema, pluginUiSlotDeclarationSchema } from "./plugin.js";
|
||||
|
||||
describe("plugin capability constants", () => {
|
||||
it("exposes each capability once", () => {
|
||||
expect(new Set(PLUGIN_CAPABILITIES).size).toBe(PLUGIN_CAPABILITIES.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin managed routine validators", () => {
|
||||
it("accepts core issue surface visibility values in routine templates", () => {
|
||||
const parsed = pluginManagedRoutineDeclarationSchema.parse({
|
||||
routineKey: "wiki.refresh",
|
||||
title: "Refresh Wiki",
|
||||
issueTemplate: { surfaceVisibility: "default" },
|
||||
});
|
||||
|
||||
expect(parsed.issueTemplate?.surfaceVisibility).toBe("default");
|
||||
});
|
||||
|
||||
it("rejects non-core issue surface visibility values in routine templates", () => {
|
||||
const parsed = pluginManagedRoutineDeclarationSchema.safeParse({
|
||||
routineKey: "wiki.refresh",
|
||||
title: "Refresh Wiki",
|
||||
issueTemplate: { surfaceVisibility: "normal" },
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin managed skill validators", () => {
|
||||
const baseManifest = {
|
||||
id: "paperclip.test-managed-skills",
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Managed Skills",
|
||||
description: "Managed skills test plugin.",
|
||||
author: "Paperclip",
|
||||
categories: ["automation"],
|
||||
entrypoints: { worker: "./dist/worker.js" },
|
||||
} as const;
|
||||
|
||||
it("requires skills.managed when managed skills are declared", () => {
|
||||
const parsed = pluginManifestV1Schema.safeParse({
|
||||
...baseManifest,
|
||||
capabilities: [],
|
||||
skills: [{ skillKey: "wiki-maintainer", displayName: "Wiki Maintainer" }],
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) return;
|
||||
expect(parsed.error.issues.some((issue) => issue.message.includes("skills.managed"))).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts managed skills with the skills.managed capability", () => {
|
||||
const parsed = pluginManifestV1Schema.parse({
|
||||
...baseManifest,
|
||||
capabilities: ["skills.managed"],
|
||||
skills: [{ skillKey: "wiki-maintainer", displayName: "Wiki Maintainer" }],
|
||||
});
|
||||
|
||||
expect(parsed.skills?.[0]?.skillKey).toBe("wiki-maintainer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin UI slot validators", () => {
|
||||
it("accepts route-scoped sidebar slots with a routePath", () => {
|
||||
const parsed = pluginUiSlotDeclarationSchema.parse({
|
||||
type: "routeSidebar",
|
||||
id: "wiki-route-sidebar",
|
||||
displayName: "Wiki Sidebar",
|
||||
exportName: "WikiSidebar",
|
||||
routePath: "wiki",
|
||||
});
|
||||
|
||||
expect(parsed.routePath).toBe("wiki");
|
||||
});
|
||||
|
||||
it("requires route-scoped sidebar slots to declare a routePath", () => {
|
||||
const parsed = pluginUiSlotDeclarationSchema.safeParse({
|
||||
type: "routeSidebar",
|
||||
id: "wiki-route-sidebar",
|
||||
displayName: "Wiki Sidebar",
|
||||
exportName: "WikiSidebar",
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) return;
|
||||
expect(parsed.error.issues[0]?.message).toBe("routeSidebar slots require routePath");
|
||||
});
|
||||
|
||||
it("keeps reserved company route protection for route-scoped sidebars", () => {
|
||||
const parsed = pluginUiSlotDeclarationSchema.safeParse({
|
||||
type: "routeSidebar",
|
||||
id: "settings-route-sidebar",
|
||||
displayName: "Settings Sidebar",
|
||||
exportName: "SettingsSidebar",
|
||||
routePath: "settings",
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) return;
|
||||
expect(parsed.error.issues.some((issue) => issue.message.includes("reserved by the host"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,15 @@ import {
|
||||
PLUGIN_API_ROUTE_AUTH_MODES,
|
||||
PLUGIN_API_ROUTE_CHECKOUT_POLICIES,
|
||||
PLUGIN_API_ROUTE_METHODS,
|
||||
ISSUE_PRIORITIES,
|
||||
ROUTINE_CATCH_UP_POLICIES,
|
||||
ROUTINE_CONCURRENCY_POLICIES,
|
||||
ROUTINE_STATUSES,
|
||||
ROUTINE_TRIGGER_KINDS,
|
||||
ROUTINE_TRIGGER_SIGNING_MODES,
|
||||
ISSUE_SURFACE_VISIBILITIES,
|
||||
} from "../constants.js";
|
||||
import { routineVariableSchema } from "./routine.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON Schema placeholder – a permissive validator for JSON Schema objects
|
||||
@@ -124,6 +132,142 @@ export type PluginEnvironmentDriverDeclarationInput = z.infer<
|
||||
|
||||
export type PluginToolDeclarationInput = z.infer<typeof pluginToolDeclarationSchema>;
|
||||
|
||||
export const pluginManagedAgentDeclarationSchema = z.object({
|
||||
agentKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "agentKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
displayName: z.string().min(1).max(100),
|
||||
role: z.string().min(1).max(100).optional(),
|
||||
title: z.string().max(200).nullable().optional(),
|
||||
icon: z.string().max(100).nullable().optional(),
|
||||
capabilities: z.string().max(2000).nullable().optional(),
|
||||
adapterType: z.string().min(1).max(100).optional(),
|
||||
adapterPreference: z.array(z.string().min(1).max(100)).max(10).optional(),
|
||||
adapterConfig: z.record(z.unknown()).optional(),
|
||||
runtimeConfig: z.record(z.unknown()).optional(),
|
||||
permissions: z.record(z.unknown()).optional(),
|
||||
status: z.enum(["idle", "paused"]).optional(),
|
||||
budgetMonthlyCents: z.number().int().min(0).optional(),
|
||||
instructions: z.object({
|
||||
entryFile: z.string().min(1).max(200).optional(),
|
||||
content: z.string().max(200_000).optional(),
|
||||
files: z.record(z.string().max(200_000)).optional(),
|
||||
assetPath: z.string().min(1).max(500).optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export type PluginManagedAgentDeclarationInput = z.infer<typeof pluginManagedAgentDeclarationSchema>;
|
||||
|
||||
export const pluginManagedProjectDeclarationSchema = z.object({
|
||||
projectKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "projectKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
displayName: z.string().min(1).max(120),
|
||||
description: z.string().max(2000).nullable().optional(),
|
||||
status: z.enum(["backlog", "planned", "in_progress", "completed", "cancelled"]).optional(),
|
||||
color: z.string().max(32).nullable().optional(),
|
||||
settings: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export type PluginManagedProjectDeclarationInput = z.infer<typeof pluginManagedProjectDeclarationSchema>;
|
||||
|
||||
const pluginManagedResourceRefSchema = z.object({
|
||||
pluginKey: z.string().min(1).max(100).optional(),
|
||||
resourceKind: z.enum(["agent", "project", "routine", "skill"]),
|
||||
resourceKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "resourceKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
});
|
||||
|
||||
export const pluginManagedRoutineDeclarationSchema = z.object({
|
||||
routineKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "routineKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
title: z.string().trim().min(1).max(200),
|
||||
description: z.string().max(10_000).nullable().optional(),
|
||||
assigneeRef: pluginManagedResourceRefSchema.extend({ resourceKind: z.literal("agent") }).nullable().optional(),
|
||||
projectRef: pluginManagedResourceRefSchema.extend({ resourceKind: z.literal("project") }).nullable().optional(),
|
||||
goalId: z.string().uuid().nullable().optional(),
|
||||
status: z.enum(ROUTINE_STATUSES).optional(),
|
||||
priority: z.enum(ISSUE_PRIORITIES).optional(),
|
||||
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES).optional(),
|
||||
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES).optional(),
|
||||
variables: z.array(routineVariableSchema).optional(),
|
||||
triggers: z.array(z.object({
|
||||
kind: z.enum(ROUTINE_TRIGGER_KINDS),
|
||||
label: z.string().trim().max(120).nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
cronExpression: z.string().trim().min(1).optional().nullable(),
|
||||
timezone: z.string().trim().min(1).optional().nullable(),
|
||||
signingMode: z.enum(ROUTINE_TRIGGER_SIGNING_MODES).optional().nullable(),
|
||||
replayWindowSec: z.number().int().min(30).max(86_400).optional().nullable(),
|
||||
})).max(20).optional(),
|
||||
issueTemplate: z.object({
|
||||
surfaceVisibility: z.enum(ISSUE_SURFACE_VISIBILITIES).optional(),
|
||||
originId: z.string().trim().max(255).nullable().optional(),
|
||||
billingCode: z.string().trim().max(200).nullable().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export type PluginManagedRoutineDeclarationInput = z.infer<typeof pluginManagedRoutineDeclarationSchema>;
|
||||
|
||||
const pluginLocalFolderRelativePathSchema = z.string().min(1).max(500).refine(
|
||||
(value) =>
|
||||
!value.startsWith("/") &&
|
||||
!value.includes("..") &&
|
||||
!value.includes("\\") &&
|
||||
!value.split("/").some((segment) => segment === "" || segment === "."),
|
||||
{ message: "local folder paths must be relative paths without traversal, empty segments, or backslashes" },
|
||||
);
|
||||
|
||||
export const pluginLocalFolderDeclarationSchema = z.object({
|
||||
folderKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "folderKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
displayName: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
access: z.enum(["read", "readWrite"]).optional(),
|
||||
requiredDirectories: z.array(pluginLocalFolderRelativePathSchema).optional(),
|
||||
requiredFiles: z.array(pluginLocalFolderRelativePathSchema).optional(),
|
||||
});
|
||||
|
||||
export type PluginLocalFolderDeclarationInput = z.infer<typeof pluginLocalFolderDeclarationSchema>;
|
||||
|
||||
export const pluginManagedSkillFileDeclarationSchema = z.object({
|
||||
path: pluginLocalFolderRelativePathSchema.refine(
|
||||
(value) => value.toLowerCase() !== "skill.md",
|
||||
{ message: "managed skill files cannot replace SKILL.md; use markdown for the main skill file" },
|
||||
),
|
||||
content: z.string().max(200_000),
|
||||
});
|
||||
|
||||
export type PluginManagedSkillFileDeclarationInput = z.infer<typeof pluginManagedSkillFileDeclarationSchema>;
|
||||
|
||||
export const pluginManagedSkillDeclarationSchema = z.object({
|
||||
skillKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "skillKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
displayName: z.string().min(1).max(100),
|
||||
slug: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "slug must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}).optional(),
|
||||
description: z.string().max(2000).nullable().optional(),
|
||||
markdown: z.string().max(200_000).optional(),
|
||||
files: z.array(pluginManagedSkillFileDeclarationSchema).max(50).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const paths = (value.files ?? []).map((file) => file.path);
|
||||
const duplicates = paths.filter((path, index) => paths.indexOf(path) !== index);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed skill file paths: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["files"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type PluginManagedSkillDeclarationInput = z.infer<typeof pluginManagedSkillDeclarationSchema>;
|
||||
|
||||
/**
|
||||
* Validates a {@link PluginUiSlotDeclaration} — a UI extension slot the plugin
|
||||
* fills with a React component. Includes `superRefine` checks for slot-specific
|
||||
@@ -178,10 +322,17 @@ export const pluginUiSlotDeclarationSchema = z.object({
|
||||
path: ["entityTypes"],
|
||||
});
|
||||
}
|
||||
if (value.routePath && value.type !== "page") {
|
||||
if (value.routePath && value.type !== "page" && value.type !== "routeSidebar") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "routePath is only supported for page slots",
|
||||
message: "routePath is only supported for page and routeSidebar slots",
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
if (value.type === "routeSidebar" && !value.routePath) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "routeSidebar slots require routePath",
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
@@ -471,6 +622,11 @@ export const pluginManifestV1Schema = z.object({
|
||||
database: pluginDatabaseDeclarationSchema.optional(),
|
||||
apiRoutes: z.array(pluginApiRouteDeclarationSchema).optional(),
|
||||
environmentDrivers: z.array(pluginEnvironmentDriverDeclarationSchema).optional(),
|
||||
agents: z.array(pluginManagedAgentDeclarationSchema).optional(),
|
||||
projects: z.array(pluginManagedProjectDeclarationSchema).optional(),
|
||||
routines: z.array(pluginManagedRoutineDeclarationSchema).optional(),
|
||||
skills: z.array(pluginManagedSkillDeclarationSchema).optional(),
|
||||
localFolders: z.array(pluginLocalFolderDeclarationSchema).optional(),
|
||||
launchers: z.array(pluginLauncherDeclarationSchema).optional(),
|
||||
ui: z.object({
|
||||
slots: z.array(pluginUiSlotDeclarationSchema).min(1).optional(),
|
||||
@@ -529,6 +685,56 @@ export const pluginManifestV1Schema = z.object({
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.agents && manifest.agents.length > 0) {
|
||||
if (!manifest.capabilities.includes("agents.managed")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'agents.managed' is required when managed agents are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.projects && manifest.projects.length > 0) {
|
||||
if (!manifest.capabilities.includes("projects.managed")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'projects.managed' is required when managed projects are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.routines && manifest.routines.length > 0) {
|
||||
if (!manifest.capabilities.includes("routines.managed")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'routines.managed' is required when managed routines are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.skills && manifest.skills.length > 0) {
|
||||
if (!manifest.capabilities.includes("skills.managed")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'skills.managed' is required when managed skills are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.localFolders && manifest.localFolders.length > 0) {
|
||||
if (!manifest.capabilities.includes("local.folders")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'local.folders' is required when local folders are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// jobs require jobs.schedule (PLUGIN_SPEC.md §17)
|
||||
if (manifest.jobs && manifest.jobs.length > 0) {
|
||||
if (!manifest.capabilities.includes("jobs.schedule")) {
|
||||
@@ -664,6 +870,66 @@ export const pluginManifestV1Schema = z.object({
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.localFolders) {
|
||||
const folderKeys = manifest.localFolders.map((folder) => folder.folderKey);
|
||||
const duplicates = folderKeys.filter((key, i) => folderKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate local folder keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["localFolders"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.agents) {
|
||||
const agentKeys = manifest.agents.map((agent) => agent.agentKey);
|
||||
const duplicates = agentKeys.filter((key, i) => agentKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed agent keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["agents"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.projects) {
|
||||
const projectKeys = manifest.projects.map((project) => project.projectKey);
|
||||
const duplicates = projectKeys.filter((key, i) => projectKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed project keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["projects"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.routines) {
|
||||
const routineKeys = manifest.routines.map((routine) => routine.routineKey);
|
||||
const duplicates = routineKeys.filter((key, i) => routineKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed routine keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["routines"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.skills) {
|
||||
const skillKeys = manifest.skills.map((skill) => skill.skillKey);
|
||||
const duplicates = skillKeys.filter((key, i) => skillKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed skill keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["skills"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// UI slot ids must be unique within the plugin (namespaced at runtime)
|
||||
if (manifest.ui) {
|
||||
if (manifest.ui.slots) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
routineRevisionSnapshotV1Schema,
|
||||
updateRoutineSchema,
|
||||
} from "./routine.js";
|
||||
|
||||
const routineId = "11111111-1111-4111-8111-111111111111";
|
||||
const companyId = "22222222-2222-4222-8222-222222222222";
|
||||
const triggerId = "33333333-3333-4333-8333-333333333333";
|
||||
const baseRevisionId = "44444444-4444-4444-8444-444444444444";
|
||||
|
||||
describe("routine validators", () => {
|
||||
it("accepts versioned routine revision snapshots with safe trigger metadata", () => {
|
||||
const parsed = routineRevisionSnapshotV1Schema.parse({
|
||||
version: 1,
|
||||
routine: {
|
||||
id: routineId,
|
||||
companyId,
|
||||
projectId: null,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "Daily triage",
|
||||
description: null,
|
||||
assigneeAgentId: null,
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [],
|
||||
},
|
||||
triggers: [{
|
||||
id: triggerId,
|
||||
kind: "webhook",
|
||||
label: "Inbound",
|
||||
enabled: true,
|
||||
cronExpression: null,
|
||||
timezone: null,
|
||||
publicId: "routine_webhook_123",
|
||||
signingMode: "bearer",
|
||||
replayWindowSec: 300,
|
||||
}],
|
||||
});
|
||||
|
||||
expect(parsed.triggers[0]?.publicId).toBe("routine_webhook_123");
|
||||
});
|
||||
|
||||
it("rejects secret-bearing trigger fields in routine revision snapshots", () => {
|
||||
expect(() => routineRevisionSnapshotV1Schema.parse({
|
||||
version: 1,
|
||||
routine: {
|
||||
id: routineId,
|
||||
companyId,
|
||||
projectId: null,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "Daily triage",
|
||||
description: null,
|
||||
assigneeAgentId: null,
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [],
|
||||
},
|
||||
triggers: [{
|
||||
id: triggerId,
|
||||
kind: "webhook",
|
||||
label: "Inbound",
|
||||
enabled: true,
|
||||
cronExpression: null,
|
||||
timezone: null,
|
||||
publicId: "routine_webhook_123",
|
||||
signingMode: "bearer",
|
||||
replayWindowSec: 300,
|
||||
secretId: "55555555-5555-4555-8555-555555555555",
|
||||
}],
|
||||
})).toThrow();
|
||||
});
|
||||
|
||||
it("accepts optional base revision ids on routine updates", () => {
|
||||
expect(updateRoutineSchema.parse({
|
||||
title: "Daily triage",
|
||||
baseRevisionId,
|
||||
}).baseRevisionId).toBe(baseRevisionId);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ROUTINE_CATCH_UP_POLICIES,
|
||||
ROUTINE_CONCURRENCY_POLICIES,
|
||||
ROUTINE_STATUSES,
|
||||
ROUTINE_TRIGGER_KINDS,
|
||||
ROUTINE_TRIGGER_SIGNING_MODES,
|
||||
ROUTINE_VARIABLE_TYPES,
|
||||
} from "../constants.js";
|
||||
@@ -63,9 +64,49 @@ export const createRoutineSchema = z.object({
|
||||
|
||||
export type CreateRoutine = z.infer<typeof createRoutineSchema>;
|
||||
|
||||
export const updateRoutineSchema = createRoutineSchema.partial();
|
||||
export const updateRoutineSchema = createRoutineSchema.partial().extend({
|
||||
baseRevisionId: z.string().uuid().optional().nullable(),
|
||||
});
|
||||
export type UpdateRoutine = z.infer<typeof updateRoutineSchema>;
|
||||
|
||||
export const routineRevisionSnapshotRoutineV1Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
companyId: z.string().uuid(),
|
||||
projectId: z.string().uuid().nullable(),
|
||||
goalId: z.string().uuid().nullable(),
|
||||
parentIssueId: z.string().uuid().nullable(),
|
||||
title: z.string().trim().min(1).max(200),
|
||||
description: z.string().nullable(),
|
||||
assigneeAgentId: z.string().uuid().nullable(),
|
||||
priority: z.enum(ISSUE_PRIORITIES),
|
||||
status: z.enum(ROUTINE_STATUSES),
|
||||
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES),
|
||||
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES),
|
||||
variables: z.array(routineVariableSchema),
|
||||
}).strict();
|
||||
|
||||
export const routineRevisionSnapshotTriggerV1Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
kind: z.enum(ROUTINE_TRIGGER_KINDS),
|
||||
label: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
cronExpression: z.string().nullable(),
|
||||
timezone: z.string().nullable(),
|
||||
publicId: z.string().nullable(),
|
||||
signingMode: z.enum(ROUTINE_TRIGGER_SIGNING_MODES).nullable(),
|
||||
replayWindowSec: z.number().int().min(30).max(86_400).nullable(),
|
||||
}).strict();
|
||||
|
||||
export const routineRevisionSnapshotV1Schema = z.object({
|
||||
version: z.literal(1),
|
||||
routine: routineRevisionSnapshotRoutineV1Schema,
|
||||
triggers: z.array(routineRevisionSnapshotTriggerV1Schema),
|
||||
}).strict();
|
||||
|
||||
export const routineRevisionSnapshotSchema = routineRevisionSnapshotV1Schema;
|
||||
export type RoutineRevisionSnapshotV1 = z.infer<typeof routineRevisionSnapshotV1Schema>;
|
||||
export type RoutineRevisionSnapshot = z.infer<typeof routineRevisionSnapshotSchema>;
|
||||
|
||||
const baseTriggerSchema = z.object({
|
||||
label: z.string().trim().max(120).optional().nullable(),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import { COMPANY_SEARCH_SCOPES } from "../types/search.js";
|
||||
|
||||
export const COMPANY_SEARCH_MAX_QUERY_LENGTH = 200;
|
||||
export const COMPANY_SEARCH_MAX_TOKENS = 8;
|
||||
export const COMPANY_SEARCH_DEFAULT_LIMIT = 20;
|
||||
export const COMPANY_SEARCH_MAX_LIMIT = 50;
|
||||
export const COMPANY_SEARCH_MAX_OFFSET = 200;
|
||||
|
||||
function firstQueryValue(value: unknown): unknown {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
function clampInteger(value: unknown, fallback: number, min: number, max: number) {
|
||||
const raw = firstQueryValue(value);
|
||||
const numeric = typeof raw === "number"
|
||||
? raw
|
||||
: typeof raw === "string" && raw.trim().length > 0
|
||||
? Number.parseInt(raw, 10)
|
||||
: Number.NaN;
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
export const companySearchQuerySchema = z.object({
|
||||
q: z.preprocess(firstQueryValue, z.string().optional().default(""))
|
||||
.transform((value) => value.slice(0, COMPANY_SEARCH_MAX_QUERY_LENGTH)),
|
||||
scope: z.preprocess(firstQueryValue, z.enum(COMPANY_SEARCH_SCOPES).catch("all")).optional().default("all"),
|
||||
limit: z.unknown()
|
||||
.optional()
|
||||
.transform((value) => clampInteger(value, COMPANY_SEARCH_DEFAULT_LIMIT, 1, COMPANY_SEARCH_MAX_LIMIT)),
|
||||
offset: z.unknown()
|
||||
.optional()
|
||||
.transform((value) => clampInteger(value, 0, 0, COMPANY_SEARCH_MAX_OFFSET)),
|
||||
});
|
||||
|
||||
export type CompanySearchQuery = z.infer<typeof companySearchQuerySchema>;
|
||||
@@ -0,0 +1,157 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createSecretProviderConfigSchema,
|
||||
createSecretSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
secretProviderConfigPayloadSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
} from "./secret.js";
|
||||
|
||||
describe("secret validators", () => {
|
||||
it("rejects externalRef on managed secrets", () => {
|
||||
expect(() =>
|
||||
createSecretSchema.parse({
|
||||
name: "OpenAI API Key",
|
||||
managedMode: "paperclip_managed",
|
||||
value: "secret-value",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/other",
|
||||
}),
|
||||
).toThrow(/Managed secrets cannot set externalRef/);
|
||||
});
|
||||
|
||||
it("allows externalRef on external reference secrets", () => {
|
||||
const parsed = createSecretSchema.parse({
|
||||
name: "Shared Secret",
|
||||
managedMode: "external_reference",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/other",
|
||||
});
|
||||
|
||||
expect(parsed.externalRef).toContain(":secret:shared/other");
|
||||
});
|
||||
|
||||
it("accepts non-sensitive local and AWS provider vault metadata", () => {
|
||||
expect(() =>
|
||||
createSecretProviderConfigSchema.parse({
|
||||
provider: "local_encrypted",
|
||||
displayName: "Local",
|
||||
config: { backupReminderAcknowledged: true },
|
||||
}),
|
||||
).not.toThrow();
|
||||
|
||||
expect(() =>
|
||||
createSecretProviderConfigSchema.parse({
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS",
|
||||
config: {
|
||||
region: "us-east-1",
|
||||
namespace: "production",
|
||||
secretNamePrefix: "paperclip",
|
||||
},
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts origin-only Vault provider vault addresses", () => {
|
||||
expect(() =>
|
||||
createSecretProviderConfigSchema.parse({
|
||||
provider: "vault",
|
||||
displayName: "Vault draft",
|
||||
config: { address: " https://vault.example.com/ " },
|
||||
}),
|
||||
).not.toThrow();
|
||||
|
||||
const parsed = secretProviderConfigPayloadSchema.parse({
|
||||
provider: "vault",
|
||||
config: { address: " https://vault.example.com/ " },
|
||||
});
|
||||
|
||||
expect(parsed.provider).toBe("vault");
|
||||
if (parsed.provider !== "vault") throw new Error("Expected vault provider payload");
|
||||
expect(parsed.config.address).toBe("https://vault.example.com");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"https://user:pass@vault.example.com",
|
||||
"https://vault.example.com?token=hvs.x",
|
||||
"https://vault.example.com#token=hvs.x",
|
||||
"https://vault.example.com/v1/secret",
|
||||
])("rejects credential-bearing or non-origin Vault addresses: %s", (address) => {
|
||||
expect(() =>
|
||||
createSecretProviderConfigSchema.parse({
|
||||
provider: "vault",
|
||||
displayName: "Vault draft",
|
||||
config: { address },
|
||||
}),
|
||||
).toThrow(/origin-only HTTP\(S\) URL/i);
|
||||
});
|
||||
|
||||
it("rejects unsafe Vault addresses in provider payload validation used by updates", () => {
|
||||
expect(() =>
|
||||
secretProviderConfigPayloadSchema.parse({
|
||||
provider: "vault",
|
||||
config: { address: "https://vault.example.com?client_token=hvs.x" },
|
||||
}),
|
||||
).toThrow(/origin-only HTTP\(S\) URL/i);
|
||||
});
|
||||
|
||||
it("rejects unsafe Vault addresses in provider vault update payloads", () => {
|
||||
expect(() =>
|
||||
updateSecretProviderConfigSchema.parse({
|
||||
config: { address: "https://vault.example.com#token=hvs.x" },
|
||||
}),
|
||||
).toThrow(/origin-only HTTP\(S\) URL/i);
|
||||
});
|
||||
|
||||
it("validates AWS remote import preview and import payloads", () => {
|
||||
expect(
|
||||
remoteSecretImportPreviewSchema.parse({
|
||||
providerConfigId: "11111111-1111-4111-8111-111111111111",
|
||||
query: "openai",
|
||||
pageSize: 50,
|
||||
}),
|
||||
).toEqual({
|
||||
providerConfigId: "11111111-1111-4111-8111-111111111111",
|
||||
query: "openai",
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
expect(
|
||||
remoteSecretImportSchema.parse({
|
||||
providerConfigId: "11111111-1111-4111-8111-111111111111",
|
||||
secrets: [
|
||||
{
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
|
||||
name: "OpenAI API key",
|
||||
key: "OPENAI_API_KEY",
|
||||
description: " Operator-entered Paperclip description ",
|
||||
providerMetadata: { name: "prod/openai" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toMatchObject({
|
||||
providerConfigId: "11111111-1111-4111-8111-111111111111",
|
||||
secrets: [
|
||||
expect.objectContaining({
|
||||
key: "OPENAI_API_KEY",
|
||||
description: "Operator-entered Paperclip description",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("caps AWS remote import paging and row counts", () => {
|
||||
expect(() =>
|
||||
remoteSecretImportPreviewSchema.parse({
|
||||
providerConfigId: "11111111-1111-4111-8111-111111111111",
|
||||
pageSize: 101,
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
remoteSecretImportSchema.parse({
|
||||
providerConfigId: "11111111-1111-4111-8111-111111111111",
|
||||
secrets: [],
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,11 @@
|
||||
import { z } from "zod";
|
||||
import { SECRET_PROVIDERS } from "../constants.js";
|
||||
import {
|
||||
SECRET_BINDING_TARGET_TYPES,
|
||||
SECRET_MANAGED_MODES,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_STATUSES,
|
||||
} from "../constants.js";
|
||||
|
||||
export const envBindingPlainSchema = z.object({
|
||||
type: z.literal("plain"),
|
||||
@@ -23,25 +29,252 @@ export const envConfigSchema = z.record(envBindingSchema);
|
||||
|
||||
export const createSecretSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
provider: z.enum(SECRET_PROVIDERS).optional(),
|
||||
value: z.string().min(1),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
||||
value: z.string().min(1).optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerMetadata: z.record(z.unknown()).optional().nullable(),
|
||||
providerVersionRef: z.string().optional().nullable(),
|
||||
}).superRefine((value, ctx) => {
|
||||
if ((value.managedMode ?? "paperclip_managed") === "external_reference") {
|
||||
if (!value.externalRef?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["externalRef"],
|
||||
message: "External reference secrets require externalRef",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value.externalRef?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["externalRef"],
|
||||
message: "Managed secrets cannot set externalRef",
|
||||
});
|
||||
}
|
||||
if (!value.value?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["value"],
|
||||
message: "Managed secrets require value",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateSecret = z.infer<typeof createSecretSchema>;
|
||||
|
||||
export const rotateSecretSchema = z.object({
|
||||
value: z.string().min(1),
|
||||
value: z.string().min(1).optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerVersionRef: z.string().optional().nullable(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
});
|
||||
|
||||
export type RotateSecret = z.infer<typeof rotateSecretSchema>;
|
||||
|
||||
export const updateSecretSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
status: z.enum(SECRET_STATUSES).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerMetadata: z.record(z.unknown()).optional().nullable(),
|
||||
});
|
||||
|
||||
export type UpdateSecret = z.infer<typeof updateSecretSchema>;
|
||||
|
||||
export const secretBindingTargetSchema = z.object({
|
||||
targetType: z.enum(SECRET_BINDING_TARGET_TYPES),
|
||||
targetId: z.string().min(1),
|
||||
configPath: z.string().min(1),
|
||||
});
|
||||
|
||||
export const createSecretBindingSchema = secretBindingTargetSchema.extend({
|
||||
secretId: z.string().uuid(),
|
||||
versionSelector: z.union([z.literal("latest"), z.number().int().positive()]).default("latest"),
|
||||
required: z.boolean().default(true),
|
||||
label: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type CreateSecretBinding = z.infer<typeof createSecretBindingSchema>;
|
||||
|
||||
const safeShortText = z.string().trim().min(1).max(160);
|
||||
const optionalSafeShortText = safeShortText.optional().nullable();
|
||||
|
||||
const deniedProviderConfigKeyPattern =
|
||||
/^(access[-_]?key([-_]?id)?|secret[-_]?access[-_]?key|secret[-_]?key|token|password|passwd|credential|credentials|private[-_]?key|pem|jwt|session[-_]?token|service[-_]?account([-_]?json)?|client[-_]?secret|secret[-_]?id|unseal[-_]?key|recovery[-_]?key|key[-_]?file([-_]?path)?|token[-_]?file([-_]?path)?)$/i;
|
||||
|
||||
function rejectSensitiveProviderConfigKeys(value: unknown, ctx: z.RefinementCtx) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!deniedProviderConfigKeyPattern.test(key)) continue;
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["config", key],
|
||||
message: `Provider vault config cannot persist sensitive field: ${key}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const localEncryptedProviderConfigSchema = z.object({
|
||||
backupReminderAcknowledged: z.boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export const awsSecretsManagerProviderConfigSchema = z.object({
|
||||
region: z.string().trim().regex(/^[a-z]{2}(?:-gov)?-[a-z]+-\d+$/, "Invalid AWS region"),
|
||||
namespace: optionalSafeShortText,
|
||||
secretNamePrefix: optionalSafeShortText,
|
||||
kmsKeyId: z.string().trim().min(1).max(512).optional().nullable(),
|
||||
ownerTag: optionalSafeShortText,
|
||||
environmentTag: optionalSafeShortText,
|
||||
}).strict();
|
||||
|
||||
export const gcpSecretManagerProviderConfigSchema = z.object({
|
||||
projectId: z.string().trim().min(1).max(128).regex(/^[a-z][a-z0-9-]{4,127}$/).optional().nullable(),
|
||||
location: optionalSafeShortText,
|
||||
namespace: optionalSafeShortText,
|
||||
secretNamePrefix: optionalSafeShortText,
|
||||
}).strict();
|
||||
|
||||
const vaultAddressSchema = z.preprocess(
|
||||
(value) => typeof value === "string" ? value.trim() : value,
|
||||
z.string().url().superRefine((value, ctx) => {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const hasPath = url.pathname !== "" && url.pathname !== "/";
|
||||
if (
|
||||
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.search ||
|
||||
url.hash ||
|
||||
hasPath
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Vault address must be an origin-only HTTP(S) URL without credentials, path, query, or fragment",
|
||||
});
|
||||
}
|
||||
}).transform((value) => new URL(value).origin),
|
||||
);
|
||||
|
||||
function rejectUnsafeVaultAddress(value: unknown, ctx: z.RefinementCtx) {
|
||||
if (value === undefined || value === null) return;
|
||||
const parsed = vaultAddressSchema.safeParse(value);
|
||||
if (parsed.success) return;
|
||||
for (const issue of parsed.error.issues) {
|
||||
ctx.addIssue({
|
||||
...issue,
|
||||
path: ["config", "address", ...issue.path],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const vaultProviderConfigSchema = z.object({
|
||||
address: vaultAddressSchema.optional().nullable(),
|
||||
namespace: optionalSafeShortText,
|
||||
mountPath: optionalSafeShortText,
|
||||
secretPathPrefix: optionalSafeShortText,
|
||||
}).strict();
|
||||
|
||||
export const secretProviderConfigPayloadSchema = z.discriminatedUnion("provider", [
|
||||
z.object({ provider: z.literal("local_encrypted"), config: localEncryptedProviderConfigSchema }),
|
||||
z.object({ provider: z.literal("aws_secrets_manager"), config: awsSecretsManagerProviderConfigSchema }),
|
||||
z.object({ provider: z.literal("gcp_secret_manager"), config: gcpSecretManagerProviderConfigSchema }),
|
||||
z.object({ provider: z.literal("vault"), config: vaultProviderConfigSchema }),
|
||||
]);
|
||||
|
||||
export const createSecretProviderConfigSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS),
|
||||
displayName: z.string().trim().min(1).max(120),
|
||||
status: z.enum(SECRET_PROVIDER_CONFIG_STATUSES).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
config: z.record(z.unknown()).default({}),
|
||||
}).superRefine((value, ctx) => {
|
||||
rejectSensitiveProviderConfigKeys(value.config, ctx);
|
||||
const parsed = secretProviderConfigPayloadSchema.safeParse({
|
||||
provider: value.provider,
|
||||
config: value.config,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.error.issues) {
|
||||
ctx.addIssue({
|
||||
...issue,
|
||||
path: issue.path[0] === "config" ? issue.path : ["config", ...issue.path],
|
||||
});
|
||||
}
|
||||
}
|
||||
const status = value.status ?? (["gcp_secret_manager", "vault"].includes(value.provider) ? "coming_soon" : "ready");
|
||||
if ((value.provider === "gcp_secret_manager" || value.provider === "vault") && status !== "coming_soon" && status !== "disabled") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["status"],
|
||||
message: `${value.provider} provider vaults are locked while coming soon`,
|
||||
});
|
||||
}
|
||||
if ((status === "coming_soon" || status === "disabled") && value.isDefault) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["isDefault"],
|
||||
message: "Only ready or warning provider vaults can be default",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateSecretProviderConfig = z.infer<typeof createSecretProviderConfigSchema>;
|
||||
|
||||
export const updateSecretProviderConfigSchema = z.object({
|
||||
displayName: z.string().trim().min(1).max(120).optional(),
|
||||
status: z.enum(SECRET_PROVIDER_CONFIG_STATUSES).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
config: z.record(z.unknown()).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
if (value.config !== undefined) {
|
||||
rejectSensitiveProviderConfigKeys(value.config, ctx);
|
||||
rejectUnsafeVaultAddress(value.config.address, ctx);
|
||||
}
|
||||
if ((value.status === "coming_soon" || value.status === "disabled") && value.isDefault) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["isDefault"],
|
||||
message: "Only ready or warning provider vaults can be default",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type UpdateSecretProviderConfig = z.infer<typeof updateSecretProviderConfigSchema>;
|
||||
|
||||
export const remoteSecretImportPreviewSchema = z.object({
|
||||
providerConfigId: z.string().uuid(),
|
||||
query: z.string().trim().max(200).optional().nullable(),
|
||||
nextToken: z.string().trim().min(1).max(4096).optional().nullable(),
|
||||
pageSize: z.number().int().min(1).max(100).optional(),
|
||||
});
|
||||
|
||||
export type RemoteSecretImportPreview = z.infer<typeof remoteSecretImportPreviewSchema>;
|
||||
|
||||
export const remoteSecretImportSelectionSchema = z.object({
|
||||
externalRef: z.string().trim().min(1).max(2048),
|
||||
name: z.string().trim().min(1).max(160).optional().nullable(),
|
||||
key: z.string().trim().min(1).max(120).regex(/^[a-zA-Z0-9_.-]+$/).optional().nullable(),
|
||||
description: z.string().trim().max(500).optional().nullable(),
|
||||
providerVersionRef: z.string().trim().min(1).max(512).optional().nullable(),
|
||||
providerMetadata: z.record(z.unknown()).optional().nullable(),
|
||||
});
|
||||
|
||||
export const remoteSecretImportSchema = z.object({
|
||||
providerConfigId: z.string().uuid(),
|
||||
secrets: z.array(remoteSecretImportSelectionSchema).min(1).max(100),
|
||||
});
|
||||
|
||||
export type RemoteSecretImportSelection = z.infer<typeof remoteSecretImportSelectionSchema>;
|
||||
export type RemoteSecretImport = z.infer<typeof remoteSecretImportSchema>;
|
||||
|
||||
Reference in New Issue
Block a user