paperclip/doc/plans/2026-05-23-cli-api-parity-openapi-reference.ts
Aron Prins 70b1a9109d
Improve CLI API parity coverage (#6626)
## Thinking Path

> - Paperclip is a control plane for AI-agent companies, with the CLI
acting as a scriptable operator and agent interface to that control
plane.
> - The REST API surface has grown across companies, agents, issues,
routines, plugins, auth, workspaces, secrets, and operational inspection
commands.
> - The CLI had drifted from that API surface: some commands were
missing, some command shapes differed from docs/reference material, and
several edge cases only failed during end-to-end local-source testing.
> - The local development runbook requires these tests to be disposable
and isolated from a real `~/.paperclip`, `~/.codex`, or `~/.claude`
installation.
> - This pull request adds broad CLI/API parity coverage, fixes the
actionable bugs found during that pass, and records the reproducible
test log under `doc/logs`.
> - The benefit is a more complete, scriptable CLI surface with
regression coverage for the command families exercised by the parity
run.

## What Changed

- Added or expanded CLI command coverage for access/auth, companies,
agents, projects, goals, issues and subresources, routines, plugins,
workspaces, activity/run/cost/dashboard inspection, assets, skills,
secrets, tokens, prompt/wake flows, and local setup helpers.
- Fixed CLI/API parity bugs found during the run, including context
profile patching, issue interaction optional payloads, malformed
tree-hold errors, environment duplicate handling, configure
invalid-section exit codes, worktree pnpm invocation, token agent ID
resolution, plugin tool worker lookup, and routine webhook secret
cleanup.
- Added missing CLI wrappers and route coverage for health/access,
invite resolution URL forwarding, join status normalization, secret
lifecycle commands, LLM docs routes, available-skill isolation, positive
board-claim coverage, and interactive `connect` prompt-flow tests.
- Added a schema-backed `/api/openapi.json` route sufficient for CLI
parity and `paperclipai openapi --json` smoke coverage.
- Added `doc/logs/2026-05-24-cli-api-parity-e2e-log.md` with the
detailed living test/bug log and renamed the log directory from
`doc/bugs` to `doc/logs`.
- Added `doc/plans/2026-05-23-cli-api-parity.md` and the OpenAPI parity
reference used during the pass.

OpenAPI note: this PR intentionally does not try to subsume
`feature/openapi-spec`. The OpenAPI implementation here is schema-backed
and better than the earlier route-inventory stub, but
`feature/openapi-spec` is the fuller/better OpenAPI branch because it
includes exact mounted-route coverage tests and additional current route
coverage. That branch should stay as its own PR and can supersede this
OpenAPI route implementation.

## Verification

Targeted automated checks run:

- `pnpm exec vitest run server/src/__tests__/openapi-routes.test.ts`
- `pnpm exec vitest run server/src/__tests__/board-claim.test.ts`
- `pnpm exec vitest run cli/src/__tests__/connect.test.ts`
- `pnpm exec vitest run cli/src/__tests__/agent-lifecycle.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts`
- `pnpm exec vitest run server/src/__tests__/routines-service.test.ts`
- `pnpm --dir cli typecheck`
- `pnpm --dir server typecheck`

Manual/local E2E verification:

- Ran the full disposable local-source CLI/API parity pass with isolated
`PAPERCLIP_HOME`, `PAPERCLIP_CONFIG`, `PAPERCLIP_CONTEXT`,
`PAPERCLIP_AUTH_STORE`, `CODEX_HOME`, and `CLAUDE_HOME` under
`tmp/cli-api-parity`.
- Verified `DATABASE_URL` and `DATABASE_MIGRATION_URL` stayed unset for
the scratch server.
- Verified live health and schema-backed OpenAPI responses on
non-default port `3197`.
- Revoked created board/agent tokens and cleaned up temporary plugins,
secrets, non-default environments, and project workspaces.
- See `doc/logs/2026-05-24-cli-api-parity-e2e-log.md` for the full
command-by-command reproduction log.

Not run:

- Full `pnpm test`, `pnpm test:run`, or `pnpm build` were not run after
the entire branch because the branch is broad and the parity pass used
focused test/typecheck verification plus live isolated CLI reruns.

## Risks

- This is a broad PR and touches many CLI command modules, so review
surface is high. The changes are grouped around one theme, but a split
may be easier if maintainers prefer narrower PRs.
- The OpenAPI route in this PR is not the final/best OpenAPI
implementation. `feature/openapi-spec` has stronger exact-route coverage
and should remain the source for the dedicated OpenAPI PR.
- The living log is intentionally detailed and large. It is useful for
reproducibility but adds documentation weight.
- No UI changes are intended; screenshots are not applicable.

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

## Model Used

- OpenAI Codex, GPT-5-based coding agent in Codex desktop. Exact served
model/context-window identifier was not exposed in the local app. Work
used shell/Git/GitHub CLI tooling, local source inspection, targeted
test execution, and live isolated Paperclip CLI/API smoke testing.

## Checklist

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

---------

Co-authored-by: Devin Foley <devin@devinfoley.com>
2026-06-02 17:13:29 -07:00

3585 lines
109 KiB
TypeScript

import {
OpenAPIRegistry,
OpenApiGeneratorV3,
extendZodWithOpenApi,
} from "@asteasolutions/zod-to-openapi";
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,
// 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,
updateCompanyMemberSchema,
updateCompanyMemberWithPermissionsSchema,
archiveCompanyMemberSchema,
updateMemberPermissionsSchema,
updateUserCompanyAccessSchema,
// Instance settings
patchInstanceGeneralSettingsSchema,
patchInstanceExperimentalSettingsSchema,
} from "@paperclipai/shared";
extendZodWithOpenApi(z);
const registry = new OpenAPIRegistry();
// ─── Common schemas ──────────────────────────────────────────────────────────
const ErrorSchema = registry.register(
"Error",
z.object({ error: z.string() }).openapi({ title: "Error" }),
);
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 } },
},
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;
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/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",
"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/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}/environments",
"POST /api/companies/{companyId}/goals",
"POST /api/companies/{companyId}/labels",
"POST /api/issues/{id}/work-products",
"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/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: "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 },
});
// ─── 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 },
});
// ─── Spec builder ─────────────────────────────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function buildOpenApiSpec(): any {
const generator = new OpenApiGeneratorV3(registry.definitions);
return applyDocumentFixups(generator.generateDocument({
openapi: "3.0.0",
info: {
title: "Paperclip API",
version: "1.0.0",
description: "REST API for the Paperclip AI agent management platform",
},
servers: [{ url: "/" }],
}));
}