Merge upstream/master (53 commits) into local
Build: Production / build (push) Failing after 13m4s

Resolved conflicts:
- ui CompanySettingsSidebar.tsx: keep both Secrets (local) and Cloud upstream (master) nav items
- ui CompanySettingsNav.tsx + test: take master's cloud-upstream/members (drops deprecated `access` tab now consolidated into `members`)
- server plugin-worker-manager.ts: take master's 15min RPC timeout cap
- pnpm-lock.yaml: regenerated via `pnpm install` against merged package.json files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 08:01:31 -04:00
536 changed files with 60296 additions and 2542 deletions
+2
View File
@@ -12,11 +12,13 @@ export const API = {
approvals: `${API_PREFIX}/approvals`,
secrets: `${API_PREFIX}/secrets`,
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
secretProviderConfigDiscoveryPreview: `${API_PREFIX}/companies/:companyId/secret-provider-configs/discovery/preview`,
costs: `${API_PREFIX}/costs`,
activity: `${API_PREFIX}/activity`,
dashboard: `${API_PREFIX}/dashboard`,
sidebarBadges: `${API_PREFIX}/sidebar-badges`,
sidebarPreferences: `${API_PREFIX}/sidebar-preferences`,
resourceMemberships: `${API_PREFIX}/resource-memberships`,
invites: `${API_PREFIX}/invites`,
joinRequests: `${API_PREFIX}/join-requests`,
members: `${API_PREFIX}/members`,
+29
View File
@@ -726,6 +726,7 @@ export const PLUGIN_CAPABILITIES = [
"companies.read",
"projects.read",
"project.workspaces.read",
"execution.workspaces.read",
"issues.read",
"issue.relations.read",
"issue.subtree.read",
@@ -738,6 +739,11 @@ export const PLUGIN_CAPABILITIES = [
"activity.read",
"costs.read",
"issues.orchestration.read",
"access.members.read",
"access.invites.read",
"authorization.grants.read",
"authorization.policies.read",
"authorization.audit.read",
"database.namespace.read",
// Data Write
"issues.create",
@@ -755,6 +761,10 @@ export const PLUGIN_CAPABILITIES = [
"agents.resume",
"agents.invoke",
"agents.managed",
"access.members.write",
"access.invites.write",
"authorization.grants.write",
"authorization.policies.write",
"agent.sessions.create",
"agent.sessions.list",
"agent.sessions.send",
@@ -856,6 +866,7 @@ export const PLUGIN_UI_SLOT_TYPES = [
"commentAnnotation",
"commentContextMenuItem",
"settingsPage",
"companySettingsPage",
] as const;
export type PluginUiSlotType = (typeof PLUGIN_UI_SLOT_TYPES)[number];
@@ -886,6 +897,21 @@ export const PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS = [
export type PluginReservedCompanyRouteSegment =
(typeof PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS)[number];
/**
* Reserved route segments under `/:companyPrefix/company/settings/...` that
* plugin company settings pages may not claim.
*/
export const PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS = [
"general",
"environments",
"access",
"members",
"invites",
"secrets",
] as const;
export type PluginReservedCompanySettingsRouteSegment =
(typeof PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS)[number];
/**
* Launcher placement zones describe where a plugin-owned launcher can appear
* in the host UI. These are intentionally aligned with current slot surfaces
@@ -961,6 +987,8 @@ export const PLUGIN_UI_SLOT_ENTITY_TYPES = [
"goal",
"run",
"comment",
"execution_workspace",
"project_workspace",
] as const;
export type PluginUiSlotEntityType = (typeof PLUGIN_UI_SLOT_ENTITY_TYPES)[number];
@@ -1067,6 +1095,7 @@ export type PluginEventType = (typeof PLUGIN_EVENT_TYPES)[number];
export const PLUGIN_BRIDGE_ERROR_CODES = [
"WORKER_UNAVAILABLE",
"CAPABILITY_DENIED",
"INVOCATION_SCOPE_DENIED",
"WORKER_ERROR",
"TIMEOUT",
"UNKNOWN",
+44
View File
@@ -111,6 +111,7 @@ export {
PLUGIN_CAPABILITIES,
PLUGIN_UI_SLOT_TYPES,
PLUGIN_UI_SLOT_ENTITY_TYPES,
PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS,
PLUGIN_LAUNCHER_PLACEMENT_ZONES,
PLUGIN_LAUNCHER_ACTIONS,
PLUGIN_LAUNCHER_BOUNDS,
@@ -226,6 +227,7 @@ export {
type PluginCapability,
type PluginUiSlotType,
type PluginUiSlotEntityType,
type PluginReservedCompanySettingsRouteSegment,
type PluginLauncherPlacementZone,
type PluginLauncherAction,
type PluginLauncherBounds,
@@ -551,12 +553,18 @@ export type {
CompanyPortabilityExportRequest,
CompanyPortabilitySecretEntry,
EnvBinding,
EnvPlainBinding,
EnvSecretRefBinding,
AgentEnvConfig,
CompanySecret,
CompanySecretProviderConfig,
SecretProviderConfigPayload,
SecretProviderConfigHealthDetails,
SecretProviderConfigHealthResponse,
SecretProviderConfigDiscoveryCandidate,
SecretProviderConfigDiscoveryPreviewResult,
SecretProviderConfigDiscoverySample,
SecretProviderConfigDiscoverySignal,
CompanySecretBinding,
CompanySecretBindingTarget,
CompanySecretUsageBinding,
@@ -577,6 +585,7 @@ export type {
SecretVersionSelector,
SecretVersionStatus,
Routine,
RoutineEnvConfig,
RoutineManagedByPlugin,
RoutineVariable,
RoutineVariableDefaultValue,
@@ -651,6 +660,18 @@ export {
upsertSidebarOrderPreferenceSchema,
type UpsertSidebarOrderPreference,
} from "./validators/sidebar-preferences.js";
export {
resourceMembershipStateSchema,
updateResourceMembershipSchema,
type UpdateResourceMembership,
} from "./validators/resource-memberships.js";
export {
RESOURCE_MEMBERSHIP_STATES,
type ResourceMembershipResourceType,
type ResourceMembershipState,
type ResourceMemberships,
type ResourceMembershipUpdateResult,
} from "./types/resource-memberships.js";
export { workspaceRuntimeControlTargetSchema } from "./validators/execution-workspace.js";
export {
@@ -680,6 +701,22 @@ export {
MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS,
} from "./types/instance.js";
export type {
CloudUpstreamConnectStartResponse,
CloudUpstreamActivationDecision,
CloudUpstreamActivationEntityType,
CloudUpstreamConnection,
CloudUpstreamConflict,
CloudUpstreamPreview,
CloudUpstreamRun,
CloudUpstreamRunEvent,
CloudUpstreamsState,
CloudUpstreamStep,
CloudUpstreamSummaryCount,
CloudUpstreamTarget,
CloudUpstreamWarning,
} from "./types/cloud-upstream.js";
export {
getClosedIsolatedExecutionWorkspaceMessage,
isClosedIsolatedExecutionWorkspace,
@@ -883,6 +920,7 @@ export {
createSecretSchema,
createSecretProviderConfigSchema,
updateSecretProviderConfigSchema,
secretProviderConfigDiscoveryPreviewSchema,
remoteSecretImportPreviewSchema,
remoteSecretImportSchema,
remoteSecretImportSelectionSchema,
@@ -909,6 +947,7 @@ export {
type CreateSecret,
type CreateSecretProviderConfig,
type UpdateSecretProviderConfig,
type SecretProviderConfigDiscoveryPreview,
type RemoteSecretImportPreview,
type RemoteSecretImport,
type RemoteSecretImportSelection,
@@ -1046,22 +1085,27 @@ export { deriveProjectUrlKey, normalizeProjectUrlKey, hasNonAsciiContent } from
export {
AGENT_MENTION_SCHEME,
PROJECT_MENTION_SCHEME,
ROUTINE_MENTION_SCHEME,
SKILL_MENTION_SCHEME,
USER_MENTION_SCHEME,
buildAgentMentionHref,
buildProjectMentionHref,
buildRoutineMentionHref,
buildSkillMentionHref,
buildUserMentionHref,
extractAgentMentionIds,
extractProjectMentionIds,
extractRoutineMentionIds,
extractSkillMentionIds,
extractUserMentionIds,
parseAgentMentionHref,
parseProjectMentionHref,
parseRoutineMentionHref,
parseSkillMentionHref,
parseUserMentionHref,
type ParsedAgentMention,
type ParsedProjectMention,
type ParsedRoutineMention,
type ParsedSkillMention,
type ParsedUserMention,
} from "./project-mentions.js";
@@ -2,14 +2,17 @@ import { describe, expect, it } from "vitest";
import {
buildAgentMentionHref,
buildProjectMentionHref,
buildRoutineMentionHref,
buildSkillMentionHref,
buildUserMentionHref,
extractAgentMentionIds,
extractProjectMentionIds,
extractRoutineMentionIds,
extractSkillMentionIds,
extractUserMentionIds,
parseAgentMentionHref,
parseProjectMentionHref,
parseRoutineMentionHref,
parseSkillMentionHref,
parseUserMentionHref,
} from "./project-mentions.js";
@@ -49,4 +52,12 @@ describe("project-mentions", () => {
});
expect(extractSkillMentionIds(`[/release-changelog](${href})`)).toEqual(["skill-123"]);
});
it("round-trips routine mentions", () => {
const href = buildRoutineMentionHref("routine-123");
expect(parseRoutineMentionHref(href)).toEqual({
routineId: "routine-123",
});
expect(extractRoutineMentionIds(`[/routine:Weekly review](${href})`)).toEqual(["routine-123"]);
});
});
+40
View File
@@ -2,6 +2,7 @@ export const PROJECT_MENTION_SCHEME = "project://";
export const AGENT_MENTION_SCHEME = "agent://";
export const USER_MENTION_SCHEME = "user://";
export const SKILL_MENTION_SCHEME = "skill://";
export const ROUTINE_MENTION_SCHEME = "routine://";
const HEX_COLOR_RE = /^[0-9a-f]{6}$/i;
const HEX_COLOR_SHORT_RE = /^[0-9a-f]{3}$/i;
@@ -11,6 +12,7 @@ const PROJECT_MENTION_LINK_RE = /\[[^\]]*]\((project:\/\/[^)\s]+)\)/gi;
const AGENT_MENTION_LINK_RE = /\[[^\]]*]\((agent:\/\/[^)\s]+)\)/gi;
const USER_MENTION_LINK_RE = /\[[^\]]*]\((user:\/\/[^)\s]+)\)/gi;
const SKILL_MENTION_LINK_RE = /\[[^\]]*]\((skill:\/\/[^)\s]+)\)/gi;
const ROUTINE_MENTION_LINK_RE = /\[[^\]]*]\((routine:\/\/[^)\s]+)\)/gi;
const AGENT_ICON_NAME_RE = /^[a-z0-9-]+$/i;
const SKILL_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/i;
@@ -33,6 +35,10 @@ export interface ParsedSkillMention {
slug: string | null;
}
export interface ParsedRoutineMention {
routineId: string;
}
function normalizeHexColor(input: string | null | undefined): string | null {
if (!input) return null;
const trimmed = input.trim();
@@ -169,6 +175,28 @@ export function parseSkillMentionHref(href: string): ParsedSkillMention | null {
};
}
export function buildRoutineMentionHref(routineId: string): string {
return `${ROUTINE_MENTION_SCHEME}${routineId.trim()}`;
}
export function parseRoutineMentionHref(href: string): ParsedRoutineMention | null {
if (!href.startsWith(ROUTINE_MENTION_SCHEME)) return null;
let url: URL;
try {
url = new URL(href);
} catch {
return null;
}
if (url.protocol !== "routine:") return null;
const routineId = `${url.hostname}${url.pathname}`.replace(/^\/+/, "").trim();
if (!routineId) return null;
return { routineId };
}
export function extractProjectMentionIds(markdown: string): string[] {
if (!markdown) return [];
const ids = new Set<string>();
@@ -217,6 +245,18 @@ export function extractSkillMentionIds(markdown: string): string[] {
return [...ids];
}
export function extractRoutineMentionIds(markdown: string): string[] {
if (!markdown) return [];
const ids = new Set<string>();
const re = new RegExp(ROUTINE_MENTION_LINK_RE);
let match: RegExpExecArray | null;
while ((match = re.exec(markdown)) !== null) {
const parsed = parseRoutineMentionHref(match[1]);
if (parsed) ids.add(parsed.routineId);
}
return [...ids];
}
function normalizeAgentIcon(input: string | null | undefined): string | null {
if (!input) return null;
const trimmed = input.trim().toLowerCase();
+1 -1
View File
@@ -58,7 +58,7 @@ export interface AgentInstructionsBundle {
export interface AgentAccessState {
canAssignTasks: boolean;
taskAssignSource: "explicit_grant" | "agent_creator" | "ceo_role" | "none";
taskAssignSource: "simple_default" | "explicit_grant" | "agent_creator" | "ceo_role" | "none";
membership: CompanyMembership | null;
grants: PrincipalPermissionGrant[];
}
+110
View File
@@ -0,0 +1,110 @@
export type CloudUpstreamStep = "connect" | "scan" | "preview" | "push" | "verify" | "activate";
export type CloudUpstreamRunStatus = "previewed" | "running" | "succeeded" | "failed" | "cancelled";
export type CloudUpstreamActivationEntityType = "agents" | "routines" | "monitors";
export interface CloudUpstreamActivationDecision {
entityType: CloudUpstreamActivationEntityType;
count: number;
status: "paused" | "activated";
activatedAt: string | null;
}
export interface CloudUpstreamTarget {
stackId: string;
stackSlug: string | null;
stackDisplayName: string | null;
companyId: string;
primaryHost: string;
origin: string;
product: string;
schemaMajor: number;
maxChunkBytes: number;
}
export interface CloudUpstreamConnection {
id: string;
companyId: string;
remoteUrl: string;
target: CloudUpstreamTarget;
tokenStatus: "pending" | "connected" | "expired" | "revoked";
scopes: string[];
authorizedGlobalUserId: string | null;
expiresAt: string | null;
createdAt: string;
updatedAt: string;
lastRunId: string | null;
}
export interface CloudUpstreamSummaryCount {
key: string;
label: string;
count: number;
}
export interface CloudUpstreamWarning {
code: string;
severity: "warning" | "blocker";
title: string;
detail: string;
}
export interface CloudUpstreamConflict {
id: string;
entityType: string;
sourceLabel: string;
targetLabel: string;
plannedAction: "create" | "update" | "skip" | "blocked";
reason: string;
}
export interface CloudUpstreamPreview {
connectionId: string;
sourceCompanyId: string;
target: CloudUpstreamTarget;
schemaCompatible: boolean;
summary: CloudUpstreamSummaryCount[];
warnings: CloudUpstreamWarning[];
conflicts: CloudUpstreamConflict[];
generatedAt: string;
}
export interface CloudUpstreamRunEvent {
id: string;
at: string;
phase: CloudUpstreamStep;
type: "created" | "updated" | "skipped" | "conflict" | "retrying" | "failed" | "completed";
message: string;
}
export interface CloudUpstreamRun {
id: string;
connectionId: string;
companyId: string;
status: CloudUpstreamRunStatus;
activeStep: CloudUpstreamStep;
progressPercent: number;
dryRun: boolean;
summary: CloudUpstreamSummaryCount[];
warnings: CloudUpstreamWarning[];
conflicts: CloudUpstreamConflict[];
events: CloudUpstreamRunEvent[];
targetUrl: string | null;
report: Record<string, unknown>;
retryOfRunId: string | null;
createdAt: string;
updatedAt: string;
completedAt: string | null;
}
export interface CloudUpstreamsState {
connections: CloudUpstreamConnection[];
runs: CloudUpstreamRun[];
}
export interface CloudUpstreamConnectStartResponse {
pendingConnectionId: string;
authorizationUrl: string;
connection: CloudUpstreamConnection;
}
+13
View File
@@ -258,6 +258,10 @@ export type {
SecretProviderConfigPayload,
SecretProviderConfigHealthDetails,
SecretProviderConfigHealthResponse,
SecretProviderConfigDiscoveryCandidate,
SecretProviderConfigDiscoveryPreviewResult,
SecretProviderConfigDiscoverySample,
SecretProviderConfigDiscoverySignal,
CompanySecretBinding,
CompanySecretBindingTarget,
CompanySecretUsageBinding,
@@ -279,6 +283,7 @@ export type {
} from "./secrets.js";
export type {
Routine,
RoutineEnvConfig,
RoutineManagedByPlugin,
RoutineVariable,
RoutineVariableDefaultValue,
@@ -322,6 +327,14 @@ export type {
} from "./user-profile.js";
export type { SidebarBadges } from "./sidebar-badges.js";
export type { SidebarOrderPreference } from "./sidebar-preferences.js";
export type {
ResourceMembershipResourceType,
ResourceMembershipState,
ResourceMemberships,
ResourceMembershipUpdateResult,
UpdateResourceMembership,
} from "./resource-memberships.js";
export { RESOURCE_MEMBERSHIP_STATES } from "./resource-memberships.js";
export type { InboxDismissal } from "./inbox-dismissal.js";
export type {
AccessUserProfile,
+1
View File
@@ -29,6 +29,7 @@ export interface InstanceGeneralSettings {
export interface InstanceExperimentalSettings {
enableEnvironments: boolean;
enableIsolatedWorkspaces: boolean;
enableCloudSync: boolean;
autoRestartDevServerWhenIdle: boolean;
enableIssueGraphLivenessAutoRecovery: boolean;
issueGraphLivenessAutoRecoveryLookbackHours: number;
+3
View File
@@ -96,6 +96,9 @@ export interface IssueDocumentSummary {
createdByUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
lockedAt: Date | null;
lockedByAgentId: string | null;
lockedByUserId: string | null;
createdAt: Date;
updatedAt: Date;
}
+4 -1
View File
@@ -346,8 +346,11 @@ export interface PluginUiSlotDeclaration {
*/
entityTypes?: PluginUiSlotEntityType[];
/**
* Optional company-scoped route segment for page and routeSidebar slots.
* Optional company-scoped route segment for page, routeSidebar, and
* companySettingsPage slots.
* Example: `kitchensink` becomes `/:companyPrefix/kitchensink`.
* For companySettingsPage, `permissions` becomes
* `/:companyPrefix/company/settings/permissions`.
*/
routePath?: string;
/**
@@ -0,0 +1,21 @@
export const RESOURCE_MEMBERSHIP_STATES = ["joined", "left"] as const;
export type ResourceMembershipState = (typeof RESOURCE_MEMBERSHIP_STATES)[number];
export type ResourceMembershipResourceType = "project" | "agent";
export interface ResourceMemberships {
projectMemberships: Record<string, ResourceMembershipState>;
agentMemberships: Record<string, ResourceMembershipState>;
updatedAt: Date | null;
}
export interface UpdateResourceMembership {
state: ResourceMembershipState;
}
export interface ResourceMembershipUpdateResult {
resourceType: ResourceMembershipResourceType;
resourceId: string;
state: ResourceMembershipState;
updatedAt: Date;
}
+6
View File
@@ -8,6 +8,7 @@ import type {
RoutineTriggerSigningMode,
RoutineVariableType,
} from "../constants.js";
import type { EnvBinding } from "./secrets.js";
export interface RoutineProjectSummary {
id: string;
@@ -45,6 +46,8 @@ export interface RoutineVariable {
options: string[];
}
export type RoutineEnvConfig = Record<string, EnvBinding>;
export interface Routine {
id: string;
companyId: string;
@@ -59,6 +62,7 @@ export interface Routine {
concurrencyPolicy: string;
catchUpPolicy: string;
variables: RoutineVariable[];
env?: RoutineEnvConfig | null;
latestRevisionId: string | null;
latestRevisionNumber: number;
createdByAgentId: string | null;
@@ -98,6 +102,7 @@ export interface RoutineRevisionSnapshotRoutineV1 {
concurrencyPolicy: RoutineConcurrencyPolicy;
catchUpPolicy: RoutineCatchUpPolicy;
variables: RoutineVariable[];
env: RoutineEnvConfig | null;
}
export interface RoutineRevisionSnapshotTriggerV1 {
@@ -169,6 +174,7 @@ export interface RoutineRun {
source: string;
status: string;
triggeredAt: Date;
routineRevisionId?: string | null;
idempotencyKey: string | null;
triggerPayload: Record<string, unknown> | null;
dispatchFingerprint: string | null;
+37
View File
@@ -138,6 +138,43 @@ export interface SecretProviderConfigHealthResponse {
checkedAt: Date;
}
export interface SecretProviderConfigDiscoverySignal {
namespace: string | null;
secretNamePrefix: string | null;
environmentTag: string | null;
ownerTag: string | null;
kmsKeyId: string | null;
hasKmsKey: boolean;
sampleCount: number;
paperclipManagedSampleCount: number;
skippedForeignPaperclipSampleCount: number;
}
export interface SecretProviderConfigDiscoverySample {
name: string;
hasKmsKey: boolean;
tagKeys: string[];
}
export interface SecretProviderConfigDiscoveryCandidate {
provider: SecretProvider;
displayName: string;
config: SecretProviderConfigPayload;
sampleCount: number;
samples: SecretProviderConfigDiscoverySample[];
signals: SecretProviderConfigDiscoverySignal;
warnings: string[];
}
export interface SecretProviderConfigDiscoveryPreviewResult {
provider: SecretProvider;
nextToken: string | null;
sampledSecretCount: number;
skippedForeignPaperclipSampleCount: number;
candidates: SecretProviderConfigDiscoveryCandidate[];
warnings: string[];
}
export interface CompanySecretVersion {
id: string;
secretId: string;
@@ -168,7 +168,11 @@ export interface ExecutionWorkspaceSummary {
id: string;
name: string;
mode: Exclude<ExecutionWorkspaceMode, "inherit" | "reuse_existing" | "agent_default"> | "adapter_managed" | "cloud_sandbox";
status: ExecutionWorkspaceStatus;
cwd: string | null;
branchName: string | null;
projectWorkspaceId: string | null;
lastUsedAt: Date;
}
export interface ExecutionWorkspace {
+4 -4
View File
@@ -31,7 +31,7 @@ export const upsertAgentInstructionsFileSchema = z.object({
export type UpsertAgentInstructionsFile = z.infer<typeof upsertAgentInstructionsFileSchema>;
const adapterConfigSchema = z.record(z.unknown()).superRefine((value, ctx) => {
const adapterConfigSchema = z.record(z.string(), z.unknown()).superRefine((value, ctx) => {
const envValue = value.env;
if (envValue === undefined) return;
const parsed = envConfigSchema.safeParse(envValue);
@@ -46,7 +46,7 @@ const adapterConfigSchema = z.record(z.unknown()).superRefine((value, ctx) => {
export const createAgentInstructionsBundleSchema = z.object({
entryFile: z.string().trim().min(1).optional(),
files: z.record(z.string()).refine((files) => Object.keys(files).length > 0, {
files: z.record(z.string(), z.string()).refine((files) => Object.keys(files).length > 0, {
message: "instructionsBundle.files must contain at least one file",
}),
});
@@ -78,7 +78,7 @@ export const createAgentSchema = z.object({
defaultEnvironmentId: z.string().uuid().optional().nullable(),
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
permissions: agentPermissionsSchema.optional(),
metadata: z.record(z.unknown()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
});
export type CreateAgent = z.infer<typeof createAgentSchema>;
@@ -126,7 +126,7 @@ export const wakeAgentSchema = z.object({
source: z.enum(["timer", "assignment", "on_demand", "automation"]).optional().default("on_demand"),
triggerDetail: z.enum(["manual", "ping", "callback", "system"]).optional(),
reason: z.string().optional().nullable(),
payload: z.record(z.unknown()).optional().nullable(),
payload: z.record(z.string(), z.unknown()).optional().nullable(),
idempotencyKey: z.string().optional().nullable(),
forceFreshSession: z.preprocess(
(value) => (value === null ? undefined : value),
+2 -2
View File
@@ -5,7 +5,7 @@ import { multilineTextSchema } from "./text.js";
export const createApprovalSchema = z.object({
type: z.enum(APPROVAL_TYPES),
requestedByAgentId: z.string().uuid().optional().nullable(),
payload: z.record(z.unknown()),
payload: z.record(z.string(), z.unknown()),
issueIds: z.array(z.string().uuid()).optional(),
});
@@ -24,7 +24,7 @@ export const requestApprovalRevisionSchema = z.object({
export type RequestApprovalRevision = z.infer<typeof requestApprovalRevisionSchema>;
export const resubmitApprovalSchema = z.object({
payload: z.record(z.unknown()).optional(),
payload: z.record(z.string(), z.unknown()).optional(),
});
export type ResubmitApproval = z.infer<typeof resubmitApprovalSchema>;
@@ -70,11 +70,11 @@ export const portabilityAgentManifestEntrySchema = z.object({
capabilities: z.string().nullable(),
reportsToSlug: z.string().min(1).nullable(),
adapterType: z.string().min(1),
adapterConfig: z.record(z.unknown()),
runtimeConfig: z.record(z.unknown()),
permissions: z.record(z.unknown()),
adapterConfig: z.record(z.string(), z.unknown()),
runtimeConfig: z.record(z.string(), z.unknown()),
permissions: z.record(z.string(), z.unknown()),
budgetMonthlyCents: z.number().int().nonnegative(),
metadata: z.record(z.unknown()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
});
export const portabilitySkillManifestEntrySchema = z.object({
@@ -88,7 +88,7 @@ export const portabilitySkillManifestEntrySchema = z.object({
sourceRef: z.string().nullable(),
trustLevel: z.string().nullable(),
compatibility: z.string().nullable(),
metadata: z.record(z.unknown()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
fileInventory: z.array(z.object({
path: z.string().min(1),
kind: z.string().min(1),
@@ -105,7 +105,7 @@ export const portabilityProjectManifestEntrySchema = z.object({
targetDate: z.string().nullable(),
color: z.string().nullable(),
status: z.string().nullable(),
executionWorkspacePolicy: z.record(z.unknown()).nullable(),
executionWorkspacePolicy: z.record(z.string(), z.unknown()).nullable(),
workspaces: z.array(z.object({
key: z.string().min(1),
name: z.string().min(1),
@@ -116,10 +116,10 @@ export const portabilityProjectManifestEntrySchema = z.object({
visibility: z.string().nullable(),
setupCommand: z.string().nullable(),
cleanupCommand: z.string().nullable(),
metadata: z.record(z.unknown()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
isPrimary: z.boolean(),
})).default([]),
metadata: z.record(z.unknown()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
});
export const portabilityIssueRoutineTriggerManifestEntrySchema = z.object({
@@ -160,15 +160,15 @@ export const portabilityIssueManifestEntrySchema = z.object({
description: z.string().nullable(),
recurring: z.boolean().default(false),
routine: portabilityIssueRoutineManifestEntrySchema.nullable(),
legacyRecurrence: z.record(z.unknown()).nullable(),
legacyRecurrence: z.record(z.string(), z.unknown()).nullable(),
status: z.string().nullable(),
priority: z.string().nullable(),
labelIds: z.array(z.string().min(1)).default([]),
billingCode: z.string().nullable(),
executionWorkspaceSettings: z.record(z.unknown()).nullable(),
assigneeAdapterOverrides: z.record(z.unknown()).nullable(),
executionWorkspaceSettings: z.record(z.string(), z.unknown()).nullable(),
assigneeAdapterOverrides: z.record(z.string(), z.unknown()).nullable(),
comments: z.array(portabilityIssueCommentManifestEntrySchema).default([]),
metadata: z.record(z.unknown()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
});
export const portabilityManifestSchema = z.object({
@@ -207,7 +207,7 @@ export const portabilitySourceSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("inline"),
rootPath: z.string().min(1).optional().nullable(),
files: z.record(portabilityFileEntrySchema),
files: z.record(z.string(), portabilityFileEntrySchema),
}),
z.object({
type: z.literal("github"),
@@ -262,7 +262,7 @@ export type CompanyPortabilityPreview = z.infer<typeof companyPortabilityPreview
export const portabilityAdapterOverrideSchema = z.object({
adapterType: z.string().min(1),
adapterConfig: z.record(z.unknown()).optional(),
adapterConfig: z.record(z.string(), z.unknown()).optional(),
});
export const companyPortabilityImportSchema = companyPortabilityPreviewSchema.extend({
@@ -24,7 +24,7 @@ export const companySkillSchema = z.object({
trustLevel: companySkillTrustLevelSchema,
compatibility: companySkillCompatibilitySchema,
fileInventory: z.array(companySkillFileInventoryEntrySchema).default([]),
metadata: z.record(z.unknown()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
@@ -16,8 +16,8 @@ const environmentFields = {
description: z.string().optional().nullable(),
driver: environmentDriverSchema,
status: environmentStatusSchema.optional().default("active"),
config: z.record(z.unknown()).optional().default({}),
metadata: z.record(z.unknown()).optional().nullable(),
config: z.record(z.string(), z.unknown()).optional().default({}),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
};
export const createEnvironmentSchema = z.object(environmentFields).strict();
@@ -28,8 +28,8 @@ export const updateEnvironmentSchema = z.object({
description: z.string().optional().nullable(),
driver: environmentDriverSchema.optional(),
status: environmentStatusSchema.optional(),
config: z.record(z.unknown()).optional(),
metadata: z.record(z.unknown()).optional().nullable(),
config: z.record(z.string(), z.unknown()).optional(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
}).strict();
export type UpdateEnvironment = z.infer<typeof updateEnvironmentSchema>;
@@ -37,7 +37,7 @@ export const probeEnvironmentConfigSchema = z.object({
name: z.string().min(1).optional(),
description: z.string().optional().nullable(),
driver: environmentDriverSchema,
config: z.record(z.unknown()).optional().default({}),
metadata: z.record(z.unknown()).optional().nullable(),
config: z.record(z.string(), z.unknown()).optional().default({}),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
}).strict();
export type ProbeEnvironmentConfig = z.infer<typeof probeEnvironmentConfigSchema>;
@@ -13,7 +13,7 @@ export const executionWorkspaceConfigSchema = z.object({
provisionCommand: z.string().optional().nullable(),
teardownCommand: z.string().optional().nullable(),
cleanupCommand: z.string().optional().nullable(),
workspaceRuntime: z.record(z.unknown()).optional().nullable(),
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
desiredState: z.enum(["running", "stopped", "manual"]).optional().nullable(),
serviceStates: z.record(z.enum(["running", "stopped", "manual"])).optional().nullable(),
}).strict();
@@ -94,7 +94,7 @@ export const workspaceRuntimeServiceSchema = z.object({
lastUsedAt: z.coerce.date(),
startedAt: z.coerce.date(),
stoppedAt: z.coerce.date().nullable(),
stopPolicy: z.record(z.unknown()).nullable(),
stopPolicy: z.record(z.string(), z.unknown()).nullable(),
healthStatus: z.enum(["unknown", "healthy", "unhealthy"]),
configIndex: z.number().int().nonnegative().nullable().optional(),
createdAt: z.coerce.date(),
@@ -125,7 +125,7 @@ export const updateExecutionWorkspaceSchema = z.object({
cleanupEligibleAt: z.string().datetime().optional().nullable(),
cleanupReason: z.string().optional().nullable(),
config: executionWorkspaceConfigSchema.optional().nullable(),
metadata: z.record(z.unknown()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
}).strict();
export type UpdateExecutionWorkspace = z.infer<typeof updateExecutionWorkspaceSchema>;
+7
View File
@@ -51,6 +51,11 @@ export {
upsertSidebarOrderPreferenceSchema,
type UpsertSidebarOrderPreference,
} from "./sidebar-preferences.js";
export {
resourceMembershipStateSchema,
updateResourceMembershipSchema,
type UpdateResourceMembership,
} from "./resource-memberships.js";
export {
companySkillSourceTypeSchema,
companySkillTrustLevelSchema,
@@ -295,6 +300,7 @@ export {
createSecretSchema,
createSecretProviderConfigSchema,
updateSecretProviderConfigSchema,
secretProviderConfigDiscoveryPreviewSchema,
remoteSecretImportPreviewSchema,
remoteSecretImportSchema,
remoteSecretImportSelectionSchema,
@@ -311,6 +317,7 @@ export {
type CreateSecret,
type CreateSecretProviderConfig,
type UpdateSecretProviderConfig,
type SecretProviderConfigDiscoveryPreview,
type RemoteSecretImportPreview,
type RemoteSecretImport,
type RemoteSecretImportSelection,
@@ -38,6 +38,7 @@ export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.
export const instanceExperimentalSettingsSchema = z.object({
enableEnvironments: z.boolean().default(false),
enableIsolatedWorkspaces: z.boolean().default(false),
enableCloudSync: z.boolean().default(false),
autoRestartDevServerWhenIdle: z.boolean().default(false),
enableIssueGraphLivenessAutoRecovery: z.boolean().default(false),
issueGraphLivenessAutoRecoveryLookbackHours: z
@@ -27,7 +27,7 @@ export const createIssueTreeHoldSchema = z
mode: issueTreeControlModeSchema,
reason: z.string().trim().min(1).max(1000).optional().nullable(),
releasePolicy: issueTreeHoldReleasePolicySchema.optional().nullable(),
metadata: z.record(z.unknown()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
})
.strict();
@@ -37,7 +37,7 @@ export const releaseIssueTreeHoldSchema = z
.object({
reason: z.string().trim().min(1).max(1000).optional().nullable(),
releasePolicy: issueTreeHoldReleasePolicySchema.optional().nullable(),
metadata: z.record(z.unknown()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
})
.strict();
@@ -73,6 +73,25 @@ describe("issue validators", () => {
).toBe(false);
});
it("allows restored recovery resolutions to return the source issue to todo", () => {
expect(
resolveIssueRecoveryActionSchema.parse({
outcome: "restored",
sourceIssueStatus: "todo",
}),
).toMatchObject({
outcome: "restored",
sourceIssueStatus: "todo",
});
expect(
resolveIssueRecoveryActionSchema.safeParse({
outcome: "false_positive",
sourceIssueStatus: "todo",
}).success,
).toBe(false);
});
it("allows cancelled recovery resolutions to atomically restore the source issue status", () => {
expect(
resolveIssueRecoveryActionSchema.parse({
+12 -8
View File
@@ -116,14 +116,14 @@ export const issueExecutionWorkspaceSettingsSchema = z
mode: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional(),
environmentId: z.string().uuid().optional().nullable(),
workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(),
workspaceRuntime: z.record(z.unknown()).optional().nullable(),
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
})
.strict();
export const issueAssigneeAdapterOverridesSchema = z
.object({
modelProfile: z.enum(MODEL_PROFILE_KEYS).optional(),
adapterConfig: z.record(z.unknown()).optional(),
adapterConfig: z.record(z.string(), z.unknown()).optional(),
useProjectWorkspace: z.boolean().optional(),
})
.strict();
@@ -248,10 +248,10 @@ export const issueRecoveryActionReadModelSchema = z.object({
returnOwnerAgentId: z.string().uuid().nullable(),
cause: z.string().min(1),
fingerprint: z.string().min(1),
evidence: z.record(z.unknown()),
evidence: z.record(z.string(), z.unknown()),
nextAction: z.string().min(1),
wakePolicy: z.record(z.unknown()).nullable(),
monitorPolicy: z.record(z.unknown()).nullable(),
wakePolicy: z.record(z.string(), z.unknown()).nullable(),
monitorPolicy: z.record(z.string(), z.unknown()).nullable(),
attemptCount: z.number().int().nonnegative(),
maxAttempts: z.number().int().positive().nullable(),
timeoutAt: z.union([z.date(), z.string().datetime()]).nullable(),
@@ -275,14 +275,18 @@ const RESOLVE_ISSUE_RECOVERY_ACTION_OUTCOMES = [
export const resolveIssueRecoveryActionSchema = z.object({
actionId: z.string().uuid().optional(),
outcome: z.enum(RESOLVE_ISSUE_RECOVERY_ACTION_OUTCOMES),
sourceIssueStatus: z.enum(["done", "in_review", "blocked"]),
sourceIssueStatus: z.enum(["todo", "done", "in_review", "blocked"]),
resolutionNote: multilineTextSchema.optional().nullable(),
}).strict().superRefine((value, ctx) => {
if (value.outcome === "restored") {
if (value.sourceIssueStatus !== "done" && value.sourceIssueStatus !== "in_review") {
if (
value.sourceIssueStatus !== "todo" &&
value.sourceIssueStatus !== "done" &&
value.sourceIssueStatus !== "in_review"
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Restored recovery actions must move the source issue to done or in_review",
message: "Restored recovery actions must move the source issue to todo, done, or in_review",
path: ["sourceIssueStatus"],
});
}
@@ -8,6 +8,37 @@ describe("plugin capability constants", () => {
});
});
describe("plugin manifest validators", () => {
it("accepts existing-style plugins that do not request access or authorization capabilities", () => {
const parsed = pluginManifestV1Schema.parse({
id: "paperclip.compat-dashboard",
apiVersion: 1,
version: "0.1.0",
displayName: "Compat Dashboard",
description: "Dashboard-only plugin without access or authorization host APIs.",
author: "Paperclip",
categories: ["ui"],
capabilities: ["ui.dashboardWidget.register"],
entrypoints: {
worker: "./dist/worker.js",
ui: "./dist/ui.js",
},
ui: {
slots: [
{
type: "dashboardWidget",
id: "compat-dashboard",
displayName: "Compat Dashboard",
exportName: "CompatDashboard",
},
],
},
});
expect(parsed.capabilities).toEqual(["ui.dashboardWidget.register"]);
});
});
describe("plugin managed routine validators", () => {
it("accepts core issue surface visibility values in routine templates", () => {
const parsed = pluginManagedRoutineDeclarationSchema.parse({
@@ -104,4 +135,54 @@ describe("plugin UI slot validators", () => {
if (parsed.success) return;
expect(parsed.error.issues.some((issue) => issue.message.includes("reserved by the host"))).toBe(true);
});
it("accepts workspace entity types as detailTab targets", () => {
const parsed = pluginUiSlotDeclarationSchema.parse({
type: "detailTab",
id: "workspace-diff-viewer",
displayName: "Diff",
exportName: "WorkspaceDiffViewer",
entityTypes: ["execution_workspace", "project_workspace"],
});
expect(parsed.entityTypes).toEqual(["execution_workspace", "project_workspace"]);
});
it("accepts execution_workspace as a toolbarButton entityType", () => {
const parsed = pluginUiSlotDeclarationSchema.parse({
type: "toolbarButton",
id: "workspace-open-diff",
displayName: "Open diff",
exportName: "OpenWorkspaceDiffButton",
entityTypes: ["execution_workspace"],
});
expect(parsed.entityTypes).toEqual(["execution_workspace"]);
});
it("accepts company settings page slots with a non-core settings route", () => {
const parsed = pluginUiSlotDeclarationSchema.parse({
type: "companySettingsPage",
id: "permissions-settings",
displayName: "Permissions",
exportName: "PermissionsSettingsPage",
routePath: "permissions",
});
expect(parsed.routePath).toBe("permissions");
});
it("prevents company settings page slots from shadowing core settings routes", () => {
const parsed = pluginUiSlotDeclarationSchema.safeParse({
type: "companySettingsPage",
id: "access-settings",
displayName: "Access",
exportName: "AccessSettingsPage",
routePath: "access",
});
expect(parsed.success).toBe(false);
if (parsed.success) return;
expect(parsed.error.issues.some((issue) => issue.message.includes("reserved by the host"))).toBe(true);
});
});
+29 -10
View File
@@ -6,6 +6,7 @@ import {
PLUGIN_UI_SLOT_TYPES,
PLUGIN_UI_SLOT_ENTITY_TYPES,
PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS,
PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS,
PLUGIN_LAUNCHER_PLACEMENT_ZONES,
PLUGIN_LAUNCHER_ACTIONS,
PLUGIN_LAUNCHER_BOUNDS,
@@ -39,7 +40,7 @@ import { routineVariableSchema } from "./routine.js";
*
* @see PLUGIN_SPEC.md §10.1 — Manifest shape
*/
export const jsonSchemaSchema = z.record(z.unknown()).refine(
export const jsonSchemaSchema = z.record(z.string(), z.unknown()).refine(
(val) => {
// Must have a "type" field if non-empty, or be a valid JSON Schema object
if (Object.keys(val).length === 0) return true;
@@ -143,9 +144,9 @@ export const pluginManagedAgentDeclarationSchema = z.object({
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(),
adapterConfig: z.record(z.string(), z.unknown()).optional(),
runtimeConfig: z.record(z.string(), z.unknown()).optional(),
permissions: z.record(z.string(), z.unknown()).optional(),
status: z.enum(["idle", "paused"]).optional(),
budgetMonthlyCents: z.number().int().min(0).optional(),
instructions: z.object({
@@ -166,7 +167,7 @@ export const pluginManagedProjectDeclarationSchema = z.object({
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(),
settings: z.record(z.string(), z.unknown()).optional(),
});
export type PluginManagedProjectDeclarationInput = z.infer<typeof pluginManagedProjectDeclarationSchema>;
@@ -322,10 +323,10 @@ export const pluginUiSlotDeclarationSchema = z.object({
path: ["entityTypes"],
});
}
if (value.routePath && value.type !== "page" && value.type !== "routeSidebar") {
if (value.routePath && value.type !== "page" && value.type !== "routeSidebar" && value.type !== "companySettingsPage") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "routePath is only supported for page and routeSidebar slots",
message: "routePath is only supported for page, routeSidebar, and companySettingsPage slots",
path: ["routePath"],
});
}
@@ -336,6 +337,13 @@ export const pluginUiSlotDeclarationSchema = z.object({
path: ["routePath"],
});
}
if (value.type === "companySettingsPage" && !value.routePath) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "companySettingsPage slots require routePath",
path: ["routePath"],
});
}
if (value.routePath && PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS.includes(value.routePath as (typeof PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS)[number])) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -343,6 +351,17 @@ export const pluginUiSlotDeclarationSchema = z.object({
path: ["routePath"],
});
}
if (
value.type === "companySettingsPage"
&& value.routePath
&& PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS.includes(value.routePath as (typeof PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS)[number])
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `company settings routePath "${value.routePath}" is reserved by the host`,
path: ["routePath"],
});
}
});
export type PluginUiSlotDeclarationInput = z.infer<typeof pluginUiSlotDeclarationSchema>;
@@ -373,7 +392,7 @@ const launcherBoundsByEnvironment: Record<
export const pluginLauncherActionDeclarationSchema = z.object({
type: z.enum(PLUGIN_LAUNCHER_ACTIONS),
target: z.string().min(1),
params: z.record(z.unknown()).optional(),
params: z.record(z.string(), z.unknown()).optional(),
}).superRefine((value, ctx) => {
if (value.type === "performAction" && value.target.includes("/")) {
ctx.addIssue({
@@ -993,7 +1012,7 @@ export type InstallPlugin = z.infer<typeof installPluginSchema>;
* the plugin's instanceConfigSchema is done at the service layer.
*/
export const upsertPluginConfigSchema = z.object({
configJson: z.record(z.unknown()),
configJson: z.record(z.string(), z.unknown()),
});
export type UpsertPluginConfig = z.infer<typeof upsertPluginConfigSchema>;
@@ -1003,7 +1022,7 @@ export type UpsertPluginConfig = z.infer<typeof upsertPluginConfigSchema>;
* Allows a partial merge of config values.
*/
export const patchPluginConfigSchema = z.object({
configJson: z.record(z.unknown()),
configJson: z.record(z.string(), z.unknown()),
});
export type PatchPluginConfig = z.infer<typeof patchPluginConfigSchema>;
+7 -7
View File
@@ -21,16 +21,16 @@ export const projectExecutionWorkspacePolicySchema = z
defaultProjectWorkspaceId: z.string().uuid().optional().nullable(),
environmentId: z.string().uuid().optional().nullable(),
workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(),
workspaceRuntime: z.record(z.unknown()).optional().nullable(),
branchPolicy: z.record(z.unknown()).optional().nullable(),
pullRequestPolicy: z.record(z.unknown()).optional().nullable(),
runtimePolicy: z.record(z.unknown()).optional().nullable(),
cleanupPolicy: z.record(z.unknown()).optional().nullable(),
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
branchPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
pullRequestPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
runtimePolicy: z.record(z.string(), z.unknown()).optional().nullable(),
cleanupPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
})
.strict();
export const projectWorkspaceRuntimeConfigSchema = z.object({
workspaceRuntime: z.record(z.unknown()).optional().nullable(),
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
desiredState: z.enum(["running", "stopped", "manual"]).optional().nullable(),
serviceStates: z.record(z.enum(["running", "stopped", "manual"])).optional().nullable(),
}).strict();
@@ -51,7 +51,7 @@ const projectWorkspaceFields = {
remoteProvider: z.string().optional().nullable(),
remoteWorkspaceRef: z.string().optional().nullable(),
sharedWorkspaceKey: z.string().optional().nullable(),
metadata: z.record(z.unknown()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
runtimeConfig: projectWorkspaceRuntimeConfigSchema.optional().nullable(),
};
@@ -0,0 +1,10 @@
import { z } from "zod";
import { RESOURCE_MEMBERSHIP_STATES } from "../types/resource-memberships.js";
export const resourceMembershipStateSchema = z.enum(RESOURCE_MEMBERSHIP_STATES);
export const updateResourceMembershipSchema = z.object({
state: resourceMembershipStateSchema,
});
export type UpdateResourceMembership = z.infer<typeof updateResourceMembershipSchema>;
+5 -2
View File
@@ -12,6 +12,7 @@ import {
ISSUE_EXECUTION_WORKSPACE_PREFERENCES,
issueExecutionWorkspaceSettingsSchema,
} from "./issue.js";
import { envConfigSchema } from "./secret.js";
const routineVariableValueSchema = z.union([z.string(), z.number().finite(), z.boolean()]);
@@ -60,6 +61,7 @@ export const createRoutineSchema = z.object({
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES).optional().default("coalesce_if_active"),
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES).optional().default("skip_missed"),
variables: z.array(routineVariableSchema).optional().default([]),
env: envConfigSchema.optional().nullable(),
});
export type CreateRoutine = z.infer<typeof createRoutineSchema>;
@@ -83,6 +85,7 @@ export const routineRevisionSnapshotRoutineV1Schema = z.object({
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES),
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES),
variables: z.array(routineVariableSchema),
env: envConfigSchema.nullable().default(null),
}).strict();
export const routineRevisionSnapshotTriggerV1Schema = z.object({
@@ -143,8 +146,8 @@ export type UpdateRoutineTrigger = z.infer<typeof updateRoutineTriggerSchema>;
export const runRoutineSchema = z.object({
triggerId: z.string().uuid().optional().nullable(),
payload: z.record(z.unknown()).optional().nullable(),
variables: z.record(routineVariableValueSchema).optional().nullable(),
payload: z.record(z.string(), z.unknown()).optional().nullable(),
variables: z.record(z.string(), routineVariableValueSchema).optional().nullable(),
projectId: z.string().uuid().optional().nullable(),
assigneeAgentId: z.string().uuid().optional().nullable(),
idempotencyKey: z.string().trim().max(255).optional().nullable(),
@@ -4,6 +4,7 @@ import {
createSecretSchema,
remoteSecretImportPreviewSchema,
remoteSecretImportSchema,
secretProviderConfigDiscoveryPreviewSchema,
secretProviderConfigPayloadSchema,
updateSecretProviderConfigSchema,
} from "./secret.js";
@@ -140,6 +141,40 @@ describe("secret validators", () => {
});
});
it("validates AWS provider vault discovery draft config without allowing sensitive keys", () => {
expect(
secretProviderConfigDiscoveryPreviewSchema.parse({
provider: "aws_secrets_manager",
config: {
region: "us-east-1",
namespace: "production",
secretNamePrefix: "paperclip",
},
query: "paperclip",
pageSize: 50,
}),
).toEqual({
provider: "aws_secrets_manager",
config: {
region: "us-east-1",
namespace: "production",
secretNamePrefix: "paperclip",
},
query: "paperclip",
pageSize: 50,
});
expect(() =>
secretProviderConfigDiscoveryPreviewSchema.parse({
provider: "aws_secrets_manager",
config: {
region: "us-east-1",
accessKeyId: "AKIA...",
},
}),
).toThrow(/sensitive field/i);
});
it("caps AWS remote import paging and row counts", () => {
expect(() =>
remoteSecretImportPreviewSchema.parse({
+30 -6
View File
@@ -25,7 +25,7 @@ export const envBindingSchema = z.union([
envBindingSecretRefSchema,
]);
export const envConfigSchema = z.record(envBindingSchema);
export const envConfigSchema = z.record(z.string(), envBindingSchema);
export const createSecretSchema = z.object({
name: z.string().min(1),
@@ -36,7 +36,7 @@ export const createSecretSchema = z.object({
value: z.string().min(1).optional().nullable(),
description: z.string().optional().nullable(),
externalRef: z.string().optional().nullable(),
providerMetadata: z.record(z.unknown()).optional().nullable(),
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
providerVersionRef: z.string().optional().nullable(),
}).superRefine((value, ctx) => {
if ((value.managedMode ?? "paperclip_managed") === "external_reference") {
@@ -83,7 +83,7 @@ export const updateSecretSchema = z.object({
providerConfigId: z.string().uuid().optional().nullable(),
description: z.string().optional().nullable(),
externalRef: z.string().optional().nullable(),
providerMetadata: z.record(z.unknown()).optional().nullable(),
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
});
export type UpdateSecret = z.infer<typeof updateSecretSchema>;
@@ -198,7 +198,7 @@ export const createSecretProviderConfigSchema = z.object({
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({}),
config: z.record(z.string(), z.unknown()).default({}),
}).superRefine((value, ctx) => {
rejectSensitiveProviderConfigKeys(value.config, ctx);
const parsed = secretProviderConfigPayloadSchema.safeParse({
@@ -236,7 +236,7 @@ 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(),
config: z.record(z.string(), z.unknown()).optional(),
}).superRefine((value, ctx) => {
if (value.config !== undefined) {
rejectSensitiveProviderConfigKeys(value.config, ctx);
@@ -262,13 +262,37 @@ export const remoteSecretImportPreviewSchema = z.object({
export type RemoteSecretImportPreview = z.infer<typeof remoteSecretImportPreviewSchema>;
export const secretProviderConfigDiscoveryPreviewSchema = z.object({
provider: z.enum(SECRET_PROVIDERS),
config: z.record(z.unknown()).default({}),
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(),
}).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],
});
}
}
});
export type SecretProviderConfigDiscoveryPreview = z.infer<typeof secretProviderConfigDiscoveryPreviewSchema>;
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(),
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
});
export const remoteSecretImportSchema = z.object({
@@ -43,7 +43,7 @@ export const createIssueWorkProductSchema = z.object({
isPrimary: z.boolean().optional().default(false),
healthStatus: z.enum(["unknown", "healthy", "unhealthy"]).optional().default("unknown"),
summary: z.string().optional().nullable(),
metadata: z.record(z.unknown()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
createdByRunId: z.string().uuid().optional().nullable(),
});