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:
@@ -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`,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface InstanceGeneralSettings {
|
||||
export interface InstanceExperimentalSettings {
|
||||
enableEnvironments: boolean;
|
||||
enableIsolatedWorkspaces: boolean;
|
||||
enableCloudSync: boolean;
|
||||
autoRestartDevServerWhenIdle: boolean;
|
||||
enableIssueGraphLivenessAutoRecovery: boolean;
|
||||
issueGraphLivenessAutoRecoveryLookbackHours: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user