Files
PaperClipAI/server/src/routes/openapi.ts
T
DottaandPaperclip 4693d770aa Add company artifacts page (#7621)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Operators need a way to inspect files and work products created by
agents across a company without opening each issue one by one.
> - The existing issue detail surfaces already show attachments and
outputs, but there was no company-level artifacts index or search-result
affordance for artifact-like records.
> - The backend needed a company-scoped artifacts projection API that
preserves issue/run attribution and safe links back to source records.
> - The UI needed a first-class Artifacts page, sidebar entry, reusable
artifact cards, and deep-link handling that keeps company prefixes
intact.
> - This pull request adds the company artifacts API and page, then
wires artifacts into search and issue output surfaces.
> - The benefit is a single place to browse, filter, and open generated
work products and attachments while preserving company boundaries.

## Linked Issues or Issue Description

Fixes #7622.

Feature request fields:

- Problem/motivation: company operators need a consolidated artifacts
surface for attachments and work products produced by agents.
- Proposed solution: add a company-scoped artifacts projection endpoint,
a board Artifacts route, reusable cards, sidebar navigation, and
artifact search integration.
- Alternatives considered: keep artifact discovery only on individual
issue pages; that forces operators to know the source issue before
finding generated outputs.
- Roadmap alignment: checked `ROADMAP.md`; this is a focused board
UI/API improvement and does not duplicate a listed roadmap item.

## What Changed

- Added shared artifact types and validators.
- Added a company-scoped artifact projection service/API with tests for
attachment/work-product attribution.
- Added Artifacts board UI route, API client, sidebar link, cards,
filters, and storybook coverage.
- Added artifact result handling to company search and issue
output/deep-link flows.
- Rebased the branch onto the latest `public-gh/master` state and
resolved the route-test conflict by preserving both upstream
team-catalog coverage and artifact route coverage.
- Fixed a local Sidebar test helper so it no longer depends on a
runtime-undefined `React.act` export in this dependency install.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/artifacts/ArtifactCard.test.tsx src/api/artifacts.test.ts
src/lib/company-routes.test.ts`
- `pnpm --filter @paperclipai/ui exec vitest run
src/pages/Artifacts.test.tsx src/pages/Search.test.tsx
src/components/Sidebar.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/company-artifacts-service.test.ts
server/src/__tests__/company-search-service.test.ts
server/src/__tests__/company-search-rate-limit-routes.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts`
- Confirmed the PR diff does not include `pnpm-lock.yaml` or
`.github/workflows/*`.
- Duplicate search: no open PRs or issues found for `artifact page
ArtifactCard` in `paperclipai/paperclip`.

Screenshots are intentionally omitted per the internal task instruction
not to add design screenshots or images to this PR unless they are
specifically part of the work. I also attempted browser capture in this
runner, but `agent-browser` failed to launch Chrome and Playwright
Chromium is missing `libatk-1.0.so.0`.

## Risks

- Low-to-medium risk: this adds a new API projection and UI surface, so
attribution/link regressions could affect artifact navigation.
- Company scoping is covered in the new service/API tests.
- No database migrations are included.
- No lockfile or workflow changes are included.

> 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 coding agent with tool use and local command
execution. Exact hosted model identifier is not exposed in this runtime.

## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [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 (intentionally omitted per task instruction; browser capture
unavailable in this runner)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-05 18:11:05 -05:00

4389 lines
134 KiB
TypeScript

import { Router } from "express";
import { z } from "zod";
import {
// Agent
createAgentSchema,
createAgentHireSchema,
updateAgentSchema,
updateAgentPermissionsSchema,
updateAgentInstructionsPathSchema,
updateAgentInstructionsBundleSchema,
upsertAgentInstructionsFileSchema,
createAgentKeySchema,
wakeAgentSchema,
resetAgentSessionSchema,
agentSkillSyncSchema,
testAdapterEnvironmentSchema,
// Issue
createIssueSchema,
updateIssueSchema,
createIssueLabelSchema,
addIssueCommentSchema,
checkoutIssueSchema,
linkIssueApprovalSchema,
createIssueWorkProductSchema,
updateIssueWorkProductSchema,
upsertIssueDocumentSchema,
restoreIssueDocumentRevisionSchema,
upsertIssueFeedbackVoteSchema,
// Project
createProjectSchema,
updateProjectSchema,
createProjectWorkspaceSchema,
updateProjectWorkspaceSchema,
// Company
createCompanySchema,
updateCompanySchema,
updateCompanyBrandingSchema,
companyArtifactsQuerySchema,
companyArtifactsResponseSchema,
// Routine
createRoutineSchema,
updateRoutineSchema,
createRoutineTriggerSchema,
updateRoutineTriggerSchema,
rotateRoutineTriggerSecretSchema,
runRoutineSchema,
// Goal
createGoalSchema,
updateGoalSchema,
// Secret
createSecretSchema,
updateSecretSchema,
rotateSecretSchema,
// Approval
createApprovalSchema,
resolveApprovalSchema,
requestApprovalRevisionSchema,
resubmitApprovalSchema,
addApprovalCommentSchema,
// Cost / budget
createCostEventSchema,
createFinanceEventSchema,
updateBudgetSchema,
upsertBudgetPolicySchema,
resolveBudgetIncidentSchema,
// Sidebar
upsertSidebarOrderPreferenceSchema,
// Execution workspaces
updateExecutionWorkspaceSchema,
workspaceRuntimeControlTargetSchema,
// Environments
createEnvironmentSchema,
updateEnvironmentSchema,
probeEnvironmentConfigSchema,
// Company skills
companySkillCreateSchema,
companySkillFileUpdateSchema,
companySkillImportSchema,
companySkillProjectScanRequestSchema,
// Issue tree
createIssueTreeHoldSchema,
previewIssueTreeControlSchema,
releaseIssueTreeHoldSchema,
// Issue interactions
createIssueThreadInteractionSchema,
createChildIssueSchema,
acceptIssueThreadInteractionSchema,
rejectIssueThreadInteractionSchema,
respondIssueThreadInteractionSchema,
// Auth / profile
updateCurrentUserProfileSchema,
// Company portability (legacy routes)
companyPortabilityExportSchema,
companyPortabilityPreviewSchema,
companyPortabilityImportSchema,
// Access / membership
acceptInviteSchema,
createCompanyInviteSchema,
createOpenClawInvitePromptSchema,
claimJoinRequestApiKeySchema,
createCliAuthChallengeSchema,
resolveCliAuthChallengeSchema,
createBoardApiKeySchema,
updateCompanyMemberSchema,
updateCompanyMemberWithPermissionsSchema,
archiveCompanyMemberSchema,
updateMemberPermissionsSchema,
updateUserCompanyAccessSchema,
// Instance settings
patchInstanceGeneralSettingsSchema,
patchInstanceExperimentalSettingsSchema,
issueGraphLivenessAutoRecoveryRequestSchema,
// Resource memberships
updateResourceMembershipSchema,
// Document annotations
createDocumentAnnotationCommentSchema,
createDocumentAnnotationThreadSchema,
updateDocumentAnnotationThreadSchema,
// Issue recovery and decomposition
createAcceptedPlanDecompositionSchema,
resolveIssueRecoveryActionSchema,
cancelIssueThreadInteractionSchema,
// Secret provider configs and remote import
createSecretProviderConfigSchema,
updateSecretProviderConfigSchema,
secretProviderConfigDiscoveryPreviewSchema,
remoteSecretImportPreviewSchema,
remoteSecretImportSchema,
} from "@paperclipai/shared";
type JsonSchema = Record<string, unknown>;
type OpenApiResponse = Record<string, unknown>;
type OpenApiPathRegistration = {
method: string;
path: string;
request?: {
params?: z.ZodTypeAny;
query?: z.ZodTypeAny;
body?: {
content: Record<string, { schema: unknown }>;
required?: boolean;
};
};
responses?: Record<string, OpenApiResponse>;
[key: string]: unknown;
};
const zodTypeName = (schema: z.ZodTypeAny) => schema._def.typeName as string;
function unwrapSchema(schema: z.ZodTypeAny): z.ZodTypeAny {
const typeName = zodTypeName(schema);
if (typeName === "ZodOptional" || typeName === "ZodDefault" || typeName === "ZodCatch") {
return unwrapSchema(schema._def.innerType);
}
if (typeName === "ZodEffects") {
return unwrapSchema(schema._def.schema);
}
return schema;
}
function isOptionalSchema(schema: z.ZodTypeAny): boolean {
const typeName = zodTypeName(schema);
if (typeName === "ZodOptional" || typeName === "ZodDefault" || typeName === "ZodCatch") {
return true;
}
if (typeName === "ZodEffects") {
return isOptionalSchema(schema._def.schema);
}
if (typeName === "ZodNullable") {
return isOptionalSchema(schema._def.innerType);
}
return false;
}
function applyStringChecks(jsonSchema: JsonSchema, checks: Array<Record<string, unknown>>) {
for (const check of checks) {
if (check.kind === "min") jsonSchema.minLength = check.value;
if (check.kind === "max") jsonSchema.maxLength = check.value;
if (check.kind === "email") jsonSchema.format = "email";
if (check.kind === "url") jsonSchema.format = "uri";
if (check.kind === "uuid") jsonSchema.format = "uuid";
if (check.kind === "datetime") jsonSchema.format = "date-time";
if (check.kind === "regex" && check.regex instanceof RegExp) {
jsonSchema.pattern = check.regex.source;
}
}
}
function applyNumberChecks(jsonSchema: JsonSchema, checks: Array<Record<string, unknown>>) {
for (const check of checks) {
if (check.kind === "int") jsonSchema.type = "integer";
if (check.kind === "min") {
jsonSchema.minimum = check.value;
if (!check.inclusive) jsonSchema.exclusiveMinimum = true;
}
if (check.kind === "max") {
jsonSchema.maximum = check.value;
if (!check.inclusive) jsonSchema.exclusiveMaximum = true;
}
}
}
function zodToOpenApiSchema(schema: z.ZodTypeAny): JsonSchema {
const unwrapped = unwrapSchema(schema);
const typeName = zodTypeName(unwrapped);
if (typeName === "ZodString") {
const jsonSchema: JsonSchema = { type: "string" };
applyStringChecks(jsonSchema, unwrapped._def.checks ?? []);
return jsonSchema;
}
if (typeName === "ZodNumber") {
const jsonSchema: JsonSchema = { type: "number" };
applyNumberChecks(jsonSchema, unwrapped._def.checks ?? []);
return jsonSchema;
}
if (typeName === "ZodBoolean") return { type: "boolean" };
if (typeName === "ZodDate") return { type: "string", format: "date-time" };
if (typeName === "ZodAny" || typeName === "ZodUnknown") return {};
if (typeName === "ZodLiteral") {
const value = unwrapped._def.value;
return { type: typeof value, enum: [value] };
}
if (typeName === "ZodEnum") {
return { type: "string", enum: unwrapped._def.values };
}
if (typeName === "ZodNativeEnum") {
const values = Object.values(unwrapped._def.values).filter(
(value) => typeof value === "string" || typeof value === "number",
);
return { enum: Array.from(new Set(values)) };
}
if (typeName === "ZodArray") {
return { type: "array", items: zodToOpenApiSchema(unwrapped._def.type) };
}
if (typeName === "ZodRecord") {
return {
type: "object",
additionalProperties: zodToOpenApiSchema(unwrapped._def.valueType),
};
}
if (typeName === "ZodNullable") {
return { ...zodToOpenApiSchema(unwrapped._def.innerType), nullable: true };
}
if (typeName === "ZodUnion") {
return { oneOf: unwrapped._def.options.map((option: z.ZodTypeAny) => zodToOpenApiSchema(option)) };
}
if (typeName === "ZodDiscriminatedUnion") {
return {
oneOf: Array.from(unwrapped._def.options.values()).map((option) =>
zodToOpenApiSchema(option as z.ZodTypeAny),
),
};
}
if (typeName === "ZodIntersection") {
return {
allOf: [
zodToOpenApiSchema(unwrapped._def.left),
zodToOpenApiSchema(unwrapped._def.right),
],
};
}
if (typeName === "ZodObject") {
const shape = unwrapped._def.shape();
const properties: Record<string, JsonSchema> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
const propertySchema = value as z.ZodTypeAny;
properties[key] = zodToOpenApiSchema(propertySchema);
if (!isOptionalSchema(propertySchema)) required.push(key);
}
const jsonSchema: JsonSchema = { type: "object", properties };
if (required.length > 0) jsonSchema.required = required;
return jsonSchema;
}
return {};
}
function normalizeContent(content: Record<string, { schema: unknown }>) {
return Object.fromEntries(
Object.entries(content).map(([contentType, media]) => [
contentType,
{
...media,
schema: isZodSchema(media.schema)
? zodToOpenApiSchema(media.schema)
: media.schema,
},
]),
);
}
function isZodSchema(value: unknown): value is z.ZodTypeAny {
return Boolean(
value &&
typeof value === "object" &&
"_def" in value &&
typeof (value as z.ZodTypeAny).safeParse === "function",
);
}
function normalizeResponses(responses: Record<string, OpenApiResponse> = {}) {
return Object.fromEntries(
Object.entries(responses).map(([status, response]) => {
const content = response.content as Record<string, { schema: unknown }> | undefined;
return [
status,
content
? {
...response,
content: normalizeContent(content),
}
: response,
];
}),
);
}
function parametersFromSchema(schema: z.ZodTypeAny, location: "path" | "query") {
const objectSchema = unwrapSchema(schema);
if (zodTypeName(objectSchema) !== "ZodObject") return [];
const shape = objectSchema._def.shape();
return Object.entries(shape).map(([name, value]) => ({
name,
in: location,
required: location === "path" ? true : !isOptionalSchema(value as z.ZodTypeAny),
schema: zodToOpenApiSchema(value as z.ZodTypeAny),
}));
}
class OpenAPIRegistry {
private readonly schemas: Record<string, JsonSchema> = {};
private readonly paths: Array<OpenApiPathRegistration> = [];
register(name: string, schema: z.ZodTypeAny) {
this.schemas[name] = zodToOpenApiSchema(schema);
return { $ref: `#/components/schemas/${name}` };
}
registerPath(pathRegistration: OpenApiPathRegistration) {
this.paths.push(pathRegistration);
}
buildPaths() {
const paths: Record<string, Record<string, unknown>> = {};
for (const { method, path, request, responses, ...operation } of this.paths) {
const normalizedOperation: Record<string, unknown> = {
...operation,
responses: normalizeResponses(responses),
};
if (request?.params) {
normalizedOperation.parameters = parametersFromSchema(request.params, "path");
}
if (request?.query) {
normalizedOperation.parameters = [
...((normalizedOperation.parameters as unknown[]) ?? []),
...parametersFromSchema(request.query, "query"),
];
}
if (request?.body) {
normalizedOperation.requestBody = {
...request.body,
content: normalizeContent(request.body.content),
};
}
paths[path] ??= {};
paths[path][method] = normalizedOperation;
}
return paths;
}
buildComponents() {
return { schemas: this.schemas };
}
}
const registry = new OpenAPIRegistry();
// ─── Common schemas ──────────────────────────────────────────────────────────
const ErrorSchema = registry.register(
"Error",
z.object({ error: z.string() }),
);
const responses = {
ok: (schema: z.ZodTypeAny = z.record(z.unknown())) => ({
description: "Success",
content: { "application/json": { schema } },
}),
noContent: { description: "No content" },
badRequest: {
description: "Bad request",
content: { "application/json": { schema: ErrorSchema } },
},
unauthorized: {
description: "Unauthorized",
content: { "application/json": { schema: ErrorSchema } },
},
forbidden: {
description: "Forbidden",
content: { "application/json": { schema: ErrorSchema } },
},
notFound: {
description: "Not found",
content: { "application/json": { schema: ErrorSchema } },
},
unprocessable: {
description: "Unprocessable entity",
content: { "application/json": { schema: ErrorSchema } },
},
serverError: {
description: "Internal server error",
content: { "application/json": { schema: ErrorSchema } },
},
};
const jsonBody = (schema: z.ZodTypeAny) => ({
content: { "application/json": { schema } },
required: true as const,
});
const r = responses;
function paramsSchemaFromPath(routePath: string): z.ZodObject<z.ZodRawShape> | undefined {
const names = [...routePath.matchAll(/\{([A-Za-z0-9_]+)\}/g)].map((match) => match[1]);
if (names.length === 0) return undefined;
const shape: z.ZodRawShape = {};
for (const name of names) {
shape[name] = z.string();
}
return z.object(shape);
}
function registerCurrentRoute(input: {
method: string;
path: string;
tags: string[];
summary: string;
query?: z.ZodTypeAny;
body?: z.ZodTypeAny;
responses?: Record<string, OpenApiResponse>;
}) {
const params = paramsSchemaFromPath(input.path);
const request = params || input.query || input.body
? {
...(params ? { params } : {}),
...(input.query ? { query: input.query } : {}),
...(input.body ? { body: jsonBody(input.body) } : {}),
}
: undefined;
registry.registerPath({
method: input.method,
path: input.path,
tags: input.tags,
summary: input.summary,
...(request ? { request } : {}),
responses: input.responses ?? { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
}
type OpenApiAuthLevel =
| "public"
| "authenticated"
| "board"
| "instance_admin";
const BOARD_SESSION_AUTH_SCHEME = "BoardSessionAuth";
const BOARD_API_KEY_AUTH_SCHEME = "BoardApiKeyAuth";
const AGENT_BEARER_AUTH_SCHEME = "AgentBearerAuth";
function securityRequirement(name: string): Record<string, string[]> {
return { [name]: [] };
}
const BOARD_SECURITY: Array<Record<string, string[]>> = [
securityRequirement(BOARD_SESSION_AUTH_SCHEME),
securityRequirement(BOARD_API_KEY_AUTH_SCHEME),
];
const AUTHENTICATED_SECURITY: Array<Record<string, string[]>> = [
...BOARD_SECURITY,
securityRequirement(AGENT_BEARER_AUTH_SCHEME),
];
const PUBLIC_OPERATIONS = new Set([
"GET /api/health",
"GET /api/openapi.json",
"GET /api/board-claim/{token}",
"POST /api/cli-auth/challenges",
"GET /api/cli-auth/challenges/{id}",
"POST /api/cli-auth/challenges/{id}/cancel",
"GET /api/invites/{token}",
"GET /api/invites/{token}/logo",
"GET /api/invites/{token}/onboarding",
"GET /api/invites/{token}/onboarding.txt",
"GET /api/invites/{token}/skills/index",
"GET /api/invites/{token}/skills/{skillName}",
"GET /api/invites/{token}/test-resolution",
"POST /api/invites/{token}/accept",
"POST /api/join-requests/{requestId}/claim-api-key",
]);
const BOARD_ONLY_PREFIXES = [
"/api/auth/",
"/api/admin/",
"/api/cloud-upstreams",
"/api/plugins",
"/api/instance/",
];
const BOARD_ONLY_OPERATIONS = new Set([
"GET /api/companies",
"POST /api/companies",
"GET /api/companies/stats",
"GET /api/companies/issues",
"POST /api/board-claim/{token}/claim",
"GET /api/cli-auth/me",
"POST /api/companies/{companyId}/invites",
"GET /api/companies/{companyId}/invites",
"POST /api/companies/{companyId}/openclaw/invite-prompt",
"GET /api/companies/{companyId}/join-requests",
"POST /api/companies/{companyId}/join-requests/{requestId}/approve",
"POST /api/companies/{companyId}/join-requests/{requestId}/reject",
"GET /api/companies/{companyId}/members",
"PATCH /api/companies/{companyId}/members/{memberId}",
"PATCH /api/companies/{companyId}/members/{memberId}/role-and-grants",
"POST /api/companies/{companyId}/members/{memberId}/archive",
"PATCH /api/companies/{companyId}/members/{memberId}/permissions",
"GET /api/companies/{companyId}/user-directory",
"GET /api/board-api-keys",
"POST /api/board-api-keys",
"DELETE /api/board-api-keys/{keyId}",
"POST /api/bootstrap/claim",
"GET /api/companies/{companyId}/resource-memberships/me",
"PUT /api/companies/{companyId}/resource-memberships/me/agents/{agentId}",
"PUT /api/companies/{companyId}/resource-memberships/me/projects/{projectId}",
"GET /api/companies/{companyId}/secret-provider-configs",
"POST /api/companies/{companyId}/secret-provider-configs",
"GET /api/companies/{companyId}/secret-providers/health",
"POST /api/companies/{companyId}/secret-provider-configs/discovery/preview",
"GET /api/secret-provider-configs/{id}",
"PATCH /api/secret-provider-configs/{id}",
"DELETE /api/secret-provider-configs/{id}",
"POST /api/secret-provider-configs/{id}/default",
"POST /api/secret-provider-configs/{id}/health",
"POST /api/companies/{companyId}/secrets/remote-import",
"POST /api/companies/{companyId}/secrets/remote-import/preview",
"GET /api/secrets/{id}/usage",
"GET /api/secrets/{id}/access-events",
"POST /api/health/dev-server/restart",
"POST /api/issues/{id}/interactions/{interactionId}/accept",
"POST /api/issues/{id}/interactions/{interactionId}/reject",
"POST /api/issues/{id}/interactions/{interactionId}/respond",
]);
const INSTANCE_ADMIN_OPERATIONS = new Set([
"POST /api/companies",
"POST /api/plugins/install",
"POST /api/instance/database-backups",
"POST /api/admin/users/{userId}/promote-instance-admin",
"POST /api/admin/users/{userId}/demote-instance-admin",
"PUT /api/admin/users/{userId}/company-access",
]);
const CREATED_OPERATIONS = new Set([
"POST /api/adapters/install",
"POST /api/companies/{companyId}/agent-hires",
"POST /api/companies/{companyId}/agents",
"POST /api/agents/{id}/keys",
"POST /api/companies/{companyId}/approvals",
"POST /api/approvals/{id}/comments",
"POST /api/companies/{companyId}/assets/images",
"POST /api/companies/{companyId}/logo",
"POST /api/cli-auth/challenges",
"POST /api/board-api-keys",
"POST /api/companies",
"POST /api/companies/{companyId}/invites",
"POST /api/companies/{companyId}/openclaw/invite-prompt",
"POST /api/companies/{companyId}/cost-events",
"POST /api/companies/{companyId}/finance-events",
"POST /api/companies/{companyId}/secret-provider-configs",
"POST /api/companies/{companyId}/environments",
"POST /api/companies/{companyId}/goals",
"POST /api/companies/{companyId}/labels",
"POST /api/issues/{id}/documents/{key}/annotations",
"POST /api/issues/{id}/documents/{key}/annotations/{threadId}/comments",
"POST /api/issues/{id}/work-products",
"POST /api/issues/{id}/low-trust/promotions",
"POST /api/issues/{id}/approvals",
"POST /api/companies/{companyId}/issues",
"POST /api/issues/{id}/children",
"POST /api/issues/{id}/interactions",
"POST /api/issues/{id}/comments",
"POST /api/companies/{companyId}/issues/{issueId}/attachments",
"POST /api/companies/{companyId}/projects",
"POST /api/projects/{id}/workspaces",
"POST /api/companies/{companyId}/routines",
"POST /api/routines/{id}/triggers",
"POST /api/companies/{companyId}/secrets",
"POST /api/companies/{companyId}/skills",
"POST /api/companies/{companyId}/skills/import",
"POST /api/join-requests/{requestId}/claim-api-key",
"POST /api/admin/users/{userId}/promote-instance-admin",
"POST /api/plugins/install",
"POST /api/instance/database-backups",
]);
const ACCEPTED_OPERATIONS = new Set([
"POST /api/companies/import",
"POST /api/health/dev-server/restart",
"POST /api/invites/{token}/accept",
]);
const FORBIDDEN_RESPONSE = {
description: "Forbidden",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
};
function operationKey(method: string, path: string) {
return `${method.toUpperCase()} ${path}`;
}
function isBoardOnlyOperation(method: string, path: string) {
const key = operationKey(method, path);
if (BOARD_ONLY_OPERATIONS.has(key)) return true;
return BOARD_ONLY_PREFIXES.some((prefix) => path.startsWith(prefix));
}
function resolveOperationAuthLevel(method: string, path: string): OpenApiAuthLevel {
const key = operationKey(method, path);
if (PUBLIC_OPERATIONS.has(key)) return "public";
if (INSTANCE_ADMIN_OPERATIONS.has(key)) return "instance_admin";
if (isBoardOnlyOperation(method, path)) return "board";
return "authenticated";
}
function applyOperationStatusOverride(
operation: Record<string, unknown>,
fromStatus: string,
toStatus: string,
) {
const responses = operation.responses as Record<string, unknown> | undefined;
if (!responses || !responses[fromStatus] || responses[toStatus]) return;
responses[toStatus] = responses[fromStatus];
delete responses[fromStatus];
}
function applyDocumentFixups(document: any): any {
document.components ??= {};
document.components.securitySchemes = {
[BOARD_SESSION_AUTH_SCHEME]: {
type: "apiKey",
in: "cookie",
name: "paperclip_session",
description:
"Board session cookie in authenticated mode. Paperclip uses Better Auth; cookie transport may vary by deployment.",
},
[BOARD_API_KEY_AUTH_SCHEME]: {
type: "http",
scheme: "bearer",
bearerFormat: "Board API Key",
description: "Board API key presented in the Authorization bearer header.",
},
[AGENT_BEARER_AUTH_SCHEME]: {
type: "http",
scheme: "bearer",
bearerFormat: "Agent API Key or Agent JWT",
description:
"Agent API key or Paperclip-issued local agent JWT presented in the Authorization bearer header.",
},
};
document.security = AUTHENTICATED_SECURITY;
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
for (const [method, operation] of Object.entries(pathItem as Record<string, any>)) {
const authLevel = resolveOperationAuthLevel(method, path);
if (authLevel === "public") {
operation.security = [];
} else if (authLevel === "authenticated") {
operation.security = AUTHENTICATED_SECURITY;
} else {
operation.security = BOARD_SECURITY;
}
operation["x-paperclip-authorization"] =
authLevel === "instance_admin"
? { actor: "board", instanceAdmin: true }
: authLevel === "board"
? { actor: "board" }
: authLevel === "authenticated"
? { actor: "board_or_agent" }
: { actor: "public" };
const key = operationKey(method, path);
if (authLevel !== "public") {
const responses = (operation.responses ??= {}) as Record<string, unknown>;
if (!responses["403"]) {
responses["403"] = FORBIDDEN_RESPONSE;
}
}
if (CREATED_OPERATIONS.has(key)) {
applyOperationStatusOverride(operation, "200", "201");
}
if (ACCEPTED_OPERATIONS.has(key)) {
applyOperationStatusOverride(operation, "200", "202");
}
}
}
return document;
}
// ─── Health ──────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/health",
tags: ["health"],
summary: "Health check",
responses: {
200: r.ok(z.object({
status: z.enum(["ok", "unhealthy"]),
version: z.string().optional(),
deploymentMode: z.string().optional(),
bootstrapStatus: z.enum(["ready", "bootstrap_pending"]).optional(),
bootstrapInviteActive: z.boolean().optional(),
})),
503: { description: "Service unavailable", content: { "application/json": { schema: ErrorSchema } } },
},
});
registry.registerPath({
method: "get",
path: "/api/openapi.json",
tags: ["health"],
summary: "Get the generated OpenAPI document",
responses: { 200: r.ok() },
});
// ─── Companies ───────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies",
tags: ["companies"],
summary: "List companies",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies",
tags: ["companies"],
summary: "Create a company",
request: { body: jsonBody(createCompanySchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/stats",
tags: ["companies"],
summary: "Company stats",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}",
tags: ["companies"],
summary: "Get a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/artifacts",
tags: ["companies"],
summary: "List company artifacts",
request: {
params: z.object({ companyId: z.string() }),
query: companyArtifactsQuerySchema,
},
responses: {
200: {
description: "Company artifact projection",
content: {
"application/json": {
schema: companyArtifactsResponseSchema,
},
},
},
401: r.unauthorized,
},
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}",
tags: ["companies"],
summary: "Update a company",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(updateCompanySchema.partial()),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}/branding",
tags: ["companies"],
summary: "Update company branding",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(updateCompanyBrandingSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/archive",
tags: ["companies"],
summary: "Archive a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "delete",
path: "/api/companies/{companyId}",
tags: ["companies"],
summary: "Delete a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/feedback-traces",
tags: ["companies"],
summary: "List company feedback traces",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/exports",
tags: ["companies"],
summary: "Export company data",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/exports/preview",
tags: ["companies"],
summary: "Preview company export",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/imports/preview",
tags: ["companies"],
summary: "Preview company import",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/imports/apply",
tags: ["companies"],
summary: "Apply company import",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Teams Catalog ──────────────────────────────────────────────────────────
for (const route of [
["get", "/api/teams/catalog", "List catalog teams"],
["get", "/api/teams/catalog/{catalogId}/files", "Get catalog team file"],
["get", "/api/teams/catalog/{catalogId}", "Get catalog team"],
["get", "/api/companies/{companyId}/teams/catalog/installed", "List installed catalog teams"],
["post", "/api/companies/{companyId}/teams/catalog/{catalogId}/preview", "Preview catalog team install"],
["post", "/api/companies/{companyId}/teams/catalog/{catalogId}/install", "Install catalog team"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["teams"],
summary: route[2],
});
}
// ─── Agents ──────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/agents",
tags: ["agents"],
summary: "List agents in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/agents",
tags: ["agents"],
summary: "Create an agent",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createAgentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/agent-hires",
tags: ["agents"],
summary: "Hire an agent",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createAgentHireSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/agent-configurations",
tags: ["agents"],
summary: "List agent configurations for a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/org",
tags: ["agents"],
summary: "Get org chart data",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/me",
tags: ["agents"],
summary: "Get the current agent",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/me/inbox-lite",
tags: ["agents"],
summary: "Get current agent inbox (lite)",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/me/inbox/mine",
tags: ["agents"],
summary: "Get current agent assigned inbox items",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}",
tags: ["agents"],
summary: "Get an agent",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/agents/{id}",
tags: ["agents"],
summary: "Update an agent",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateAgentSchema.omit({ permissions: true })),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "delete",
path: "/api/agents/{id}",
tags: ["agents"],
summary: "Delete an agent",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/agents/{id}/permissions",
tags: ["agents"],
summary: "Update agent permissions",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateAgentPermissionsSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/agents/{id}/instructions-path",
tags: ["agents"],
summary: "Update agent instructions path",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateAgentInstructionsPathSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/instructions-bundle",
tags: ["agents"],
summary: "Get agent instructions bundle",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/agents/{id}/instructions-bundle",
tags: ["agents"],
summary: "Update agent instructions bundle",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateAgentInstructionsBundleSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/instructions-bundle/file",
tags: ["agents"],
summary: "Get agent instructions file",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "put",
path: "/api/agents/{id}/instructions-bundle/file",
tags: ["agents"],
summary: "Upsert agent instructions file",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(upsertAgentInstructionsFileSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/agents/{id}/instructions-bundle/file",
tags: ["agents"],
summary: "Delete agent instructions file",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/configuration",
tags: ["agents"],
summary: "Get agent configuration",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/config-revisions",
tags: ["agents"],
summary: "List agent config revisions",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/config-revisions/{revisionId}",
tags: ["agents"],
summary: "Get an agent config revision",
request: { params: z.object({ id: z.string(), revisionId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/config-revisions/{revisionId}/rollback",
tags: ["agents"],
summary: "Roll back to a config revision",
request: { params: z.object({ id: z.string(), revisionId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/runtime-state",
tags: ["agents"],
summary: "Get agent runtime state",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/runtime-state/reset-session",
tags: ["agents"],
summary: "Reset agent session",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(resetAgentSessionSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/task-sessions",
tags: ["agents"],
summary: "List agent task sessions",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/skills",
tags: ["agents"],
summary: "List agent skills",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/skills/sync",
tags: ["agents"],
summary: "Sync desired skills onto an agent configuration",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(agentSkillSyncSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/agents/{id}/keys",
tags: ["agents"],
summary: "List agent API keys",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/keys",
tags: ["agents"],
summary: "Create an agent API key",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(createAgentKeySchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/agents/{id}/keys/{keyId}",
tags: ["agents"],
summary: "Delete an agent API key",
request: { params: z.object({ id: z.string(), keyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/wakeup",
tags: ["agents"],
summary: "Wake up an agent",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(wakeAgentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/pause",
tags: ["agents"],
summary: "Pause an agent",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/resume",
tags: ["agents"],
summary: "Resume an agent",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/terminate",
tags: ["agents"],
summary: "Terminate an agent",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/instance/scheduler-heartbeats",
tags: ["agents"],
summary: "List scheduler heartbeats",
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Adapters ────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/adapters/{type}/models",
tags: ["adapters"],
summary: "List models for an adapter type",
request: { params: z.object({ companyId: z.string(), type: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/adapters/{type}/detect-model",
tags: ["adapters"],
summary: "Detect active model for an adapter",
request: { params: z.object({ companyId: z.string(), type: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/adapters/{type}/test-environment",
tags: ["adapters"],
summary: "Validate adapter environment access for a company",
request: {
params: z.object({ companyId: z.string(), type: z.string() }),
body: jsonBody(testAdapterEnvironmentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Issues ──────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/issues",
tags: ["issues"],
summary: "List issues in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/issues",
tags: ["issues"],
summary: "Create an issue",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createIssueSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}",
tags: ["issues"],
summary: "Get an issue",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/issues/{id}",
tags: ["issues"],
summary: "Update an issue",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateIssueSchema.partial()),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "delete",
path: "/api/issues/{id}",
tags: ["issues"],
summary: "Delete an issue",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/heartbeat-context",
tags: ["issues"],
summary: "Get issue heartbeat context",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/work-products",
tags: ["issues"],
summary: "List issue work products",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/work-products",
tags: ["issues"],
summary: "Create an issue work product",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(createIssueWorkProductSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/work-products/{id}",
tags: ["issues"],
summary: "Update a work product",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateIssueWorkProductSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/work-products/{id}",
tags: ["issues"],
summary: "Delete a work product",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/documents",
tags: ["issues"],
summary: "List issue documents",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/documents/{key}",
tags: ["issues"],
summary: "Get an issue document",
request: { params: z.object({ id: z.string(), key: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "put",
path: "/api/issues/{id}/documents/{key}",
tags: ["issues"],
summary: "Upsert an issue document",
request: {
params: z.object({ id: z.string(), key: z.string() }),
body: jsonBody(upsertIssueDocumentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/issues/{id}/documents/{key}",
tags: ["issues"],
summary: "Delete an issue document",
request: { params: z.object({ id: z.string(), key: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/documents/{key}/revisions",
tags: ["issues"],
summary: "List issue document revisions",
request: { params: z.object({ id: z.string(), key: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/documents/{key}/revisions/{revisionId}/restore",
tags: ["issues"],
summary: "Restore a document revision",
request: {
params: z.object({ id: z.string(), key: z.string(), revisionId: z.string() }),
body: jsonBody(restoreIssueDocumentRevisionSchema),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/comments",
tags: ["issues"],
summary: "List issue comments",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/comments",
tags: ["issues"],
summary: "Add a comment to an issue",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(addIssueCommentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/issues/{id}/comments/{commentId}",
tags: ["issues"],
summary: "Delete an issue comment",
request: { params: z.object({ id: z.string(), commentId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/approvals",
tags: ["issues"],
summary: "List issue approvals",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/approvals",
tags: ["issues"],
summary: "Link an approval to an issue",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(linkIssueApprovalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/issues/{id}/approvals/{approvalId}",
tags: ["issues"],
summary: "Unlink an approval from an issue",
request: { params: z.object({ id: z.string(), approvalId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/checkout",
tags: ["issues"],
summary: "Check out an issue",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(checkoutIssueSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/release",
tags: ["issues"],
summary: "Release an issue",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/read",
tags: ["issues"],
summary: "Mark an issue as read",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/issues/{id}/read",
tags: ["issues"],
summary: "Mark an issue as unread",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/inbox-archive",
tags: ["issues"],
summary: "Archive issue from inbox",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/issues/{id}/inbox-archive",
tags: ["issues"],
summary: "Un-archive issue from inbox",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/feedback-votes",
tags: ["issues"],
summary: "List issue feedback votes",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/feedback-votes",
tags: ["issues"],
summary: "Upsert a feedback vote",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(upsertIssueFeedbackVoteSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/feedback-traces",
tags: ["issues"],
summary: "List issue feedback traces",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/feedback-traces/{traceId}",
tags: ["issues"],
summary: "Get a feedback trace",
request: { params: z.object({ traceId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/feedback-traces/{traceId}/bundle",
tags: ["issues"],
summary: "Get a feedback trace bundle",
request: { params: z.object({ traceId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/attachments",
tags: ["issues"],
summary: "List issue attachments",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/labels",
tags: ["issues"],
summary: "List labels in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/labels",
tags: ["issues"],
summary: "Create a label",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createIssueLabelSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/labels/{labelId}",
tags: ["issues"],
summary: "Delete a label",
request: { params: z.object({ labelId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Projects ────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/projects",
tags: ["projects"],
summary: "List projects in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/projects",
tags: ["projects"],
summary: "Create a project",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createProjectSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/projects/{id}",
tags: ["projects"],
summary: "Get a project",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/projects/{id}",
tags: ["projects"],
summary: "Update a project",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateProjectSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/projects/{id}",
tags: ["projects"],
summary: "Delete a project",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/projects/{id}/workspaces",
tags: ["projects"],
summary: "List project workspaces",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/projects/{id}/workspaces",
tags: ["projects"],
summary: "Create a project workspace",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(createProjectWorkspaceSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/projects/{id}/workspaces/{workspaceId}",
tags: ["projects"],
summary: "Update a project workspace",
request: {
params: z.object({ id: z.string(), workspaceId: z.string() }),
body: jsonBody(updateProjectWorkspaceSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "delete",
path: "/api/projects/{id}/workspaces/{workspaceId}",
tags: ["projects"],
summary: "Delete a project workspace",
request: { params: z.object({ id: z.string(), workspaceId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Routines ────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/routines",
tags: ["routines"],
summary: "List routines in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/routines",
tags: ["routines"],
summary: "Create a routine",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createRoutineSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/routines/{id}",
tags: ["routines"],
summary: "Get a routine",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/routines/{id}",
tags: ["routines"],
summary: "Update a routine",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateRoutineSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/routines/{id}/runs",
tags: ["routines"],
summary: "List runs for a routine",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/routines/{id}/run",
tags: ["routines"],
summary: "Manually run a routine",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(runRoutineSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/routines/{id}/triggers",
tags: ["routines"],
summary: "Create a routine trigger",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(createRoutineTriggerSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/routine-triggers/{id}",
tags: ["routines"],
summary: "Update a routine trigger",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateRoutineTriggerSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/routine-triggers/{id}",
tags: ["routines"],
summary: "Delete a routine trigger",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/routine-triggers/{id}/rotate-secret",
tags: ["routines"],
summary: "Rotate a routine trigger secret",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(rotateRoutineTriggerSecretSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/routine-triggers/public/{publicId}/fire",
tags: ["routines"],
summary: "Fire a public routine trigger",
request: { params: z.object({ publicId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
// ─── Goals ───────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/goals",
tags: ["goals"],
summary: "List goals in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/goals",
tags: ["goals"],
summary: "Create a goal",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createGoalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/goals/{id}",
tags: ["goals"],
summary: "Get a goal",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/goals/{id}",
tags: ["goals"],
summary: "Update a goal",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateGoalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/goals/{id}",
tags: ["goals"],
summary: "Delete a goal",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Secrets ─────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/secret-providers",
tags: ["secrets"],
summary: "List secret providers",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/secrets",
tags: ["secrets"],
summary: "List secrets in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/secrets",
tags: ["secrets"],
summary: "Create a secret",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createSecretSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/secrets/{id}",
tags: ["secrets"],
summary: "Update a secret",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateSecretSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/secrets/{id}/rotate",
tags: ["secrets"],
summary: "Rotate a secret",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(rotateSecretSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/secrets/{id}",
tags: ["secrets"],
summary: "Delete a secret",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Approvals ───────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/approvals",
tags: ["approvals"],
summary: "List approvals in a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/approvals",
tags: ["approvals"],
summary: "Create an approval",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createApprovalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/approvals/{id}",
tags: ["approvals"],
summary: "Get an approval",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/approvals/{id}/issues",
tags: ["approvals"],
summary: "List issues linked to an approval",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/approvals/{id}/approve",
tags: ["approvals"],
summary: "Approve an approval",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(resolveApprovalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/approvals/{id}/reject",
tags: ["approvals"],
summary: "Reject an approval",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(resolveApprovalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/approvals/{id}/request-revision",
tags: ["approvals"],
summary: "Request revision on an approval",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(requestApprovalRevisionSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/approvals/{id}/resubmit",
tags: ["approvals"],
summary: "Resubmit an approval",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(resubmitApprovalSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/approvals/{id}/comments",
tags: ["approvals"],
summary: "List approval comments",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/approvals/{id}/comments",
tags: ["approvals"],
summary: "Add a comment to an approval",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(addApprovalCommentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Costs ───────────────────────────────────────────────────────────────────
const costSummaryPaths = [
"summary", "by-agent", "by-agent-model", "by-provider",
"by-biller", "by-project", "finance-summary", "finance-by-biller",
"finance-by-kind", "finance-events", "window-spend", "quota-windows",
] as const;
for (const segment of costSummaryPaths) {
registry.registerPath({
method: "get",
path: `/api/companies/{companyId}/costs/${segment}`,
tags: ["costs"],
summary: `Cost report: ${segment}`,
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
}
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/cost-events",
tags: ["costs"],
summary: "Record a cost event",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createCostEventSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/finance-events",
tags: ["costs"],
summary: "Record a finance event",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createFinanceEventSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/budgets/policies",
tags: ["costs"],
summary: "Create or update a budget policy",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(upsertBudgetPolicySchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/budget-incidents/{incidentId}/resolve",
tags: ["costs"],
summary: "Resolve a budget incident",
request: {
params: z.object({ companyId: z.string(), incidentId: z.string() }),
body: jsonBody(resolveBudgetIncidentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/budgets/overview",
tags: ["costs"],
summary: "Get budget overview",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}/budgets",
tags: ["costs"],
summary: "Update company budget",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(updateBudgetSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/agents/{agentId}/budgets",
tags: ["costs"],
summary: "Update agent budget",
request: {
params: z.object({ agentId: z.string() }),
body: jsonBody(updateBudgetSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Activity ────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/activity",
tags: ["activity"],
summary: "List company activity",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/activity",
tags: ["activity"],
summary: "Create an activity entry",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(z.object({
actorType: z.enum(["agent", "user", "system", "plugin"]).optional(),
actorId: z.string().min(1),
action: z.string().min(1),
entityType: z.string().min(1),
entityId: z.string().min(1),
agentId: z.string().uuid().optional().nullable(),
details: z.record(z.unknown()).optional().nullable(),
})),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/activity",
tags: ["activity"],
summary: "List activity for an issue",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/runs",
tags: ["activity"],
summary: "List runs for an issue",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/heartbeat-runs/{runId}/issues",
tags: ["activity"],
summary: "List issues for a heartbeat run",
request: { params: z.object({ runId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Dashboard ───────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/dashboard",
tags: ["dashboard"],
summary: "Get dashboard data",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Sidebar ─────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/sidebar-badges",
tags: ["sidebar"],
summary: "Get sidebar badge counts",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/sidebar-preferences/me",
tags: ["sidebar"],
summary: "Get current user sidebar preferences",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "put",
path: "/api/sidebar-preferences/me",
tags: ["sidebar"],
summary: "Update current user sidebar preferences",
request: { body: jsonBody(upsertSidebarOrderPreferenceSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/sidebar-preferences/me",
tags: ["sidebar"],
summary: "Get sidebar preferences for company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "put",
path: "/api/companies/{companyId}/sidebar-preferences/me",
tags: ["sidebar"],
summary: "Update sidebar preferences for company",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(upsertSidebarOrderPreferenceSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Inbox dismissals ────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/inbox-dismissals",
tags: ["inbox"],
summary: "List inbox dismissals",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/inbox-dismissals",
tags: ["inbox"],
summary: "Create an inbox dismissal",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(z.object({
itemKey: z.string().trim().min(1).regex(/^(approval|join|run):.+$/, "Unsupported inbox item key"),
})),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Instance settings ────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/instance/settings/general",
tags: ["instance"],
summary: "Get general instance settings",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/instance/settings/general",
tags: ["instance"],
summary: "Update general instance settings",
request: { body: jsonBody(patchInstanceGeneralSettingsSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/instance/settings/experimental",
tags: ["instance"],
summary: "Get experimental instance settings",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/instance/settings/experimental",
tags: ["instance"],
summary: "Update experimental instance settings",
request: { body: jsonBody(patchInstanceExperimentalSettingsSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Access / invites / members ───────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/invites",
tags: ["access"],
summary: "List company invites",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/invites",
tags: ["access"],
summary: "Create a company invite",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createCompanyInviteSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/join-requests",
tags: ["access"],
summary: "List company join requests",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/join-requests/{requestId}/approve",
tags: ["access"],
summary: "Approve a company join request",
request: { params: z.object({ companyId: z.string(), requestId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/join-requests/{requestId}/reject",
tags: ["access"],
summary: "Reject a company join request",
request: { params: z.object({ companyId: z.string(), requestId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/invites/{inviteId}/revoke",
tags: ["access"],
summary: "Revoke an invite",
request: { params: z.object({ inviteId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/invites/{token}",
tags: ["access"],
summary: "Get an invite by token",
request: { params: z.object({ token: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/invites/{token}/accept",
tags: ["access"],
summary: "Accept an invite and create or replay a join request",
request: {
params: z.object({ token: z.string() }),
body: jsonBody(acceptInviteSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/members",
tags: ["access"],
summary: "List company members",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}/members/{memberId}",
tags: ["access"],
summary: "Update a company member status or role",
request: {
params: z.object({ companyId: z.string(), memberId: z.string() }),
body: jsonBody(updateCompanyMemberSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}/members/{memberId}/role-and-grants",
tags: ["access"],
summary: "Update a company member role and explicit grants",
request: {
params: z.object({ companyId: z.string(), memberId: z.string() }),
body: jsonBody(updateCompanyMemberWithPermissionsSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/members/{memberId}/archive",
tags: ["access"],
summary: "Archive a company member",
request: {
params: z.object({ companyId: z.string(), memberId: z.string() }),
body: jsonBody(archiveCompanyMemberSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}/members/{memberId}/permissions",
tags: ["access"],
summary: "Update explicit company member permissions",
request: {
params: z.object({ companyId: z.string(), memberId: z.string() }),
body: jsonBody(updateMemberPermissionsSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/user-directory",
tags: ["access"],
summary: "Get company user directory",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/cli-auth/me",
tags: ["access"],
summary: "Get current CLI auth session",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/openclaw/invite-prompt",
tags: ["access"],
summary: "Create an OpenClaw invite prompt bundle",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createOpenClawInvitePromptSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/cli-auth/challenges",
tags: ["access"],
summary: "Create a CLI auth challenge",
request: { body: jsonBody(createCliAuthChallengeSchema) },
responses: { 200: r.ok(), 400: r.badRequest },
});
registry.registerPath({
method: "post",
path: "/api/cli-auth/challenges/{id}/approve",
tags: ["access"],
summary: "Approve a CLI auth challenge",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(resolveCliAuthChallengeSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/cli-auth/challenges/{id}/cancel",
tags: ["access"],
summary: "Cancel a CLI auth challenge",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(resolveCliAuthChallengeSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/cli-auth/revoke-current",
tags: ["access"],
summary: "Revoke current CLI auth session",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/skills/available",
tags: ["access"],
summary: "List available skills",
responses: { 200: r.ok() },
});
registry.registerPath({
method: "get",
path: "/api/skills/index",
tags: ["access"],
summary: "Get skills index",
responses: { 200: r.ok() },
});
registry.registerPath({
method: "get",
path: "/api/skills/{skillName}",
tags: ["access"],
summary: "Get a skill by name",
request: { params: z.object({ skillName: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/join-requests/{requestId}/claim-api-key",
tags: ["access"],
summary: "Claim the initial API key for an approved agent join request",
request: {
params: z.object({ requestId: z.string() }),
body: jsonBody(claimJoinRequestApiKeySchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 403: r.forbidden, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/admin/users",
tags: ["admin"],
summary: "List all users (admin)",
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
});
// ─── Auth / profile ──────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/auth/get-session",
tags: ["auth"],
summary: "Get current session",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/auth/profile",
tags: ["auth"],
summary: "Get current user profile",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/auth/profile",
tags: ["auth"],
summary: "Update current user profile",
request: { body: jsonBody(updateCurrentUserProfileSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/users/{userSlug}/profile",
tags: ["auth"],
summary: "Get a user profile within a company",
request: { params: z.object({ companyId: z.string(), userSlug: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
// ─── Heartbeat runs ──────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/heartbeat-runs",
tags: ["runs"],
summary: "List heartbeat runs for a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/live-runs",
tags: ["runs"],
summary: "List live runs for a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{issueId}/live-runs",
tags: ["runs"],
summary: "List live runs for an issue",
request: { params: z.object({ issueId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{issueId}/active-run",
tags: ["runs"],
summary: "Get active run for an issue",
request: { params: z.object({ issueId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/heartbeat-runs/{runId}",
tags: ["runs"],
summary: "Get a heartbeat run",
request: { params: z.object({ runId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/heartbeat-runs/{runId}/cancel",
tags: ["runs"],
summary: "Cancel a heartbeat run",
request: { params: z.object({ runId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/heartbeat-runs/{runId}/watchdog-decisions",
tags: ["runs"],
summary: "Submit watchdog decisions for a run",
request: {
params: z.object({ runId: z.string() }),
body: jsonBody(z.object({
decision: z.enum(["snooze", "continue", "dismissed_false_positive"]),
evaluationIssueId: z.string().optional().nullable(),
reason: z.string().optional().nullable(),
snoozedUntil: z.string().datetime().optional().nullable(),
})),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/heartbeat-runs/{runId}/events",
tags: ["runs"],
summary: "Get events for a heartbeat run",
request: { params: z.object({ runId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/heartbeat-runs/{runId}/log",
tags: ["runs"],
summary: "Get log for a heartbeat run",
request: { params: z.object({ runId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/heartbeat-runs/{runId}/workspace-operations",
tags: ["runs"],
summary: "List workspace operations for a run",
request: { params: z.object({ runId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/workspace-operations/{operationId}/log",
tags: ["runs"],
summary: "Get log for a workspace operation",
request: { params: z.object({ operationId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Agent runs & heartbeat ───────────────────────────────────────────────────
registry.registerPath({
method: "post",
path: "/api/agents/{id}/approve",
tags: ["agents"],
summary: "Approve a pending agent action",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/heartbeat/invoke",
tags: ["agents"],
summary: "Invoke agent heartbeat",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/agents/{id}/claude-login",
tags: ["agents"],
summary: "Trigger Claude login for agent",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Issue interactions & tree ───────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/issues/{id}/interactions",
tags: ["issues"],
summary: "List issue thread interactions",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/interactions",
tags: ["issues"],
summary: "Create an issue thread interaction",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(createIssueThreadInteractionSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/interactions/{interactionId}/accept",
tags: ["issues"],
summary: "Accept an issue thread interaction",
request: {
params: z.object({ id: z.string(), interactionId: z.string() }),
body: jsonBody(acceptIssueThreadInteractionSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/interactions/{interactionId}/reject",
tags: ["issues"],
summary: "Reject an issue thread interaction",
request: {
params: z.object({ id: z.string(), interactionId: z.string() }),
body: jsonBody(rejectIssueThreadInteractionSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/interactions/{interactionId}/respond",
tags: ["issues"],
summary: "Answer questions on an issue thread interaction",
request: {
params: z.object({ id: z.string(), interactionId: z.string() }),
body: jsonBody(respondIssueThreadInteractionSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/children",
tags: ["issues"],
summary: "Create child issues",
request: { params: z.object({ id: z.string() }), body: jsonBody(createChildIssueSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/admin/force-release",
tags: ["issues"],
summary: "Force-release an issue (admin)",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/tree-control/state",
tags: ["issues"],
summary: "Get issue tree control state",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/tree-control/preview",
tags: ["issues"],
summary: "Preview issue tree control changes",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(previewIssueTreeControlSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/tree-holds",
tags: ["issues"],
summary: "List issue tree holds",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/tree-holds",
tags: ["issues"],
summary: "Create an issue tree hold",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(createIssueTreeHoldSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/tree-holds/{holdId}",
tags: ["issues"],
summary: "Get an issue tree hold",
request: { params: z.object({ id: z.string(), holdId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/issues/{id}/tree-holds/{holdId}/release",
tags: ["issues"],
summary: "Release an issue tree hold",
request: {
params: z.object({ id: z.string(), holdId: z.string() }),
body: jsonBody(releaseIssueTreeHoldSchema),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Attachments ──────────────────────────────────────────────────────────────
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/issues/{issueId}/attachments",
tags: ["assets"],
summary: "Upload an attachment to an issue",
request: { params: z.object({ companyId: z.string(), issueId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/attachments/{attachmentId}/content",
tags: ["assets"],
summary: "Download attachment content",
request: { params: z.object({ attachmentId: z.string() }) },
responses: { 200: { description: "File content" }, 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "delete",
path: "/api/attachments/{attachmentId}",
tags: ["assets"],
summary: "Delete an attachment",
request: { params: z.object({ attachmentId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Assets ──────────────────────────────────────────────────────────────────
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/assets/images",
tags: ["assets"],
summary: "Upload an image asset",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/logo",
tags: ["assets"],
summary: "Upload company logo",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/assets/{assetId}/content",
tags: ["assets"],
summary: "Download asset content",
request: { params: z.object({ assetId: z.string() }) },
responses: { 200: { description: "File content" }, 401: r.unauthorized, 404: r.notFound },
});
// ─── Company skills ───────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/skills",
tags: ["skills"],
summary: "List skills for a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/skills/{skillId}",
tags: ["skills"],
summary: "Get a company skill",
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/skills/{skillId}/update-status",
tags: ["skills"],
summary: "Get skill update status",
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/skills/{skillId}/files",
tags: ["skills"],
summary: "List skill files",
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/skills",
tags: ["skills"],
summary: "Create a company skill",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(companySkillCreateSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/companies/{companyId}/skills/{skillId}/files",
tags: ["skills"],
summary: "Update a skill file",
request: {
params: z.object({ companyId: z.string(), skillId: z.string() }),
body: jsonBody(companySkillFileUpdateSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/skills/import",
tags: ["skills"],
summary: "Import a skill",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(companySkillImportSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/skills/scan-projects",
tags: ["skills"],
summary: "Scan project for skills",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(companySkillProjectScanRequestSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/skills/{skillId}/install-update",
tags: ["skills"],
summary: "Install a skill update",
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/companies/{companyId}/skills/{skillId}",
tags: ["skills"],
summary: "Delete a company skill",
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Execution workspaces ─────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/execution-workspaces",
tags: ["execution-workspaces"],
summary: "List execution workspaces for a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/execution-workspaces/{id}",
tags: ["execution-workspaces"],
summary: "Get an execution workspace",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/execution-workspaces/{id}/close-readiness",
tags: ["execution-workspaces"],
summary: "Check close-readiness of a workspace",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/execution-workspaces/{id}/workspace-operations",
tags: ["execution-workspaces"],
summary: "List workspace operations",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/execution-workspaces/{id}",
tags: ["execution-workspaces"],
summary: "Update an execution workspace",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateExecutionWorkspaceSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/execution-workspaces/{id}/runtime-services/{action}",
tags: ["execution-workspaces"],
summary: "Control a runtime service in a workspace",
request: {
params: z.object({ id: z.string(), action: z.string() }),
body: jsonBody(workspaceRuntimeControlTargetSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/execution-workspaces/{id}/runtime-commands/{action}",
tags: ["execution-workspaces"],
summary: "Run a runtime command in a workspace",
request: {
params: z.object({ id: z.string(), action: z.string() }),
body: jsonBody(workspaceRuntimeControlTargetSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Environments ─────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/environments",
tags: ["environments"],
summary: "List environments for a company",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/environments/capabilities",
tags: ["environments"],
summary: "Get environment capabilities",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/environments",
tags: ["environments"],
summary: "Create an environment",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(createEnvironmentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/environments/{id}",
tags: ["environments"],
summary: "Get an environment",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/environments/{id}/leases",
tags: ["environments"],
summary: "List leases for an environment",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/environment-leases/{leaseId}",
tags: ["environments"],
summary: "Get an environment lease",
request: { params: z.object({ leaseId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "patch",
path: "/api/environments/{id}",
tags: ["environments"],
summary: "Update an environment",
request: {
params: z.object({ id: z.string() }),
body: jsonBody(updateEnvironmentSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/environments/{id}",
tags: ["environments"],
summary: "Delete an environment",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/environments/{id}/probe",
tags: ["environments"],
summary: "Probe an environment",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/environments/probe-config",
tags: ["environments"],
summary: "Probe environment config",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(probeEnvironmentConfigSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Adapters (full) ──────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/adapters",
tags: ["adapters"],
summary: "List all adapters",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/adapters/install",
tags: ["adapters"],
summary: "Install an adapter",
request: {
body: jsonBody(z.object({
packageName: z.string(),
isLocalPath: z.boolean().optional(),
version: z.string().optional(),
})),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/adapters/{type}",
tags: ["adapters"],
summary: "Enable or disable an adapter",
request: {
params: z.object({ type: z.string() }),
body: jsonBody(z.object({ disabled: z.boolean() })),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "patch",
path: "/api/adapters/{type}/override",
tags: ["adapters"],
summary: "Pause or resume an adapter's override of a builtin",
request: {
params: z.object({ type: z.string() }),
body: jsonBody(z.object({ paused: z.boolean() })),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "delete",
path: "/api/adapters/{type}",
tags: ["adapters"],
summary: "Delete an adapter",
request: { params: z.object({ type: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/adapters/{type}/reload",
tags: ["adapters"],
summary: "Reload an adapter",
request: { params: z.object({ type: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/adapters/{type}/reinstall",
tags: ["adapters"],
summary: "Reinstall an adapter",
request: { params: z.object({ type: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/adapters/{type}/config-schema",
tags: ["adapters"],
summary: "Get adapter config schema",
request: { params: z.object({ type: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Plugins ──────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/plugins",
tags: ["plugins"],
summary: "List installed plugins",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/examples",
tags: ["plugins"],
summary: "List example plugins",
responses: { 200: r.ok() },
});
registry.registerPath({
method: "get",
path: "/api/plugins/ui-contributions",
tags: ["plugins"],
summary: "List plugin UI contributions",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/tools",
tags: ["plugins"],
summary: "List plugin tools",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/tools/execute",
tags: ["plugins"],
summary: "Execute a plugin tool",
request: {
body: jsonBody(z.object({
tool: z.string(),
parameters: z.record(z.unknown()).optional(),
runContext: z.object({
agentId: z.string(),
runId: z.string(),
companyId: z.string(),
projectId: z.string(),
}),
})),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/install",
tags: ["plugins"],
summary: "Install a plugin",
request: {
body: jsonBody(z.object({
packageName: z.string(),
version: z.string().optional(),
isLocalPath: z.boolean().optional(),
})),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}",
tags: ["plugins"],
summary: "Get a plugin",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "delete",
path: "/api/plugins/{pluginId}",
tags: ["plugins"],
summary: "Delete a plugin",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/enable",
tags: ["plugins"],
summary: "Enable a plugin",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/disable",
tags: ["plugins"],
summary: "Disable a plugin",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/health",
tags: ["plugins"],
summary: "Get plugin health",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/logs",
tags: ["plugins"],
summary: "Get plugin logs",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/upgrade",
tags: ["plugins"],
summary: "Upgrade a plugin",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/config",
tags: ["plugins"],
summary: "Get plugin config",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/config",
tags: ["plugins"],
summary: "Set plugin config",
request: {
params: z.object({ pluginId: z.string() }),
body: jsonBody(z.object({ configJson: z.record(z.unknown()) })),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/config/test",
tags: ["plugins"],
summary: "Test plugin config",
request: {
params: z.object({ pluginId: z.string() }),
body: jsonBody(z.object({ configJson: z.record(z.unknown()) })),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/jobs",
tags: ["plugins"],
summary: "List plugin jobs",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/jobs/{jobId}/runs",
tags: ["plugins"],
summary: "List runs for a plugin job",
request: { params: z.object({ pluginId: z.string(), jobId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/jobs/{jobId}/trigger",
tags: ["plugins"],
summary: "Trigger a plugin job",
request: { params: z.object({ pluginId: z.string(), jobId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/webhooks/{endpointKey}",
tags: ["plugins"],
summary: "Deliver an external webhook payload to a plugin",
request: {
params: z.object({ pluginId: z.string(), endpointKey: z.string() }),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/dashboard",
tags: ["plugins"],
summary: "Get plugin dashboard data",
request: { params: z.object({ pluginId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/bridge/data",
tags: ["plugins"],
summary: "Send data via plugin bridge",
request: {
params: z.object({ pluginId: z.string() }),
body: jsonBody(z.object({
key: z.string(),
companyId: z.string().optional(),
params: z.record(z.unknown()).optional(),
})),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/bridge/action",
tags: ["plugins"],
summary: "Send action via plugin bridge",
request: {
params: z.object({ pluginId: z.string() }),
body: jsonBody(z.object({
key: z.string(),
companyId: z.string().optional(),
params: z.record(z.unknown()).optional(),
})),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/data/{key}",
tags: ["plugins"],
summary: "Get plugin data by key (URL-keyed bridge)",
request: {
params: z.object({ pluginId: z.string(), key: z.string() }),
body: jsonBody(z.object({
companyId: z.string().optional(),
params: z.record(z.unknown()).optional(),
})),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/plugins/{pluginId}/actions/{key}",
tags: ["plugins"],
summary: "Invoke a plugin action (URL-keyed bridge)",
request: {
params: z.object({ pluginId: z.string(), key: z.string() }),
body: jsonBody(z.object({
companyId: z.string().optional(),
params: z.record(z.unknown()).optional(),
})),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
// ─── Instance database backups ────────────────────────────────────────────────
registry.registerPath({
method: "post",
path: "/api/instance/database-backups",
tags: ["instance"],
summary: "Trigger a database backup",
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
});
// ─── LLM text endpoints ───────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/llms/agent-configuration.txt",
tags: ["llms"],
summary: "Get agent configuration as plain text (for LLM context)",
responses: { 200: { description: "Plain text agent configuration" }, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/llms/agent-configuration/{adapterType}.txt",
tags: ["llms"],
summary: "Get agent configuration for a specific adapter type",
request: { params: z.object({ adapterType: z.string() }) },
responses: { 200: { description: "Plain text agent configuration" }, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/llms/agent-icons.txt",
tags: ["llms"],
summary: "Get agent icon names as plain text",
responses: { 200: { description: "Plain text icon list" }, 401: r.unauthorized },
});
// ─── Issues (legacy / misc) ───────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/issues",
tags: ["issues"],
summary: "Legacy — returns error directing to /api/companies/{companyId}/issues",
responses: { 400: r.badRequest },
});
registry.registerPath({
method: "get",
path: "/api/issues/{id}/comments/{commentId}",
tags: ["issues"],
summary: "Get a single issue comment",
request: { params: z.object({ id: z.string(), commentId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
// ─── Org chart images ─────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/org.svg",
tags: ["companies"],
summary: "Get org chart as SVG",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: { description: "SVG image" }, 401: r.unauthorized },
});
registry.registerPath({
method: "get",
path: "/api/companies/{companyId}/org.png",
tags: ["companies"],
summary: "Get org chart as PNG",
request: { params: z.object({ companyId: z.string() }) },
responses: { 200: { description: "PNG image" }, 401: r.unauthorized },
});
// ─── Company portability (legacy routes) ─────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/companies/issues",
tags: ["companies"],
summary: "Legacy — returns error directing to correct issues path",
responses: { 400: r.badRequest },
});
registry.registerPath({
method: "post",
path: "/api/companies/{companyId}/export",
tags: ["companies"],
summary: "Export a company (legacy singular form)",
request: {
params: z.object({ companyId: z.string() }),
body: jsonBody(companyPortabilityExportSchema),
},
responses: { 200: r.ok(), 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/import/preview",
tags: ["companies"],
summary: "Preview a company import (legacy route)",
request: { body: jsonBody(companyPortabilityPreviewSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/companies/import",
tags: ["companies"],
summary: "Apply a company import (legacy route)",
request: { body: jsonBody(companyPortabilityImportSchema) },
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Board claim & CLI auth ───────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/board-claim/{token}",
tags: ["access"],
summary: "Get board claim details by token",
request: { params: z.object({ token: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/board-claim/{token}/claim",
tags: ["access"],
summary: "Claim a board token",
request: { params: z.object({ token: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/cli-auth/challenges/{id}",
tags: ["access"],
summary: "Get a CLI auth challenge",
request: { params: z.object({ id: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
// ─── Invite onboarding ────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/invites/{token}/logo",
tags: ["access"],
summary: "Get company logo for an invite",
request: { params: z.object({ token: z.string() }) },
responses: { 200: { description: "Image file" }, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/invites/{token}/onboarding",
tags: ["access"],
summary: "Get onboarding data for an invite",
request: { params: z.object({ token: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/invites/{token}/onboarding.txt",
tags: ["access"],
summary: "Get onboarding instructions as plain text",
request: { params: z.object({ token: z.string() }) },
responses: { 200: { description: "Plain text onboarding instructions" }, 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/invites/{token}/skills/index",
tags: ["access"],
summary: "Get skills index for an invite",
request: { params: z.object({ token: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/invites/{token}/skills/{skillName}",
tags: ["access"],
summary: "Get a skill by name for an invite",
request: { params: z.object({ token: z.string(), skillName: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
registry.registerPath({
method: "get",
path: "/api/invites/{token}/test-resolution",
tags: ["access"],
summary: "Test invite token resolution",
request: { params: z.object({ token: z.string() }) },
responses: { 200: r.ok(), 404: r.notFound },
});
// ─── Admin ────────────────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/admin/users/{userId}/company-access",
tags: ["admin"],
summary: "Get company access for a user (admin)",
request: { params: z.object({ userId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
});
registry.registerPath({
method: "put",
path: "/api/admin/users/{userId}/company-access",
tags: ["admin"],
summary: "Set company access for a user (admin)",
request: {
params: z.object({ userId: z.string() }),
body: jsonBody(updateUserCompanyAccessSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
});
registry.registerPath({
method: "post",
path: "/api/admin/users/{userId}/promote-instance-admin",
tags: ["admin"],
summary: "Promote a user to instance admin",
request: { params: z.object({ userId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
});
registry.registerPath({
method: "post",
path: "/api/admin/users/{userId}/demote-instance-admin",
tags: ["admin"],
summary: "Demote a user from instance admin",
request: { params: z.object({ userId: z.string() }) },
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
});
// ─── Project workspace runtime ────────────────────────────────────────────────
registry.registerPath({
method: "post",
path: "/api/projects/{id}/workspaces/{workspaceId}/runtime-services/{action}",
tags: ["projects"],
summary: "Control a runtime service in a project workspace",
request: {
params: z.object({ id: z.string(), workspaceId: z.string(), action: z.string() }),
body: jsonBody(workspaceRuntimeControlTargetSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registry.registerPath({
method: "post",
path: "/api/projects/{id}/workspaces/{workspaceId}/runtime-commands/{action}",
tags: ["projects"],
summary: "Run a runtime command in a project workspace",
request: {
params: z.object({ id: z.string(), workspaceId: z.string(), action: z.string() }),
body: jsonBody(workspaceRuntimeControlTargetSchema),
},
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
// ─── Plugin bridge stream ─────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/plugins/{pluginId}/bridge/stream/{channel}",
tags: ["plugins"],
summary: "Subscribe to a plugin bridge SSE stream",
request: { params: z.object({ pluginId: z.string(), channel: z.string() }) },
responses: {
200: { description: "Server-sent event stream (text/event-stream)" },
401: r.unauthorized,
},
});
// ─── Plugin UI static ─────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/_plugins/{pluginId}/ui/{filePath}",
tags: ["plugins"],
summary: "Serve plugin UI static file",
request: { params: z.object({ pluginId: z.string(), filePath: z.string() }) },
responses: { 200: { description: "Static file content" }, 404: r.notFound },
});
// ─── Adapter UI parser ────────────────────────────────────────────────────────
registry.registerPath({
method: "get",
path: "/api/adapters/{type}/ui-parser.js",
tags: ["adapters"],
summary: "Get adapter UI parser script",
request: { params: z.object({ type: z.string() }) },
responses: { 200: { description: "JavaScript file" }, 404: r.notFound },
});
// ─── Current route coverage ─────────────────────────────────────────────────
registerCurrentRoute({
method: "get",
path: "/api/adapters/{type}",
tags: ["adapters"],
summary: "Get adapter registration details",
});
registerCurrentRoute({
method: "get",
path: "/api/companies/{companyId}/adapters/{type}/model-profiles",
tags: ["adapters"],
summary: "List adapter model profiles for a company",
});
registerCurrentRoute({
method: "post",
path: "/api/health/dev-server/restart",
tags: ["health"],
summary: "Request a managed dev-server restart",
responses: { 202: r.ok(), 403: r.forbidden, 404: r.notFound, 409: { description: "Restart is not required" } },
});
registerCurrentRoute({
method: "post",
path: "/api/bootstrap/claim",
tags: ["access"],
summary: "Claim first instance admin from a browser session",
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound, 409: { description: "Instance admin already claimed" } },
});
registerCurrentRoute({
method: "get",
path: "/api/board-api-keys",
tags: ["access"],
summary: "List board API keys",
responses: { 200: r.ok(), 401: r.unauthorized },
});
registerCurrentRoute({
method: "post",
path: "/api/board-api-keys",
tags: ["access"],
summary: "Create a named board API key",
body: createBoardApiKeySchema,
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized },
});
registerCurrentRoute({
method: "delete",
path: "/api/board-api-keys/{keyId}",
tags: ["access"],
summary: "Revoke a board API key",
});
for (const route of [
["get", "/api/companies/import/jobs/{jobId}", "Get company import job status"],
["get", "/api/companies/{companyId}/search", "Search company data"],
["get", "/api/companies/{companyId}/issues/count", "Count issues in a company"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["companies"],
summary: route[2],
});
}
registerCurrentRoute({
method: "get",
path: "/api/issues/{id}/cost-summary",
tags: ["costs"],
summary: "Get issue cost summary",
});
for (const route of [
["get", "/api/companies/{companyId}/resource-memberships/me", "List current user's resource memberships"],
["put", "/api/companies/{companyId}/resource-memberships/me/agents/{agentId}", "Join or leave an agent resource"],
["put", "/api/companies/{companyId}/resource-memberships/me/projects/{projectId}", "Join or leave a project resource"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["resource-memberships"],
summary: route[2],
...(route[0] === "put" ? { body: updateResourceMembershipSchema } : {}),
});
}
const cloudCompanyQuerySchema = z.object({
companyId: z.string().min(1),
});
const cloudCompanyBodySchema = z.object({
companyId: z.string().min(1),
});
const cloudConnectStartSchema = z.object({
companyId: z.string().min(1),
remoteUrl: z.string().min(1),
redirectUri: z.string().min(1),
});
const cloudConnectFinishSchema = z.object({
pendingConnectionId: z.string().min(1),
code: z.string().min(1),
state: z.string().min(1),
});
const cloudPushRunSchema = cloudCompanyBodySchema.extend({
retryOfRunId: z.string().optional(),
});
const cloudPushRunActivationSchema = cloudCompanyBodySchema.extend({
entityType: z.enum(["agents", "routines", "monitors"]),
});
registerCurrentRoute({
method: "get",
path: "/api/cloud-upstreams",
tags: ["cloud-upstreams"],
summary: "List cloud upstream connections",
query: cloudCompanyQuerySchema,
});
registerCurrentRoute({
method: "post",
path: "/api/cloud-upstreams/connect/start",
tags: ["cloud-upstreams"],
summary: "Start a cloud upstream connection",
body: cloudConnectStartSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/cloud-upstreams/connect/finish",
tags: ["cloud-upstreams"],
summary: "Finish a cloud upstream connection",
body: cloudConnectFinishSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/cloud-upstreams/{connectionId}/push-runs/preview",
tags: ["cloud-upstreams"],
summary: "Preview a cloud upstream push run",
body: cloudCompanyBodySchema,
});
registerCurrentRoute({
method: "post",
path: "/api/cloud-upstreams/{connectionId}/push-runs",
tags: ["cloud-upstreams"],
summary: "Create a cloud upstream push run",
body: cloudPushRunSchema,
});
registerCurrentRoute({
method: "get",
path: "/api/cloud-upstreams/{connectionId}/push-runs/{runId}",
tags: ["cloud-upstreams"],
summary: "Get a cloud upstream push run",
query: cloudCompanyQuerySchema,
});
registerCurrentRoute({
method: "post",
path: "/api/cloud-upstreams/{connectionId}/push-runs/{runId}/cancel",
tags: ["cloud-upstreams"],
summary: "Cancel a cloud upstream push run",
body: cloudCompanyBodySchema,
});
registerCurrentRoute({
method: "post",
path: "/api/cloud-upstreams/{connectionId}/push-runs/{runId}/activation",
tags: ["cloud-upstreams"],
summary: "Activate cloud upstream push run entities",
body: cloudPushRunActivationSchema,
});
for (const route of [
["get", "/api/companies/{companyId}/secret-providers/health", "Check configured secret providers"],
["get", "/api/companies/{companyId}/secret-provider-configs", "List secret provider configurations"],
["get", "/api/secret-provider-configs/{id}", "Get a secret provider configuration"],
["delete", "/api/secret-provider-configs/{id}", "Delete a secret provider configuration"],
["post", "/api/secret-provider-configs/{id}/default", "Set the default secret provider configuration"],
["post", "/api/secret-provider-configs/{id}/health", "Check a secret provider configuration"],
["get", "/api/secrets/{id}/usage", "Get secret usage"],
["get", "/api/secrets/{id}/access-events", "List secret access events"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["secrets"],
summary: route[2],
});
}
registerCurrentRoute({
method: "post",
path: "/api/companies/{companyId}/secret-provider-configs",
tags: ["secrets"],
summary: "Create a secret provider configuration",
body: createSecretProviderConfigSchema,
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registerCurrentRoute({
method: "patch",
path: "/api/secret-provider-configs/{id}",
tags: ["secrets"],
summary: "Update a secret provider configuration",
body: updateSecretProviderConfigSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/companies/{companyId}/secret-provider-configs/discovery/preview",
tags: ["secrets"],
summary: "Preview secret provider discovery",
body: secretProviderConfigDiscoveryPreviewSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/companies/{companyId}/secrets/remote-import/preview",
tags: ["secrets"],
summary: "Preview remote secret import",
body: remoteSecretImportPreviewSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/companies/{companyId}/secrets/remote-import",
tags: ["secrets"],
summary: "Import remote secrets",
body: remoteSecretImportSchema,
});
for (const route of [
["get", "/api/skills/catalog", "List catalog skills"],
["get", "/api/skills/catalog/{catalogId}", "Get a catalog skill"],
["get", "/api/skills/catalog/{catalogId}/files", "List catalog skill files"],
["post", "/api/companies/{companyId}/skills/install-catalog", "Install a catalog skill"],
["post", "/api/companies/{companyId}/skills/{skillId}/audit", "Audit a company skill"],
["post", "/api/companies/{companyId}/skills/{skillId}/reset", "Reset a company skill"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["skills"],
summary: route[2],
...(route[0] === "post" ? { body: z.record(z.unknown()).optional() } : {}),
});
}
registerCurrentRoute({
method: "post",
path: "/api/instance/settings/experimental/issue-graph-liveness-auto-recovery/preview",
tags: ["instance-settings"],
summary: "Preview issue graph liveness auto-recovery",
body: issueGraphLivenessAutoRecoveryRequestSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/instance/settings/experimental/issue-graph-liveness-auto-recovery/run",
tags: ["instance-settings"],
summary: "Run issue graph liveness auto-recovery",
body: issueGraphLivenessAutoRecoveryRequestSchema,
});
registerCurrentRoute({
method: "get",
path: "/api/issues/{id}/accepted-plan-decompositions",
tags: ["issues"],
summary: "List accepted plan decompositions",
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/accepted-plan-decompositions",
tags: ["issues"],
summary: "Create accepted plan decomposition child issues",
body: createAcceptedPlanDecompositionSchema,
});
for (const route of [
["get", "/api/issues/{id}/documents/{key}/annotations", "List document annotation threads"],
["get", "/api/issues/{id}/documents/{key}/annotations/{threadId}", "Get a document annotation thread"],
["post", "/api/issues/{id}/documents/{key}/lock", "Lock an issue document"],
["post", "/api/issues/{id}/documents/{key}/unlock", "Unlock an issue document"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["issues"],
summary: route[2],
});
}
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/documents/{key}/annotations",
tags: ["issues"],
summary: "Create a document annotation thread",
body: createDocumentAnnotationThreadSchema,
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/documents/{key}/annotations/{threadId}/comments",
tags: ["issues"],
summary: "Add a document annotation comment",
body: createDocumentAnnotationCommentSchema,
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/low-trust/promotions",
tags: ["issues"],
summary: "Promote quarantined low-trust output",
body: z.object({
sourceArtifactKind: z.enum(["comment", "document", "work_product", "issue"]),
sourceArtifactId: z.string().uuid(),
title: z.string().trim().min(1).max(200),
summary: z.string().trim().min(1).max(8_000),
}),
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
});
registerCurrentRoute({
method: "patch",
path: "/api/issues/{id}/documents/{key}/annotations/{threadId}",
tags: ["issues"],
summary: "Update a document annotation thread",
body: updateDocumentAnnotationThreadSchema,
});
registerCurrentRoute({
method: "get",
path: "/api/issues/{id}/recovery-actions",
tags: ["issues"],
summary: "List issue recovery actions",
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/recovery-actions/resolve",
tags: ["issues"],
summary: "Resolve an issue recovery action",
body: resolveIssueRecoveryActionSchema,
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/scheduled-retry/retry-now",
tags: ["issues"],
summary: "Retry a scheduled issue run now",
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/monitor/check-now",
tags: ["issues"],
summary: "Run an issue monitor check now",
});
registerCurrentRoute({
method: "post",
path: "/api/issues/{id}/interactions/{interactionId}/cancel",
tags: ["issues"],
summary: "Cancel an issue question interaction",
body: cancelIssueThreadInteractionSchema,
});
for (const route of [
["get", "/api/routines/{id}/revisions", "List routine revisions"],
["post", "/api/routines/{id}/revisions/{revisionId}/restore", "Restore a routine revision"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["routines"],
summary: route[2],
});
}
const pluginLocalFolderRequestSchema = z.object({
path: z.string().min(1),
access: z.enum(["read", "readWrite"]).optional(),
requiredDirectories: z.array(z.string()).optional(),
requiredFiles: z.array(z.string()).optional(),
});
for (const route of [
["get", "/api/plugins/{pluginId}/companies/{companyId}/local-folders", "List plugin local folders"],
["get", "/api/plugins/{pluginId}/companies/{companyId}/local-folders/{folderKey}/status", "Get plugin local folder status"],
["post", "/api/plugins/{pluginId}/companies/{companyId}/local-folders/{folderKey}/validate", "Validate a plugin local folder"],
["put", "/api/plugins/{pluginId}/companies/{companyId}/local-folders/{folderKey}", "Save a plugin local folder"],
] as const) {
registerCurrentRoute({
method: route[0],
path: route[1],
tags: ["plugins"],
summary: route[2],
...(route[0] === "post" || route[0] === "put" ? { body: pluginLocalFolderRequestSchema } : {}),
});
}
// ─── Spec builder ─────────────────────────────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function buildOpenApiDocument(): any {
return applyDocumentFixups({
openapi: "3.0.0",
info: {
title: "Paperclip API",
version: "1.0.0",
description: "REST API for the Paperclip AI agent management platform",
},
servers: [{ url: "/" }],
components: registry.buildComponents(),
paths: registry.buildPaths(),
});
}
export const buildOpenApiSpec = buildOpenApiDocument;
export function openApiRoutes() {
const router = Router();
router.get("/openapi.json", (_req, res) => {
res.json(buildOpenApiDocument());
});
return router;
}