Files
PaperClipAI/packages/shared/src/validators/agent.ts
T
Dotta a70b0f70b6 Merge remote-tracking branch 'origin/master' into PAP-16679-paperclip-runner
* origin/master: (57 commits)
  fix(adapter-utils): extend the duplex fail-closed run disposition to the CLI lane (#11966)
  fix(recovery): stop automatic stranded-task takeovers (#11961)
  feat(sandbox): add the duplex bridge broker, gated transport selection, and fixed observability (#11769)
  Drop the mission step from the wizard arc (#11935)
  fix(runtime): guard empty Bash 3.2 array expansion (#11891)
  chore(lockfile): refresh pnpm-lock.yaml (#11915)
  feat(commitperclip): surface the Co-Authored-By trailers a squash merge needs (#11498)
  Rebuild the onboarding agent arc on the prototype's step design (#11905)
  fix(workspaces): read process cwd on macOS so port-owner adoption works (#11763)
  fix(workspaces): attach PR preparation to existing branches (#11703)
  fix(grok-local): stop defaulting --permission-mode to dontAsk (#11898)
  Fix remote-only workspace base refs and pre-adapter retry loops (#11892)
  feat(environments): refer to the managed default environment by name, not the sandbox driver key (#11838)
  test(server): cover buffered duplex chunk drain after a late listener bind (#11865)
  fix(server,ui,claude-local): verify auth on the adapter Test lane and enforce managed-sandbox tenant binding (#11810)
  chore(lockfile): refresh pnpm-lock.yaml (#11867)
  fix(kimi): align package with Node 24 policy (#11890)
  feat(ui): rename "Agent mode" to "Auto mode" and show full work-mode labels (#11866)
  fix(build): enforce Node 24 across Paperclip (#11792)
  fix(server): end the duplex route on the pre-bind bounds when frames arrive before the bind (#11860)
  ...

# Conflicts:
#	Dockerfile
#	packages/db/src/migrations/meta/0226_snapshot.json
#	packages/db/src/migrations/meta/_journal.json
#	pnpm-lock.yaml
#	server/package.json
#	server/src/routes/agents.ts
#	server/src/services/heartbeat.ts
#	server/src/services/issues.ts
#	server/src/services/recovery/service.ts
#	ui/package.json
#	ui/src/adapters/transcript.test.ts
#	ui/src/components/TaskChatThread.test.tsx
#	ui/src/components/TaskChatThread.tsx
#	ui/src/components/task-chat/TaskChatTurn.tsx
#	ui/src/index.css
#	ui/src/pages/Agents.tsx
2026-08-22 15:46:15 -05:00

254 lines
9.9 KiB
TypeScript

import { z } from "zod";
import {
AGENT_ICON_NAMES,
AGENT_ROLES,
AGENT_STATUSES,
INBOX_MINE_ISSUE_STATUS_FILTER,
} from "../constants.js";
import { agentAdapterTypeSchema } from "../adapter-type.js";
import { envConfigSchema } from "./secret.js";
import { trustAuthorizationPolicySchema, trustPresetSchema } from "./trust-policy.js";
import { agentDesiredSkillSelectionSchema } from "./adapter-skills.js";
import { objectWithoutDefaults } from "./partial.js";
export const agentPermissionsSchema = z.object({
canCreateAgents: z.boolean().optional().default(false),
canCreateSkills: z.boolean().optional().default(true),
trustPreset: trustPresetSchema.optional(),
authorizationPolicy: trustAuthorizationPolicySchema.optional(),
}).catchall(z.unknown());
export const agentInstructionsBundleModeSchema = z.enum(["managed", "external"]);
export const updateAgentInstructionsBundleSchema = z.object({
mode: agentInstructionsBundleModeSchema.optional(),
rootPath: z.string().trim().min(1).nullable().optional(),
entryFile: z.string().trim().min(1).optional(),
clearLegacyPromptTemplate: z.boolean().optional().default(false),
});
export type UpdateAgentInstructionsBundle = z.infer<typeof updateAgentInstructionsBundleSchema>;
export const upsertAgentInstructionsFileSchema = z.object({
path: z.string().trim().min(1),
content: z.string(),
clearLegacyPromptTemplate: z.boolean().optional().default(false),
});
export type UpsertAgentInstructionsFile = z.infer<typeof upsertAgentInstructionsFileSchema>;
const adapterConfigSchema = z.record(z.string(), z.unknown()).superRefine((value, ctx) => {
const envValue = value.env;
if (envValue === undefined) return;
const parsed = envConfigSchema.safeParse(envValue);
if (!parsed.success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "adapterConfig.env must be a map of valid env bindings",
path: ["env"],
});
}
});
export const createAgentInstructionsBundleSchema = z.object({
entryFile: z.string().trim().min(1).optional(),
files: z.record(z.string(), z.string()).refine((files) => Object.keys(files).length > 0, {
message: "instructionsBundle.files must contain at least one file",
}),
});
const agentModelProfileConfigSchema = z.object({
enabled: z.boolean().optional(),
label: z.string().trim().min(1).optional(),
// Disabled profiles created before model-profile editing may not have an
// adapter payload yet. Keep them valid so unrelated runtime settings (such
// as debug capture) can be updated without fabricating model configuration.
adapterConfig: adapterConfigSchema.optional().default({}),
}).strict();
export const agentRuntimeConfigSchema = z.object({
modelProfiles: z.object({
cheap: agentModelProfileConfigSchema.optional(),
}).strict().optional(),
debug: z.object({
providerTrace: z.literal("raw").optional(),
}).strict().optional(),
}).catchall(z.unknown());
export const createAgentSchema = z.object({
name: z.string().min(1),
role: z.enum(AGENT_ROLES).optional().default("general"),
title: z.string().optional().nullable(),
icon: z.enum(AGENT_ICON_NAMES).optional().nullable(),
reportsTo: z.string().guid().optional().nullable(),
capabilities: z.string().optional().nullable(),
desiredSkills: z.array(agentDesiredSkillSelectionSchema).optional(),
adapterType: agentAdapterTypeSchema,
adapterConfig: adapterConfigSchema.optional().default({}),
instructionsBundle: createAgentInstructionsBundleSchema.optional(),
runtimeConfig: agentRuntimeConfigSchema.optional().default({}),
defaultEnvironmentId: z.string().guid().optional().nullable(),
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
permissions: agentPermissionsSchema.optional(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
// The optional stored-session claim from a completed Claude login session. It
// is the non-secret `storedSessionId`; it carries no token. The agent-create
// transaction consumes it as the one-time stored-session claim.
storedSessionId: z.string().min(1).max(256).optional(),
// The optional apply-existing flag. When true, the caller binds the fixed
// Claude OAuth token reference to the owner stored value with no new login
// round trip. The server permits the no-claim bind only for a user actor and
// only when that owner already has a stored value. It carries no token.
applyStoredClaudeLogin: z.boolean().optional(),
});
export type CreateAgent = z.infer<typeof createAgentSchema>;
export const builtInAgentProvisionSchema = z.object({
adapterType: agentAdapterTypeSchema.optional(),
adapterConfig: adapterConfigSchema.optional(),
budgetMonthlyCents: z.number().int().nonnegative().optional(),
}).strict();
export type BuiltInAgentProvision = z.infer<typeof builtInAgentProvisionSchema>;
export const builtInAgentEmptyMutationSchema = z.object({}).strict().default({});
export type BuiltInAgentEmptyMutation = z.infer<typeof builtInAgentEmptyMutationSchema>;
export const builtInAgentResetSchema = z.object({
resources: z.array(z.enum(["agent", "instructions", "skill", "routine"])).optional(),
}).strict().default({});
export type BuiltInAgentReset = z.infer<typeof builtInAgentResetSchema>;
export const createAgentHireSchema = createAgentSchema.extend({
sourceIssueId: z.string().guid().optional().nullable(),
sourceIssueIds: z.array(z.string().guid()).optional(),
});
export type CreateAgentHire = z.infer<typeof createAgentHireSchema>;
export const updateAgentSchema = objectWithoutDefaults(
createAgentSchema.omit({ permissions: true }),
)
.partial()
.extend({
permissions: z.never().optional(),
replaceAdapterConfig: z.boolean().optional(),
status: z.enum(AGENT_STATUSES).optional(),
spentMonthlyCents: z.number().int().nonnegative().optional(),
});
export type UpdateAgent = z.infer<typeof updateAgentSchema>;
export const updateAgentInstructionsPathSchema = z.object({
path: z.string().trim().min(1).nullable(),
adapterConfigKey: z.string().trim().min(1).optional(),
});
export type UpdateAgentInstructionsPath = z.infer<typeof updateAgentInstructionsPathSchema>;
export const taskBridgeAgentKeyScopeSchema = z.object({
kind: z.literal("task_bridge"),
projectId: z.string().guid().optional().nullable(),
projectIds: z.array(z.string().guid()).max(50).optional(),
parentIssueId: z.string().guid().optional().nullable(),
parentIssueIds: z.array(z.string().guid()).max(50).optional(),
allowedAssigneeAgentIds: z.array(z.string().guid()).max(50).optional(),
}).strict().superRefine((value, ctx) => {
const hasProjectBoundary = Boolean(value.projectId) || Boolean(value.projectIds?.length);
const hasParentBoundary = Boolean(value.parentIssueId) || Boolean(value.parentIssueIds?.length);
if (!hasProjectBoundary && !hasParentBoundary) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "task_bridge keys require at least one project or parent issue boundary",
path: ["projectId"],
});
}
});
export const standardAgentKeyScopeSchema = z.object({
kind: z.literal("standard"),
}).strict();
export const skillTestAgentKeyScopeSchema = z.object({
kind: z.literal("skill_test"),
issueId: z.string().guid(),
}).strict();
export const agentApiKeyScopeSchema = z.union([
standardAgentKeyScopeSchema,
taskBridgeAgentKeyScopeSchema,
skillTestAgentKeyScopeSchema,
]);
export type AgentApiKeyScope = z.infer<typeof agentApiKeyScopeSchema>;
export type TaskBridgeAgentKeyScope = z.infer<typeof taskBridgeAgentKeyScopeSchema>;
export type SkillTestAgentKeyScope = z.infer<typeof skillTestAgentKeyScopeSchema>;
export function normalizeAgentApiKeyScope(value: unknown): AgentApiKeyScope {
const parsed = agentApiKeyScopeSchema.safeParse(value);
return parsed.success ? parsed.data : { kind: "standard" };
}
export const createAgentKeySchema = z.object({
name: z.string().min(1).default("default"),
scope: agentApiKeyScopeSchema.optional().default({ kind: "standard" }),
});
export type CreateAgentKey = z.infer<typeof createAgentKeySchema>;
export const agentMineInboxQuerySchema = z.object({
userId: z.string().trim().min(1),
status: z.string().trim().min(1).optional().default(INBOX_MINE_ISSUE_STATUS_FILTER),
});
export type AgentMineInboxQuery = z.infer<typeof agentMineInboxQuerySchema>;
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.string(), z.unknown()).optional().nullable(),
idempotencyKey: z.string().optional().nullable(),
forceFreshSession: z.preprocess(
(value) => (value === null ? undefined : value),
z.boolean().optional().default(false),
),
debug: z.object({
providerTrace: z.literal("raw"),
}).strict().optional(),
});
export type WakeAgent = z.infer<typeof wakeAgentSchema>;
export const resetAgentSessionSchema = z.object({
taskKey: z.string().min(1).optional().nullable(),
});
export type ResetAgentSession = z.infer<typeof resetAgentSessionSchema>;
export const testAdapterEnvironmentSchema = z.object({
adapterConfig: adapterConfigSchema.optional().default({}),
/**
* Optional environment to run the adapter test inside. When omitted, the
* test runs against the local Paperclip host. When provided and the
* environment is non-local (SSH/sandbox), the test probes are executed
* inside that environment so the result reflects real agent execution.
*/
environmentId: z.string().guid().optional().nullable(),
});
export type TestAdapterEnvironment = z.infer<typeof testAdapterEnvironmentSchema>;
export const updateAgentPermissionsSchema = z.object({
canCreateAgents: z.boolean(),
canCreateSkills: z.boolean().optional(),
canAssignTasks: z.boolean(),
trustPreset: trustPresetSchema.optional(),
authorizationPolicy: trustAuthorizationPolicySchema.optional(),
});
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;