[codex] Roll up May 17 branch changes (#6210)

## Thinking Path

> - Paperclip is the control plane for autonomous AI companies, so agent
work needs visible ownership, recovery, and operator controls.
> - This local branch had accumulated several related control-plane
reliability and operator-experience fixes across recovery actions,
watchdog folding, model-profile defaults, mentions, markdown editing,
plugin launchers, and small UI polish.
> - The branch needed to be converted into a PR against the current
`origin/master` without losing dirty work or including lockfile/workflow
churn.
> - The safest standalone shape is a single rollup PR because the
recovery/server/UI files overlap heavily across the local commits and
splitting would create avoidable conflicts.
> - This pull request replays the local branch onto latest
`origin/master`, preserves the uncommitted work as logical commits, and
adds a Zod 4 validator compatibility fix found during verification.
> - The benefit is that the May 17 local branch can be reviewed and
merged as one coherent, conflict-free branch under the 100-file Greptile
limit.

## What Changed

- Rebased the local May 17 branch work onto current `origin/master` in a
dedicated worktree.
- Preserved and committed previously dirty changes for recovery retry
handling, plugin/sidebar launcher polish, and `.herenow` ignores.
- Added recovery-action behavior for returning source issues to `todo`
when retrying source-scoped recovery.
- Included the existing local recovery/liveness/watchdog fold, Codex
cheap-profile, markdown/mention, duplicate-agent, and UI polish commits
from the branch.
- Normalized shared validator `z.record(...)` schemas to explicit
string-key records for Zod 4 compatibility.
- Confirmed the PR has no `pnpm-lock.yaml` or `.github/workflows/*`
changes and stays below the 100-file Greptile limit.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `npm run install` in
`node_modules/.pnpm/sqlite3@5.1.7/node_modules/sqlite3` to build the
local native sqlite3 binding after installing with scripts disabled
- `pnpm exec vitest run packages/shared/src/validators/issue.test.ts
packages/shared/src/project-mentions.test.ts
packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/heartbeat-model-profile.test.ts
server/src/__tests__/issue-recovery-actions.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts
server/src/__tests__/plugin-local-folders.test.ts
ui/src/components/IssueRecoveryActionCard.test.tsx
ui/src/components/Sidebar.test.tsx
ui/src/components/SidebarAccountMenu.test.tsx
ui/src/components/IssueProperties.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/MarkdownBody.test.tsx
ui/src/lib/duplicate-agent-payload.test.ts
ui/src/pages/Routines.test.tsx`
- First pass: 13 files passed with 201 passing tests; 3 server files
failed before sqlite3 native binding was built.
- After rebuilding sqlite3:
`server/src/__tests__/heartbeat-model-profile.test.ts`,
`server/src/__tests__/issue-recovery-actions.test.ts`, and
`server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts`
passed/loaded; embedded Postgres tests were skipped by the local host
guard.
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/adapter-utils typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`

## Risks

- Medium risk: this is a broad rollup PR across recovery semantics,
server tests, shared validators, and UI surfaces.
- Some embedded Postgres tests skipped locally due the host guard, so CI
should provide the stronger database-backed signal.
- UI changes were covered by component tests, but no browser screenshot
was captured in this PR creation pass.
- This branch may overlap with existing recovery/liveness PR work; merge
this PR independently or restack/close overlapping branches rather than
merging duplicate implementations together.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent, tool-enabled local repository
and GitHub workflow, medium reasoning effort.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-05-17 17:15:06 -05:00 committed by GitHub
parent 705c1b8d81
commit d734bd43d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 3675 additions and 180 deletions

View file

@ -1047,22 +1047,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";

View file

@ -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"]);
});
});

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();

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),

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>;

View file

@ -67,11 +67,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({
@ -85,7 +85,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),
@ -102,7 +102,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),
@ -113,10 +113,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({
@ -157,15 +157,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({
@ -197,7 +197,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"),
@ -251,7 +251,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({

View file

@ -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(),
});

View file

@ -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>;

View file

@ -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>;

View file

@ -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();

View file

@ -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({

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"],
});
}

View file

@ -39,7 +39,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 +143,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 +166,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>;
@ -373,7 +373,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 +993,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 +1003,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>;

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(),
};

View file

@ -146,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(),

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);
@ -268,7 +268,7 @@ export const remoteSecretImportSelectionSchema = z.object({
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({

View file

@ -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(),
});